From bae2b6f379b140aa4e14e18d78ed63cca116600d Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Thu, 12 Dec 2024 11:16:53 +0100 Subject: [PATCH] feat(cdp): mapping (#26655) Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> --- .../hogfunctions/HogFunctionConfiguration.tsx | 8 +- .../hogfunctions/HogFunctionInputs.tsx | 72 ++++++-- .../filters/HogFunctionFilters.tsx | 147 ++++++++-------- .../hogFunctionConfigurationLogic.tsx | 133 +++++++++----- .../mapping/HogFunctionMapping.tsx | 162 ++++++++++++++++++ frontend/src/types.ts | 11 +- posthog/api/hog_function.py | 59 ++++--- posthog/api/hog_function_template.py | 8 +- .../api/test/__snapshots__/test_decide.ambr | 11 ++ .../test_early_access_feature.ambr | 4 + .../test_organization_feature_flag.ambr | 2 + .../api/test/__snapshots__/test_survey.ambr | 5 + posthog/api/test/test_hog_function.py | 2 + .../api/test/test_hog_function_templates.py | 1 + posthog/cdp/filters.py | 7 +- posthog/cdp/site_functions.py | 55 ++++-- .../cdp/templates/_internal/template_blank.py | 87 +++++++++- .../cdp/templates/hog_function_template.py | 8 + posthog/cdp/test/test_site_functions.py | 34 +++- .../migrations/0529_hog_function_mappings.py | 17 ++ posthog/migrations/max_migration.txt | 2 +- posthog/models/hog_functions/hog_function.py | 1 + posthog/models/test/test_remote_config.py | 20 ++- .../test_process_scheduled_changes.ambr | 3 + .../test/__snapshots__/test_feature_flag.ambr | 5 + 25 files changed, 683 insertions(+), 181 deletions(-) create mode 100644 frontend/src/scenes/pipeline/hogfunctions/mapping/HogFunctionMapping.tsx create mode 100644 posthog/migrations/0529_hog_function_mappings.py diff --git a/frontend/src/scenes/pipeline/hogfunctions/HogFunctionConfiguration.tsx b/frontend/src/scenes/pipeline/hogfunctions/HogFunctionConfiguration.tsx index f837bc49fe7b3..b84ca60f244ab 100644 --- a/frontend/src/scenes/pipeline/hogfunctions/HogFunctionConfiguration.tsx +++ b/frontend/src/scenes/pipeline/hogfunctions/HogFunctionConfiguration.tsx @@ -35,6 +35,7 @@ import { HogFunctionIconEditable } from './HogFunctionIcon' import { HogFunctionInputs } from './HogFunctionInputs' import { HogFunctionStatusIndicator } from './HogFunctionStatusIndicator' import { HogFunctionTest, HogFunctionTestPlaceholder } from './HogFunctionTest' +import { HogFunctionMapping } from './mapping/HogFunctionMapping' const EVENT_THRESHOLD_ALERT_LEVEL = 8000 @@ -396,7 +397,10 @@ export function HogFunctionConfiguration({ templateId, id }: HogFunctionConfigur
- + {showSource && canEditSource ? ( } @@ -421,6 +425,8 @@ export function HogFunctionConfiguration({ templateId, id }: HogFunctionConfigur
+ + {canEditSource && (
void +} + export type HogFunctionInputWithSchemaProps = { + configuration: HogFunctionConfigurationType | HogFunctionMappingType + setConfigurationValue: (key: string, value: any) => void schema: HogFunctionInputSchemaType } @@ -196,9 +208,15 @@ type HogFunctionInputSchemaControlsProps = { value: HogFunctionInputSchemaType onChange: (value: HogFunctionInputSchemaType | null) => void onDone: () => void + supportsSecrets: boolean } -function HogFunctionInputSchemaControls({ value, onChange, onDone }: HogFunctionInputSchemaControlsProps): JSX.Element { +function HogFunctionInputSchemaControls({ + value, + onChange, + onDone, + supportsSecrets, +}: HogFunctionInputSchemaControlsProps): JSX.Element { const _onChange = (data: Partial | null): void => { if (data?.key?.length === 0) { setLocalVariableError('Input variable name cannot be empty') @@ -230,13 +248,15 @@ function HogFunctionInputSchemaControls({ value, onChange, onDone }: HogFunction label="Required" bordered /> - _onChange({ secret })} - label="Secret" - bordered - /> + {supportsSecrets ? ( + _onChange({ secret })} + label="Secret" + bordered + /> + ) : null}
} size="small" onClick={() => onChange(null)} /> onDone()}> @@ -314,10 +334,13 @@ function HogFunctionInputSchemaControls({ value, onChange, onDone }: HogFunction ) } -export function HogFunctionInputWithSchema({ schema }: HogFunctionInputWithSchemaProps): JSX.Element { +export function HogFunctionInputWithSchema({ + schema, + configuration, + setConfigurationValue, +}: HogFunctionInputWithSchemaProps): JSX.Element { const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id: schema.key }) - const { showSource, configuration } = useValues(hogFunctionConfigurationLogic) - const { setConfigurationValue } = useActions(hogFunctionConfigurationLogic) + const { showSource } = useValues(hogFunctionConfigurationLogic) const [editing, setEditing] = useState(false) const value = configuration.inputs?.[schema.key] @@ -349,6 +372,7 @@ export function HogFunctionInputWithSchema({ schema }: HogFunctionInputWithSchem }, [showSource]) const supportsTemplating = ['string', 'json', 'dictionary', 'email'].includes(schema.type) + const supportsSecrets = 'type' in configuration // no secrets for mapping inputs return (
setEditing(false)} + supportsSecrets={supportsSecrets} />
)} @@ -451,11 +476,17 @@ export function HogFunctionInputWithSchema({ schema }: HogFunctionInputWithSchem ) } -export function HogFunctionInputs(): JSX.Element { - const { showSource, configuration } = useValues(hogFunctionConfigurationLogic) - const { setConfigurationValue } = useActions(hogFunctionConfigurationLogic) +export function HogFunctionInputs({ + configuration, + setConfigurationValue, +}: HogFunctionInputsProps): JSX.Element | null { + const { showSource } = useValues(hogFunctionConfigurationLogic) if (!configuration?.inputs_schema?.length) { + if (!('type' in configuration)) { + // If this is a mapping, don't show any error message. + return null + } return This function does not require any input variables. } @@ -477,7 +508,14 @@ export function HogFunctionInputs(): JSX.Element { > {configuration.inputs_schema?.map((schema) => { - return + return ( + + ) })} diff --git a/frontend/src/scenes/pipeline/hogfunctions/filters/HogFunctionFilters.tsx b/frontend/src/scenes/pipeline/hogfunctions/filters/HogFunctionFilters.tsx index 1dd5588045f54..60d65c3595e0b 100644 --- a/frontend/src/scenes/pipeline/hogfunctions/filters/HogFunctionFilters.tsx +++ b/frontend/src/scenes/pipeline/hogfunctions/filters/HogFunctionFilters.tsx @@ -45,7 +45,7 @@ function sanitizeActionFilters(filters?: FilterType): Partial - - {({ value, onChange }) => ( - <> - onChange({ ...value, filter_test_accounts })} - fullWidth - /> - { - onChange({ - ...value, - properties, - }) - }} - pageKey={`HogFunctionPropertyFilters.${id}`} - /> + + {({ value, onChange }) => { + const filters = (value ?? {}) as HogFunctionFiltersType + return ( + <> + onChange({ ...filters, filter_test_accounts })} + fullWidth + /> + { + onChange({ + ...filters, + properties, + }) + }} + pageKey={`HogFunctionPropertyFilters.${id}`} + /> - Match event and actions -

- If set, the destination will only run if the event matches any of the below. -

- { - onChange({ - ...value, - ...sanitizeActionFilters(payload), - }) - }} - typeKey="plugin-filters" - mathAvailability={MathAvailability.None} - hideRename - hideDuplicate - showNestedArrow={false} - actionsTaxonomicGroupTypes={[ - TaxonomicFilterGroupType.Events, - TaxonomicFilterGroupType.Actions, - ]} - propertiesTaxonomicGroupTypes={[ - TaxonomicFilterGroupType.EventProperties, - TaxonomicFilterGroupType.EventFeatureFlags, - TaxonomicFilterGroupType.Elements, - TaxonomicFilterGroupType.PersonProperties, - TaxonomicFilterGroupType.HogQLExpression, - ...groupsTaxonomicTypes, - ]} - propertyFiltersPopover - addFilterDefaultOptions={{ - id: '$pageview', - name: '$pageview', - type: EntityTypes.EVENTS, - }} - buttonCopy="Add event matcher" - /> - - )} + {!useMapping ? ( + <> +
+ Match events and actions +
+

+ If set, the destination will only run if the event matches any of the + below. +

+ { + onChange({ + ...value, + ...sanitizeActionFilters(payload), + }) + }} + typeKey="plugin-filters" + mathAvailability={MathAvailability.None} + hideRename + hideDuplicate + showNestedArrow={false} + actionsTaxonomicGroupTypes={[ + TaxonomicFilterGroupType.Events, + TaxonomicFilterGroupType.Actions, + ]} + propertiesTaxonomicGroupTypes={[ + TaxonomicFilterGroupType.EventProperties, + TaxonomicFilterGroupType.EventFeatureFlags, + TaxonomicFilterGroupType.Elements, + TaxonomicFilterGroupType.PersonProperties, + TaxonomicFilterGroupType.HogQLExpression, + ...groupsTaxonomicTypes, + ]} + propertyFiltersPopover + addFilterDefaultOptions={{ + id: '$pageview', + name: '$pageview', + type: EntityTypes.EVENTS, + }} + buttonCopy="Add event matcher" + /> + + ) : null} + + ) + }}
- {showMasking ? ( {({ value, onChange }) => ( diff --git a/frontend/src/scenes/pipeline/hogfunctions/hogFunctionConfigurationLogic.tsx b/frontend/src/scenes/pipeline/hogfunctions/hogFunctionConfigurationLogic.tsx index 229abea7e424d..7a870913f2d24 100644 --- a/frontend/src/scenes/pipeline/hogfunctions/hogFunctionConfigurationLogic.tsx +++ b/frontend/src/scenes/pipeline/hogfunctions/hogFunctionConfigurationLogic.tsx @@ -29,8 +29,10 @@ import { EventType, FilterLogicalOperator, HogFunctionConfigurationType, + HogFunctionInputSchemaType, HogFunctionInputType, HogFunctionInvocationGlobals, + HogFunctionMappingType, HogFunctionSubTemplateIdType, HogFunctionSubTemplateType, HogFunctionTemplateType, @@ -64,38 +66,46 @@ const NEW_FUNCTION_TEMPLATE: HogFunctionTemplateType = { } export function sanitizeConfiguration(data: HogFunctionConfigurationType): HogFunctionConfigurationType { - const sanitizedInputs: Record = {} - - data.inputs_schema?.forEach((input) => { - const secret = data.inputs?.[input.key]?.secret - let value = data.inputs?.[input.key]?.value - - if (secret) { - // If set this means we haven't changed the value - sanitizedInputs[input.key] = { - value: '********', // Don't send the actual value - secret: true, + function sanitizeInputs( + data: HogFunctionConfigurationType | HogFunctionMappingType + ): Record { + const sanitizedInputs: Record = {} + data.inputs_schema?.forEach((input) => { + const secret = data.inputs?.[input.key]?.secret + let value = data.inputs?.[input.key]?.value + + if (secret) { + // If set this means we haven't changed the value + sanitizedInputs[input.key] = { + value: '********', // Don't send the actual value + secret: true, + } + return } - return - } - if (input.type === 'json' && typeof value === 'string') { - try { - value = JSON.parse(value) - } catch (e) { - // Ignore + if (input.type === 'json' && typeof value === 'string') { + try { + value = JSON.parse(value) + } catch (e) { + // Ignore + } } - } - sanitizedInputs[input.key] = { - value: value, - } - }) + sanitizedInputs[input.key] = { + value: value, + } + }) + return sanitizedInputs + } const payload: HogFunctionConfigurationType = { ...data, filters: data.filters, - inputs: sanitizedInputs, + mappings: data.mappings?.map((mapping) => ({ + ...mapping, + inputs: sanitizeInputs(mapping), + })), + inputs: sanitizeInputs(data), masking: data.masking?.hash ? data.masking : null, icon_url: data.icon_url, } @@ -107,15 +117,32 @@ const templateToConfiguration = ( template: HogFunctionTemplateType, subTemplate?: HogFunctionSubTemplateType | null ): HogFunctionConfigurationType => { - const inputs: Record = {} + function getInputs( + inputs_schema?: HogFunctionInputSchemaType[] | null, + subTemplate?: HogFunctionSubTemplateType | null + ): Record { + const inputs: Record = {} + inputs_schema?.forEach((schema) => { + if (typeof subTemplate?.inputs?.[schema.key] !== 'undefined') { + inputs[schema.key] = { value: subTemplate.inputs[schema.key] } + } else if (schema.default !== undefined) { + inputs[schema.key] = { value: schema.default } + } + }) + return inputs + } - template.inputs_schema?.forEach((schema) => { - if (typeof subTemplate?.inputs?.[schema.key] !== 'undefined') { - inputs[schema.key] = { value: subTemplate.inputs[schema.key] } - } else if (schema.default !== undefined) { - inputs[schema.key] = { value: schema.default } - } - }) + function getMappingInputs( + inputs_schema?: HogFunctionInputSchemaType[] | null + ): Record { + const inputs: Record = {} + inputs_schema?.forEach((schema) => { + if (schema.default !== undefined) { + inputs[schema.key] = { value: schema.default } + } + }) + return inputs + } return { type: template.type ?? 'destination', @@ -123,9 +150,15 @@ const templateToConfiguration = ( description: subTemplate?.name ?? template.description, inputs_schema: template.inputs_schema, filters: subTemplate?.filters ?? template.filters, + mappings: (subTemplate?.mappings ?? template.mappings)?.map( + (mapping): HogFunctionMappingType => ({ + ...mapping, + inputs: getMappingInputs(mapping.inputs_schema), + }) + ), hog: template.hog, icon_url: template.icon_url, - inputs, + inputs: getInputs(template.inputs_schema, subTemplate), enabled: template.type !== 'broadcast', } } @@ -139,7 +172,6 @@ export function convertToHogFunctionInvocationGlobals( return { project: { id: team?.id ?? 0, - name: team?.name ?? 'Default project', url: projectUrl, }, @@ -299,7 +331,7 @@ export const hogFunctionConfigurationLogic = kea { - if (values.type !== 'destination') { + if (values.type !== 'destination' && values.type !== 'site_destination') { return null } if (values.sparkline === null) { @@ -423,10 +455,9 @@ export const hogFunctionConfigurationLogic = kea { - const payload = sanitizeConfiguration(data) - + const payload: Record = sanitizeConfiguration(data) // Only sent on create - ;(payload as any).template_id = props.templateId || values.hogFunction?.template?.id + payload.template_id = props.templateId || values.hogFunction?.template?.id if (!values.hasAddon) { // Remove the source field if the user doesn't have the addon @@ -434,7 +465,7 @@ export const hogFunctionConfigurationLogic = kea [s.hogFunction, s.template], + (hogFunction, template) => (hogFunction ?? template)?.type === 'site_destination', + ], defaultFormState: [ (s) => [s.template, s.hogFunction, s.subTemplate], (template, hogFunction, subTemplate): HogFunctionConfigurationType | null => { @@ -614,7 +649,21 @@ export const hogFunctionConfigurationLogic = kea [s.configuration, s.matchingFilters, s.type], (configuration, matchingFilters, type): TrendsQuery | null => { - if (type !== 'destination') { + if (type !== 'destination' && type !== 'site_destination') { return null } return { diff --git a/frontend/src/scenes/pipeline/hogfunctions/mapping/HogFunctionMapping.tsx b/frontend/src/scenes/pipeline/hogfunctions/mapping/HogFunctionMapping.tsx new file mode 100644 index 0000000000000..08cd1aeceee8d --- /dev/null +++ b/frontend/src/scenes/pipeline/hogfunctions/mapping/HogFunctionMapping.tsx @@ -0,0 +1,162 @@ +import { IconPlus, IconPlusSmall, IconTrash } from '@posthog/icons' +import { LemonButton, LemonLabel } from '@posthog/lemon-ui' +import { useValues } from 'kea' +import { Group } from 'kea-forms' +import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' +import { LemonField } from 'lib/lemon-ui/LemonField' +import { getDefaultEventName } from 'lib/utils/getAppContext' +import { ActionFilter } from 'scenes/insights/filters/ActionFilter/ActionFilter' +import { MathAvailability } from 'scenes/insights/filters/ActionFilter/ActionFilterRow/ActionFilterRow' + +import { groupsModel } from '~/models/groupsModel' +import { EntityTypes, FilterType, HogFunctionConfigurationType, HogFunctionMappingType } from '~/types' + +import { hogFunctionConfigurationLogic } from '../hogFunctionConfigurationLogic' +import { HogFunctionInputs } from '../HogFunctionInputs' + +export function HogFunctionMapping(): JSX.Element | null { + const { groupsTaxonomicTypes } = useValues(groupsModel) + const { useMapping, showSource } = useValues(hogFunctionConfigurationLogic) + + if (!useMapping) { + return null + } + + return ( + + {({ value, onChange }) => { + const mappings = (value ?? []) as HogFunctionMappingType[] + return ( + <> + {mappings.map((mapping, index) => ( +
+
+ + Mapping #{index + 1} + + {mappings.length > 1 ? ( + } + title="Delete graph series" + data-attr={`delete-prop-filter-${index}`} + noPadding + onClick={() => onChange(mappings.filter((_, i) => i !== index))} + /> + ) : null} +
+ + onChange(mappings.map((m, i) => (i === index ? { ...m, filters: f } : m))) + } + typeKey={`match-group-${index}`} + mathAvailability={MathAvailability.None} + hideRename + hideDuplicate + showNestedArrow={false} + actionsTaxonomicGroupTypes={[ + TaxonomicFilterGroupType.Events, + TaxonomicFilterGroupType.Actions, + ]} + propertiesTaxonomicGroupTypes={[ + TaxonomicFilterGroupType.EventProperties, + TaxonomicFilterGroupType.EventFeatureFlags, + TaxonomicFilterGroupType.Elements, + TaxonomicFilterGroupType.PersonProperties, + TaxonomicFilterGroupType.HogQLExpression, + ...groupsTaxonomicTypes, + ]} + propertyFiltersPopover + addFilterDefaultOptions={{ + id: '$pageview', + name: '$pageview', + type: EntityTypes.EVENTS, + }} + buttonCopy="Add event matcher" + /> + + { + onChange(mappings.map((m, i) => (i === index ? { ...m, [key]: value } : m))) + }} + /> + + {showSource ? ( + } + size="small" + type="secondary" + className="my-4" + onClick={() => { + onChange( + mappings.map((m, i) => { + if (i !== index) { + return m + } + const inputs_schema = m.inputs_schema ?? [] + return { + ...m, + inputs_schema: [ + ...inputs_schema, + { + type: 'string', + key: `var_${inputs_schema.length + 1}`, + label: '', + required: false, + }, + ], + } + }) + ) + }} + > + Add input variable + + ) : null} +
+ ))} +
+ } + onClick={() => { + const inputsSchema = + mappings.length > 0 + ? structuredClone(mappings[mappings.length - 1].inputs_schema || []) + : [] + const newMapping = { + inputs_schema: inputsSchema, + inputs: Object.fromEntries( + inputsSchema + .filter((m) => m.default !== undefined) + .map((m) => [m.key, { value: structuredClone(m.default) }]) + ), + filters: { + events: [ + { + id: getDefaultEventName(), + name: getDefaultEventName(), + type: EntityTypes.EVENTS, + order: 0, + properties: [], + }, + ], + actions: [], + }, + } + onChange([...mappings, newMapping]) + }} + > + Add mapping + +
+ + ) + }} +
+ ) +} diff --git a/frontend/src/types.ts b/frontend/src/types.ts index 1b13624434254..9317f2356aeee 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -4654,6 +4654,12 @@ export interface HogFunctionFiltersType { bytecode_error?: string } +export interface HogFunctionMappingType { + inputs_schema?: HogFunctionInputSchemaType[] + inputs?: Record | null + filters?: HogFunctionFiltersType | null +} + export type HogFunctionTypeType = | 'destination' | 'site_destination' @@ -4679,6 +4685,7 @@ export type HogFunctionType = { inputs_schema?: HogFunctionInputSchemaType[] inputs?: Record | null + mappings?: HogFunctionMappingType[] | null masking?: HogFunctionMasking | null filters?: HogFunctionFiltersType | null template?: HogFunctionTemplateType @@ -4696,7 +4703,7 @@ export type HogFunctionConfigurationType = Omit< sub_template_id?: HogFunctionSubTemplateIdType } -export type HogFunctionSubTemplateType = Pick & { +export type HogFunctionSubTemplateType = Pick & { id: HogFunctionSubTemplateIdType name: string description: string | null @@ -4704,7 +4711,7 @@ export type HogFunctionSubTemplateType = Pick & { status: HogFunctionTemplateStatus sub_templates?: HogFunctionSubTemplateType[] diff --git a/posthog/api/hog_function.py b/posthog/api/hog_function.py index a382ebda866e0..9e47a1da1cdea 100644 --- a/posthog/api/hog_function.py +++ b/posthog/api/hog_function.py @@ -107,6 +107,7 @@ class Meta: "inputs", "filters", "masking", + "mappings", "icon_url", "template", "template_id", @@ -153,6 +154,7 @@ def validate(self, attrs): # Without the addon, they cannot deviate from the template attrs["inputs_schema"] = template.inputs_schema + attrs["mappings"] = template.mappings attrs["hog"] = template.hog if "type" not in attrs: @@ -163,29 +165,40 @@ def validate(self, attrs): attrs["filters"] = attrs.get("filters") or {} attrs["inputs_schema"] = attrs.get("inputs_schema") or [] attrs["inputs"] = attrs.get("inputs") or {} - - if "inputs_schema" in attrs: - attrs["inputs_schema"] = validate_inputs_schema(attrs["inputs_schema"]) - - if "inputs" in attrs: - inputs = attrs["inputs"] or {} - existing_encrypted_inputs = None - - if instance and instance.encrypted_inputs: - existing_encrypted_inputs = instance.encrypted_inputs - - attrs["inputs_schema"] = attrs.get("inputs_schema", instance.inputs_schema if instance else []) - attrs["inputs"] = validate_inputs(attrs["inputs_schema"], inputs, existing_encrypted_inputs, attrs["type"]) - - if "filters" in attrs: - if attrs["type"] in TYPES_WITH_COMPILED_FILTERS: - attrs["filters"] = compile_filters_bytecode(attrs["filters"], team) - elif attrs["type"] in TYPES_WITH_TRANSPILED_FILTERS: - compiler = JavaScriptCompiler() - code = compiler.visit(compile_filters_expr(attrs["filters"], team)) - attrs["filters"]["transpiled"] = {"lang": "ts", "code": code, "stl": list(compiler.stl_functions)} - if "bytecode" in attrs["filters"]: - del attrs["filters"]["bytecode"] + attrs["mappings"] = attrs.get("mappings") or None + + # Used for both top level input validation, and mappings input validation + def validate_input_and_filters(attrs: dict, type: str): + if "inputs_schema" in attrs: + attrs["inputs_schema"] = validate_inputs_schema(attrs["inputs_schema"]) + + if "inputs" in attrs: + inputs = attrs["inputs"] or {} + existing_encrypted_inputs = None + + if instance and instance.encrypted_inputs: + existing_encrypted_inputs = instance.encrypted_inputs + + attrs["inputs_schema"] = attrs.get("inputs_schema", instance.inputs_schema if instance else []) + attrs["inputs"] = validate_inputs(attrs["inputs_schema"], inputs, existing_encrypted_inputs, type) + + if "filters" in attrs: + if type in TYPES_WITH_COMPILED_FILTERS: + attrs["filters"] = compile_filters_bytecode(attrs["filters"], team) + elif type in TYPES_WITH_TRANSPILED_FILTERS: + compiler = JavaScriptCompiler() + code = compiler.visit(compile_filters_expr(attrs["filters"], team)) + attrs["filters"]["transpiled"] = {"lang": "ts", "code": code, "stl": list(compiler.stl_functions)} + if "bytecode" in attrs["filters"]: + del attrs["filters"]["bytecode"] + + validate_input_and_filters(attrs, attrs["type"]) + + if attrs.get("mappings", None) is not None: + if attrs["type"] != "site_destination": + raise serializers.ValidationError({"mappings": "Mappings are only allowed for site destinations."}) + for mapping in attrs["mappings"]: + validate_input_and_filters(mapping, attrs["type"]) if "hog" in attrs: if attrs["type"] in TYPES_WITH_JAVASCRIPT_SOURCE: diff --git a/posthog/api/hog_function_template.py b/posthog/api/hog_function_template.py index 2044affa77075..64023aa5c843a 100644 --- a/posthog/api/hog_function_template.py +++ b/posthog/api/hog_function_template.py @@ -6,13 +6,18 @@ from rest_framework.exceptions import NotFound from posthog.cdp.templates import HOG_FUNCTION_TEMPLATES -from posthog.cdp.templates.hog_function_template import HogFunctionTemplate, HogFunctionSubTemplate +from posthog.cdp.templates.hog_function_template import HogFunctionMapping, HogFunctionTemplate, HogFunctionSubTemplate from rest_framework_dataclasses.serializers import DataclassSerializer logger = structlog.get_logger(__name__) +class HogFunctionMappingSerializer(DataclassSerializer): + class Meta: + dataclass = HogFunctionMapping + + class HogFunctionSubTemplateSerializer(DataclassSerializer): class Meta: dataclass = HogFunctionSubTemplate @@ -20,6 +25,7 @@ class Meta: class HogFunctionTemplateSerializer(DataclassSerializer): sub_templates = HogFunctionSubTemplateSerializer(many=True, required=False) + mappings = HogFunctionMappingSerializer(many=True, required=False) class Meta: dataclass = HogFunctionTemplate diff --git a/posthog/api/test/__snapshots__/test_decide.ambr b/posthog/api/test/__snapshots__/test_decide.ambr index 183687699249e..5caca4e947152 100644 --- a/posthog/api/test/__snapshots__/test_decide.ambr +++ b/posthog/api/test/__snapshots__/test_decide.ambr @@ -265,6 +265,7 @@ "posthog_hogfunction"."inputs", "posthog_hogfunction"."encrypted_inputs", "posthog_hogfunction"."filters", + "posthog_hogfunction"."mappings", "posthog_hogfunction"."masking", "posthog_hogfunction"."template_id", "posthog_team"."id", @@ -635,6 +636,7 @@ "posthog_hogfunction"."inputs", "posthog_hogfunction"."encrypted_inputs", "posthog_hogfunction"."filters", + "posthog_hogfunction"."mappings", "posthog_hogfunction"."masking", "posthog_hogfunction"."template_id" FROM "posthog_hogfunction" @@ -1090,6 +1092,7 @@ "posthog_hogfunction"."inputs", "posthog_hogfunction"."encrypted_inputs", "posthog_hogfunction"."filters", + "posthog_hogfunction"."mappings", "posthog_hogfunction"."masking", "posthog_hogfunction"."template_id", "posthog_team"."id", @@ -1521,6 +1524,7 @@ "posthog_hogfunction"."inputs", "posthog_hogfunction"."encrypted_inputs", "posthog_hogfunction"."filters", + "posthog_hogfunction"."mappings", "posthog_hogfunction"."masking", "posthog_hogfunction"."template_id" FROM "posthog_hogfunction" @@ -1823,6 +1827,7 @@ "posthog_hogfunction"."inputs", "posthog_hogfunction"."encrypted_inputs", "posthog_hogfunction"."filters", + "posthog_hogfunction"."mappings", "posthog_hogfunction"."masking", "posthog_hogfunction"."template_id" FROM "posthog_hogfunction" @@ -1954,6 +1959,7 @@ "posthog_hogfunction"."inputs", "posthog_hogfunction"."encrypted_inputs", "posthog_hogfunction"."filters", + "posthog_hogfunction"."mappings", "posthog_hogfunction"."masking", "posthog_hogfunction"."template_id", "posthog_team"."id", @@ -2385,6 +2391,7 @@ "posthog_hogfunction"."inputs", "posthog_hogfunction"."encrypted_inputs", "posthog_hogfunction"."filters", + "posthog_hogfunction"."mappings", "posthog_hogfunction"."masking", "posthog_hogfunction"."template_id" FROM "posthog_hogfunction" @@ -2679,6 +2686,7 @@ "posthog_hogfunction"."inputs", "posthog_hogfunction"."encrypted_inputs", "posthog_hogfunction"."filters", + "posthog_hogfunction"."mappings", "posthog_hogfunction"."masking", "posthog_hogfunction"."template_id" FROM "posthog_hogfunction" @@ -2912,6 +2920,7 @@ "posthog_hogfunction"."inputs", "posthog_hogfunction"."encrypted_inputs", "posthog_hogfunction"."filters", + "posthog_hogfunction"."mappings", "posthog_hogfunction"."masking", "posthog_hogfunction"."template_id" FROM "posthog_hogfunction" @@ -3211,6 +3220,7 @@ "posthog_hogfunction"."inputs", "posthog_hogfunction"."encrypted_inputs", "posthog_hogfunction"."filters", + "posthog_hogfunction"."mappings", "posthog_hogfunction"."masking", "posthog_hogfunction"."template_id" FROM "posthog_hogfunction" @@ -3288,6 +3298,7 @@ "posthog_hogfunction"."inputs", "posthog_hogfunction"."encrypted_inputs", "posthog_hogfunction"."filters", + "posthog_hogfunction"."mappings", "posthog_hogfunction"."masking", "posthog_hogfunction"."template_id", "posthog_team"."id", diff --git a/posthog/api/test/__snapshots__/test_early_access_feature.ambr b/posthog/api/test/__snapshots__/test_early_access_feature.ambr index 652fd9e16d409..20922177aa86c 100644 --- a/posthog/api/test/__snapshots__/test_early_access_feature.ambr +++ b/posthog/api/test/__snapshots__/test_early_access_feature.ambr @@ -366,6 +366,7 @@ "posthog_hogfunction"."inputs", "posthog_hogfunction"."encrypted_inputs", "posthog_hogfunction"."filters", + "posthog_hogfunction"."mappings", "posthog_hogfunction"."masking", "posthog_hogfunction"."template_id" FROM "posthog_hogfunction" @@ -776,6 +777,7 @@ "posthog_hogfunction"."inputs", "posthog_hogfunction"."encrypted_inputs", "posthog_hogfunction"."filters", + "posthog_hogfunction"."mappings", "posthog_hogfunction"."masking", "posthog_hogfunction"."template_id" FROM "posthog_hogfunction" @@ -1302,6 +1304,7 @@ "posthog_hogfunction"."inputs", "posthog_hogfunction"."encrypted_inputs", "posthog_hogfunction"."filters", + "posthog_hogfunction"."mappings", "posthog_hogfunction"."masking", "posthog_hogfunction"."template_id" FROM "posthog_hogfunction" @@ -1710,6 +1713,7 @@ "posthog_hogfunction"."inputs", "posthog_hogfunction"."encrypted_inputs", "posthog_hogfunction"."filters", + "posthog_hogfunction"."mappings", "posthog_hogfunction"."masking", "posthog_hogfunction"."template_id" FROM "posthog_hogfunction" diff --git a/posthog/api/test/__snapshots__/test_organization_feature_flag.ambr b/posthog/api/test/__snapshots__/test_organization_feature_flag.ambr index 4f5f8483b7bbb..bd705cda0b1cd 100644 --- a/posthog/api/test/__snapshots__/test_organization_feature_flag.ambr +++ b/posthog/api/test/__snapshots__/test_organization_feature_flag.ambr @@ -354,6 +354,7 @@ "posthog_hogfunction"."inputs", "posthog_hogfunction"."encrypted_inputs", "posthog_hogfunction"."filters", + "posthog_hogfunction"."mappings", "posthog_hogfunction"."masking", "posthog_hogfunction"."template_id" FROM "posthog_hogfunction" @@ -1593,6 +1594,7 @@ "posthog_hogfunction"."inputs", "posthog_hogfunction"."encrypted_inputs", "posthog_hogfunction"."filters", + "posthog_hogfunction"."mappings", "posthog_hogfunction"."masking", "posthog_hogfunction"."template_id" FROM "posthog_hogfunction" diff --git a/posthog/api/test/__snapshots__/test_survey.ambr b/posthog/api/test/__snapshots__/test_survey.ambr index 3d5e9328d3961..08a02cbc601d2 100644 --- a/posthog/api/test/__snapshots__/test_survey.ambr +++ b/posthog/api/test/__snapshots__/test_survey.ambr @@ -397,6 +397,7 @@ "posthog_hogfunction"."inputs", "posthog_hogfunction"."encrypted_inputs", "posthog_hogfunction"."filters", + "posthog_hogfunction"."mappings", "posthog_hogfunction"."masking", "posthog_hogfunction"."template_id" FROM "posthog_hogfunction" @@ -740,6 +741,7 @@ "posthog_hogfunction"."inputs", "posthog_hogfunction"."encrypted_inputs", "posthog_hogfunction"."filters", + "posthog_hogfunction"."mappings", "posthog_hogfunction"."masking", "posthog_hogfunction"."template_id" FROM "posthog_hogfunction" @@ -1141,6 +1143,7 @@ "posthog_hogfunction"."inputs", "posthog_hogfunction"."encrypted_inputs", "posthog_hogfunction"."filters", + "posthog_hogfunction"."mappings", "posthog_hogfunction"."masking", "posthog_hogfunction"."template_id" FROM "posthog_hogfunction" @@ -1470,6 +1473,7 @@ "posthog_hogfunction"."inputs", "posthog_hogfunction"."encrypted_inputs", "posthog_hogfunction"."filters", + "posthog_hogfunction"."mappings", "posthog_hogfunction"."masking", "posthog_hogfunction"."template_id" FROM "posthog_hogfunction" @@ -1828,6 +1832,7 @@ "posthog_hogfunction"."inputs", "posthog_hogfunction"."encrypted_inputs", "posthog_hogfunction"."filters", + "posthog_hogfunction"."mappings", "posthog_hogfunction"."masking", "posthog_hogfunction"."template_id" FROM "posthog_hogfunction" diff --git a/posthog/api/test/test_hog_function.py b/posthog/api/test/test_hog_function.py index 55788bb058191..c1c00fbf0cb3d 100644 --- a/posthog/api/test/test_hog_function.py +++ b/posthog/api/test/test_hog_function.py @@ -218,6 +218,7 @@ def test_create_hog_function(self, *args): "icon_url": None, "template": None, "masking": None, + "mappings": None, "status": {"rating": 0, "state": 0, "tokens": 0}, } @@ -271,6 +272,7 @@ def test_creates_with_template_id(self, *args): "hog": template_webhook.hog, "filters": None, "masking": None, + "mappings": None, "sub_templates": response.json()["template"]["sub_templates"], } diff --git a/posthog/api/test/test_hog_function_templates.py b/posthog/api/test/test_hog_function_templates.py index 956be4de638a9..5df8ed8339db5 100644 --- a/posthog/api/test/test_hog_function_templates.py +++ b/posthog/api/test/test_hog_function_templates.py @@ -17,6 +17,7 @@ "category": template.category, "filters": template.filters, "masking": template.masking, + "mappings": template.mappings, "icon_url": template.icon_url, } diff --git a/posthog/cdp/filters.py b/posthog/cdp/filters.py index 3d34a4c4c29fc..b3ea85d49f45b 100644 --- a/posthog/cdp/filters.py +++ b/posthog/cdp/filters.py @@ -59,10 +59,9 @@ def hog_function_filters_to_expr(filters: dict, team: Team, actions: dict[int, A all_filters_exprs.append(ast.And(exprs=exprs)) if all_filters_exprs: - final_expr = ast.Or(exprs=all_filters_exprs) - return final_expr - else: - return ast.Constant(value=True) + return ast.Or(exprs=all_filters_exprs) + + return ast.Constant(value=True) def filter_action_ids(filters: Optional[dict]) -> list[int]: diff --git a/posthog/cdp/site_functions.py b/posthog/cdp/site_functions.py index 3896f4f73515e..e3c75ef325273 100644 --- a/posthog/cdp/site_functions.py +++ b/posthog/cdp/site_functions.py @@ -8,8 +8,7 @@ def get_transpiled_function(hog_function: HogFunction) -> str: - # Wrap in IIFE = Immediately Invoked Function Expression = to avoid polluting global scope - response = "(function() {\n" + response = "" # Build the inputs in three parts: # 1) a simple object with constants/scalars @@ -32,12 +31,6 @@ def get_transpiled_function(hog_function: HogFunction) -> str: else: inputs_object.append(f"{key_string}: {json.dumps(value)}") - # Convert the filters to code - filters_expr = hog_function_filters_to_expr(hog_function.filters or {}, hog_function.team, {}) - filters_code = compiler.visit(filters_expr) - # Start with the STL functions - response += compiler.get_stl_code() + "\n" - # A function to calculate the inputs from globals. If "initial" is true, no errors are logged. response += "function buildInputs(globals, initial) {\n" @@ -59,6 +52,36 @@ def get_transpiled_function(hog_function: HogFunction) -> str: response += f"const source = {transpile(hog_function.hog, 'site')}();" + # Convert the global filters to code + filters_expr = hog_function_filters_to_expr(hog_function.filters or {}, hog_function.team, {}) + filters_code = compiler.visit(filters_expr) + + # Convert the mappings to code + mapping_code = "" + for mapping in hog_function.mappings or []: + mapping_inputs = mapping.get("inputs", {}) + mapping_inputs_schema = mapping.get("inputs_schema", []) + mapping_filters_expr = hog_function_filters_to_expr(mapping.get("filters", {}) or {}, hog_function.team, {}) + mapping_filters_code = compiler.visit(mapping_filters_expr) + mapping_code += f"if ({mapping_filters_code}) {{ const newInputs = structuredClone(inputs); \n" + + for schema in mapping_inputs_schema: + if "key" in schema and schema["key"] not in mapping_inputs: + mapping_inputs[schema["key"]] = {"value": schema.get("default", None)} + + for key, input in mapping_inputs.items(): + value = input.get("value") if input is not None else schema.get("default", None) + key_string = json.dumps(str(key) or "") + if (isinstance(value, str) and "{" in value) or isinstance(value, dict) or isinstance(value, list): + base_code = transpile_template_code(value, compiler) + mapping_code += ( + f"try {{ newInputs[{json.dumps(key)}] = {base_code}; }} catch (e) {{ console.error(e) }}\n" + ) + else: + mapping_code += f"newInputs[{json.dumps(key)}] = {json.dumps(value)};\n" + mapping_code += "source.onEvent({ inputs: newInputs, posthog });" + mapping_code += "}\n" + # We are exposing an init function which is what the client will use to actually run this setup code. # The return includes any extra methods that the client might need to use - so far just processEvent response += ( @@ -73,7 +96,10 @@ def get_transpiled_function(hog_function: HogFunction) -> str: const filterMatches = """ + filters_code + """; - if (filterMatches) { source.onEvent({ ...globals, inputs, posthog }); } + if (!filterMatches) { return; } + """ + + (mapping_code or ";") + + """ } } @@ -81,7 +107,12 @@ def get_transpiled_function(hog_function: HogFunction) -> str: const posthog = config.posthog; const callback = config.callback; if ('onLoad' in source) { - const r = source.onLoad({ inputs: buildInputs({}, true), posthog: posthog }); + const globals = { + person: { + properties: posthog.get_property('$stored_person_properties'), + } + } + const r = source.onLoad({ inputs: buildInputs(globals, true), posthog: posthog }); if (r && typeof r.then === 'function' && typeof r.finally === 'function') { r.catch(() => callback(false)).then(() => callback(true)) } else { callback(true) } } else { callback(true); @@ -95,6 +126,8 @@ def get_transpiled_function(hog_function: HogFunction) -> str: return { init: init };""" ) - response += "\n})" + # Wrap in IIFE = Immediately Invoked (invokable) Function Expression = to avoid polluting global scope + # Add collected STL functions above the generated code + response = "(function() {\n" + compiler.get_stl_code() + "\n" + response + "\n})" return response diff --git a/posthog/cdp/templates/_internal/template_blank.py b/posthog/cdp/templates/_internal/template_blank.py index 4f141ad9fc3e8..9e19b75a6434e 100644 --- a/posthog/cdp/templates/_internal/template_blank.py +++ b/posthog/cdp/templates/_internal/template_blank.py @@ -1,11 +1,11 @@ -from posthog.cdp.templates.hog_function_template import HogFunctionTemplate +from posthog.cdp.templates.hog_function_template import HogFunctionMapping, HogFunctionTemplate blank_site_destination: HogFunctionTemplate = HogFunctionTemplate( status="client-side", type="site_destination", id="template-blank-site-destination", name="New client-side destination", - description="Run code on your website when an event is sent to PostHog. Works only with posthog-js when opt_in_site_apps is set to true.", + description="New destination with complex event mapping. Works only with posthog-js when opt_in_site_apps is set to true.", icon_url="/static/hedgehog/builder-hog-01.png", category=["Custom", "Analytics"], hog=""" @@ -15,9 +15,9 @@ await new Promise((resolve) => window.setTimeout(resolve, 1000)) console.log("🦔 Script loaded") } -export function onEvent({ posthog, ...globals }) { - const { event, person } = globals - console.log(`🦔 Sending event: ${event.event}`, globals) +export function onEvent({ inputs, posthog }) { + console.log(`🦔 Sending event of type ${inputs.eventType}`, inputs.payload) + // fetch('url', { method: 'POST', body: JSON.stringify(inputs.payload) }) } """.strip(), inputs_schema=[ @@ -50,6 +50,83 @@ "required": True, }, ], + mappings=[ + HogFunctionMapping( + filters={"events": [{"id": "$pageview", "type": "events"}]}, + inputs_schema=[ + { + "key": "eventType", + "type": "string", + "label": "Event Type", + "description": "The destination's event type", + "default": "acquisition", + "required": True, + }, + { + "key": "payload", + "type": "json", + "label": "Payload", + "description": "Payload sent to the destination.", + "default": { + "event": "{event}", + "person": "{person}", + }, + "secret": False, + "required": True, + }, + ], + ), + HogFunctionMapping( + filters={"events": [{"id": "$autocapture", "type": "events"}]}, + inputs_schema=[ + { + "key": "eventType", + "type": "string", + "label": "Event Type", + "description": "The destination's event type", + "default": "conversion", + "required": True, + }, + { + "key": "payload", + "type": "json", + "label": "Payload", + "description": "Payload sent to the destination.", + "default": { + "event": "{event}", + "person": "{person}", + }, + "secret": False, + "required": True, + }, + ], + ), + HogFunctionMapping( + filters={"events": [{"id": "$pageleave", "type": "events"}]}, + inputs_schema=[ + { + "key": "eventType", + "type": "string", + "label": "Event Type", + "description": "The destination's event type", + "default": "retention", + "required": True, + }, + { + "key": "payload", + "type": "json", + "label": "Payload", + "description": "Payload sent to the destination.", + "default": { + "event": "{event}", + "person": "{person}", + }, + "secret": False, + "required": True, + }, + ], + ), + ], ) blank_site_app: HogFunctionTemplate = HogFunctionTemplate( diff --git a/posthog/cdp/templates/hog_function_template.py b/posthog/cdp/templates/hog_function_template.py index c3227f9b8eb73..004d3ce266b0f 100644 --- a/posthog/cdp/templates/hog_function_template.py +++ b/posthog/cdp/templates/hog_function_template.py @@ -23,6 +23,13 @@ class HogFunctionSubTemplate: inputs: Optional[dict] = None +@dataclasses.dataclass(frozen=True) +class HogFunctionMapping: + filters: Optional[dict] = None + inputs: Optional[dict] = None + inputs_schema: Optional[list[dict]] = None + + @dataclasses.dataclass(frozen=True) class HogFunctionTemplate: status: Literal["alpha", "beta", "stable", "free", "client-side"] @@ -46,6 +53,7 @@ class HogFunctionTemplate: category: list[str] sub_templates: Optional[list[HogFunctionSubTemplate]] = None filters: Optional[dict] = None + mappings: Optional[list[HogFunctionMapping]] = None masking: Optional[dict] = None icon_url: Optional[str] = None diff --git a/posthog/cdp/test/test_site_functions.py b/posthog/cdp/test/test_site_functions.py index 44f0e11f373ae..9370cb7266740 100644 --- a/posthog/cdp/test/test_site_functions.py +++ b/posthog/cdp/test/test_site_functions.py @@ -77,7 +77,8 @@ def test_get_transpiled_function_basic(self): const filterGlobals = { ...globals.groups, ...globals.event, person: globals.person, inputs, pdi: { distinct_id: globals.event.distinct_id, person: globals.person } }; let __getGlobal = (key) => filterGlobals[key]; const filterMatches = true; - if (filterMatches) { source.onEvent({ ...globals, inputs, posthog }); } + if (!filterMatches) { return; } + ; } } @@ -85,7 +86,12 @@ def test_get_transpiled_function_basic(self): const posthog = config.posthog; const callback = config.callback; if ('onLoad' in source) { - const r = source.onLoad({ inputs: buildInputs({}, true), posthog: posthog }); + const globals = { + person: { + properties: posthog.get_property('$stored_person_properties'), + } + } + const r = source.onLoad({ inputs: buildInputs(globals, true), posthog: posthog }); if (r && typeof r.then === 'function' && typeof r.finally === 'function') { r.catch(() => callback(false)).then(() => callback(true)) } else { callback(true) } } else { callback(true); @@ -131,7 +137,8 @@ def test_get_transpiled_function_with_filters(self): assert "console.log(event.event);" in result assert "const filterMatches = " in result assert '__getGlobal("event") == "$pageview"' in result - assert "if (filterMatches) { source.onEvent({" in result + assert "const filterMatches = !!(!!((__getGlobal" in result + assert "if (!filterMatches) { return; }" in result def test_get_transpiled_function_with_invalid_template_input(self): self.hog_function.hog = "export function onLoad() { console.log(inputs.greeting); }" @@ -255,3 +262,24 @@ def test_get_transpiled_function_with_complex_filters(self): assert "const filterMatches = " in result assert '__getGlobal("event") == "$pageview"' in result assert "https://example.com" in result + + def test_get_transpiled_function_with_mappings(self): + self.hog_function.hog = "export function onLoad({ inputs, posthog }) { console.log(inputs); }" + self.hog_function.inputs = {"greeting": {"value": "Hello, {person.properties.nonexistent_property}!"}} + self.hog_function.filters = { + "events": [{"id": "$pageview", "name": "$pageview", "type": "events"}], + } + self.hog_function.mappings = [ + { + "inputs": {"greeting": {"value": "Hallo, {person.properties.nonexistent_property}!"}}, + "filters": {"events": [{"id": "$autocapture", "name": "$autocapture", "type": "events"}]}, + } + ] + + result = self.compile_and_run() + + assert "console.log(inputs);" in result + assert 'const filterMatches = !!(!!((__getGlobal("event") == "$pageview")));' in result + assert 'if (!!(!!((__getGlobal("event") == "$autocapture")))) {' in result + assert "const newInputs = structuredClone(inputs);" in result + assert 'newInputs["greeting"] = concat("Hallo, ", __getProperty' in result diff --git a/posthog/migrations/0529_hog_function_mappings.py b/posthog/migrations/0529_hog_function_mappings.py new file mode 100644 index 0000000000000..a15735cc89290 --- /dev/null +++ b/posthog/migrations/0529_hog_function_mappings.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.15 on 2024-12-10 11:36 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("posthog", "0528_project_field_in_taxonomy"), + ] + + operations = [ + migrations.AddField( + model_name="hogfunction", + name="mappings", + field=models.JSONField(blank=True, null=True), + ), + ] diff --git a/posthog/migrations/max_migration.txt b/posthog/migrations/max_migration.txt index 647b659f0832e..e0d6699d21ad6 100644 --- a/posthog/migrations/max_migration.txt +++ b/posthog/migrations/max_migration.txt @@ -1 +1 @@ -0528_project_field_in_taxonomy +0529_hog_function_mappings diff --git a/posthog/models/hog_functions/hog_function.py b/posthog/models/hog_functions/hog_function.py index 48e3db90a9dcd..2f7a33d3ec56e 100644 --- a/posthog/models/hog_functions/hog_function.py +++ b/posthog/models/hog_functions/hog_function.py @@ -81,6 +81,7 @@ class Meta: encrypted_inputs: EncryptedJSONStringField = EncryptedJSONStringField(null=True, blank=True) filters = models.JSONField(null=True, blank=True) + mappings = models.JSONField(null=True, blank=True) masking = models.JSONField(null=True, blank=True) template_id = models.CharField(max_length=400, null=True, blank=True) diff --git a/posthog/models/test/test_remote_config.py b/posthog/models/test/test_remote_config.py index ceef7a8b85d74..406575184d2e5 100644 --- a/posthog/models/test/test_remote_config.py +++ b/posthog/models/test/test_remote_config.py @@ -552,7 +552,8 @@ def test_renders_js_including_site_functions(self): const filterGlobals = { ...globals.groups, ...globals.event, person: globals.person, inputs, pdi: { distinct_id: globals.event.distinct_id, person: globals.person } }; let __getGlobal = (key) => filterGlobals[key]; const filterMatches = !!(!!(!ilike(__getProperty(__getProperty(__getGlobal("person"), "properties", true), "email", true), "%@posthog.com%") && ((!match(toString(__getProperty(__getGlobal("properties"), "$host", true)), "^(localhost|127\\\\.0\\\\.0\\\\.1)($|:)")) ?? 1) && (__getGlobal("event") == "$pageview"))); - if (filterMatches) { source.onEvent({ ...globals, inputs, posthog }); } + if (!filterMatches) { return; } + ; } } @@ -560,7 +561,12 @@ def test_renders_js_including_site_functions(self): const posthog = config.posthog; const callback = config.callback; if ('onLoad' in source) { - const r = source.onLoad({ inputs: buildInputs({}, true), posthog: posthog }); + const globals = { + person: { + properties: posthog.get_property('$stored_person_properties'), + } + } + const r = source.onLoad({ inputs: buildInputs(globals, true), posthog: posthog }); if (r && typeof r.then === 'function' && typeof r.finally === 'function') { r.catch(() => callback(false)).then(() => callback(true)) } else { callback(true) } } else { callback(true); @@ -592,7 +598,8 @@ def test_renders_js_including_site_functions(self): const filterGlobals = { ...globals.groups, ...globals.event, person: globals.person, inputs, pdi: { distinct_id: globals.event.distinct_id, person: globals.person } }; let __getGlobal = (key) => filterGlobals[key]; const filterMatches = true; - if (filterMatches) { source.onEvent({ ...globals, inputs, posthog }); } + if (!filterMatches) { return; } + ; } } @@ -600,7 +607,12 @@ def test_renders_js_including_site_functions(self): const posthog = config.posthog; const callback = config.callback; if ('onLoad' in source) { - const r = source.onLoad({ inputs: buildInputs({}, true), posthog: posthog }); + const globals = { + person: { + properties: posthog.get_property('$stored_person_properties'), + } + } + const r = source.onLoad({ inputs: buildInputs(globals, true), posthog: posthog }); if (r && typeof r.then === 'function' && typeof r.finally === 'function') { r.catch(() => callback(false)).then(() => callback(true)) } else { callback(true) } } else { callback(true); diff --git a/posthog/tasks/test/__snapshots__/test_process_scheduled_changes.ambr b/posthog/tasks/test/__snapshots__/test_process_scheduled_changes.ambr index 03fbd54487917..3fc2e6d23976d 100644 --- a/posthog/tasks/test/__snapshots__/test_process_scheduled_changes.ambr +++ b/posthog/tasks/test/__snapshots__/test_process_scheduled_changes.ambr @@ -415,6 +415,7 @@ "posthog_hogfunction"."inputs", "posthog_hogfunction"."encrypted_inputs", "posthog_hogfunction"."filters", + "posthog_hogfunction"."mappings", "posthog_hogfunction"."masking", "posthog_hogfunction"."template_id" FROM "posthog_hogfunction" @@ -980,6 +981,7 @@ "posthog_hogfunction"."inputs", "posthog_hogfunction"."encrypted_inputs", "posthog_hogfunction"."filters", + "posthog_hogfunction"."mappings", "posthog_hogfunction"."masking", "posthog_hogfunction"."template_id" FROM "posthog_hogfunction" @@ -1334,6 +1336,7 @@ "posthog_hogfunction"."inputs", "posthog_hogfunction"."encrypted_inputs", "posthog_hogfunction"."filters", + "posthog_hogfunction"."mappings", "posthog_hogfunction"."masking", "posthog_hogfunction"."template_id" FROM "posthog_hogfunction" diff --git a/posthog/test/__snapshots__/test_feature_flag.ambr b/posthog/test/__snapshots__/test_feature_flag.ambr index 2a478be1ac0dd..3a833b55c0c8a 100644 --- a/posthog/test/__snapshots__/test_feature_flag.ambr +++ b/posthog/test/__snapshots__/test_feature_flag.ambr @@ -490,6 +490,7 @@ "posthog_hogfunction"."inputs", "posthog_hogfunction"."encrypted_inputs", "posthog_hogfunction"."filters", + "posthog_hogfunction"."mappings", "posthog_hogfunction"."masking", "posthog_hogfunction"."template_id" FROM "posthog_hogfunction" @@ -859,6 +860,7 @@ "posthog_hogfunction"."inputs", "posthog_hogfunction"."encrypted_inputs", "posthog_hogfunction"."filters", + "posthog_hogfunction"."mappings", "posthog_hogfunction"."masking", "posthog_hogfunction"."template_id" FROM "posthog_hogfunction" @@ -1116,6 +1118,7 @@ "posthog_hogfunction"."inputs", "posthog_hogfunction"."encrypted_inputs", "posthog_hogfunction"."filters", + "posthog_hogfunction"."mappings", "posthog_hogfunction"."masking", "posthog_hogfunction"."template_id" FROM "posthog_hogfunction" @@ -1527,6 +1530,7 @@ "posthog_hogfunction"."inputs", "posthog_hogfunction"."encrypted_inputs", "posthog_hogfunction"."filters", + "posthog_hogfunction"."mappings", "posthog_hogfunction"."masking", "posthog_hogfunction"."template_id" FROM "posthog_hogfunction" @@ -2179,6 +2183,7 @@ "posthog_hogfunction"."inputs", "posthog_hogfunction"."encrypted_inputs", "posthog_hogfunction"."filters", + "posthog_hogfunction"."mappings", "posthog_hogfunction"."masking", "posthog_hogfunction"."template_id" FROM "posthog_hogfunction"