Skip to content

Commit

Permalink
feat(cdp): site destination mapping templates (#26866)
Browse files Browse the repository at this point in the history
Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
  • Loading branch information
mariusandra and github-actions[bot] authored Dec 12, 2024
1 parent 0d72070 commit aad8698
Show file tree
Hide file tree
Showing 9 changed files with 165 additions and 57 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,10 @@ export const hogFunctionConfigurationLogic = kea<hogFunctionConfigurationLogicTy
errors: (data) => {
return {
name: !data.name ? 'Name is required' : undefined,
mappings:
data.type === 'site_destination' && (!data.mappings || data.mappings.length === 0)
? 'You must add at least one mapping'
: undefined,
...(values.inputFormErrors as any),
}
},
Expand Down Expand Up @@ -639,8 +643,26 @@ export const hogFunctionConfigurationLogic = kea<hogFunctionConfigurationLogicTy
},
],
matchingFilters: [
(s) => [s.configuration],
(configuration): PropertyGroupFilter => {
(s) => [s.configuration, s.useMapping],
(configuration, useMapping): PropertyGroupFilter => {
// We're using mappings, but none are provided, so match zero events.
if (useMapping && !configuration.mappings?.length) {
return {
type: FilterLogicalOperator.And,
values: [
{
type: FilterLogicalOperator.And,
values: [
{
type: PropertyFilterType.HogQL,
key: 'false',
},
],
},
],
}
}

const seriesProperties: PropertyGroupFilterValue = {
type: FilterLogicalOperator.Or,
values: [],
Expand Down Expand Up @@ -809,7 +831,6 @@ export const hogFunctionConfigurationLogic = kea<hogFunctionConfigurationLogicTy
return hogFunction?.template?.hog && hogFunction.template.hog !== configuration.hog
},
],

subTemplate: [
(s) => [s.template, s.subTemplateId],
(template, subTemplateId) => {
Expand All @@ -821,8 +842,11 @@ export const hogFunctionConfigurationLogic = kea<hogFunctionConfigurationLogicTy
return subTemplate
},
],

forcedSubTemplateId: [() => [router.selectors.searchParams], ({ sub_template }) => !!sub_template],
mappingTemplates: [
(s) => [s.hogFunction, s.template],
(hogFunction, template) => template?.mapping_templates ?? hogFunction?.template?.mapping_templates ?? [],
],
})),

listeners(({ actions, values, cache }) => ({
Expand Down Expand Up @@ -865,6 +889,20 @@ export const hogFunctionConfigurationLogic = kea<hogFunctionConfigurationLogicTy
...(cache.configFromUrl ?? {}),
}

if (values.template?.mapping_templates) {
config.mappings = [
...(config.mappings ?? []),
...values.template.mapping_templates
.filter((t) => t.include_by_default)
.map((template) => ({
...template,
inputs: template.inputs_schema?.reduce((acc, input) => {
acc[input.key] = { value: input.default }
return acc
}, {} as Record<string, HogFunctionInputType>),
})),
]
}
const paramsFromUrl = cache.paramsFromUrl ?? {}
const unsavedConfigurationToApply =
(values.unsavedConfiguration?.timestamp ?? 0) > Date.now() - UNSAVED_CONFIGURATION_TTL
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { IconPlus, IconPlusSmall, IconTrash } from '@posthog/icons'
import { LemonButton, LemonLabel } from '@posthog/lemon-ui'
import { LemonButton, LemonLabel, LemonSelect } 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 { useState } from 'react'
import { ActionFilter } from 'scenes/insights/filters/ActionFilter/ActionFilter'
import { MathAvailability } from 'scenes/insights/filters/ActionFilter/ActionFilterRow/ActionFilterRow'

Expand All @@ -16,7 +17,8 @@ import { HogFunctionInputs } from '../HogFunctionInputs'

export function HogFunctionMapping(): JSX.Element | null {
const { groupsTaxonomicTypes } = useValues(groupsModel)
const { useMapping, showSource } = useValues(hogFunctionConfigurationLogic)
const { useMapping, showSource, mappingTemplates } = useValues(hogFunctionConfigurationLogic)
const [selectedMappingTemplate, setSelectedMappingTemplate] = useState<string | null>(null)

if (!useMapping) {
return null
Expand All @@ -34,16 +36,14 @@ export function HogFunctionMapping(): JSX.Element | null {
<LemonLabel info="When an event matches these filters, the function is run with the appended inputs.">
Mapping #{index + 1}
</LemonLabel>
{mappings.length > 1 ? (
<LemonButton
key="delete"
icon={<IconTrash />}
title="Delete graph series"
data-attr={`delete-prop-filter-${index}`}
noPadding
onClick={() => onChange(mappings.filter((_, i) => i !== index))}
/>
) : null}
<LemonButton
key="delete"
icon={<IconTrash />}
title="Delete graph series"
data-attr={`delete-prop-filter-${index}`}
noPadding
onClick={() => onChange(mappings.filter((_, i) => i !== index))}
/>
</div>
<ActionFilter
bordered
Expand Down Expand Up @@ -119,40 +119,73 @@ export function HogFunctionMapping(): JSX.Element | null {
</div>
))}
<div className="border bg-bg-light rounded p-3 space-y-2">
<LemonButton
type="tertiary"
data-attr="add-action-event-button"
icon={<IconPlusSmall />}
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: [],
},
<LemonLabel>New Mapping</LemonLabel>
<div className="flex gap-2">
{mappingTemplates.length ? (
<LemonSelect
placeholder="Select a template"
value={selectedMappingTemplate}
onChange={setSelectedMappingTemplate}
options={mappingTemplates.map((t) => ({
label: t.name,
value: t.name,
}))}
/>
) : null}
<LemonButton
type="secondary"
data-attr="add-action-event-button"
icon={<IconPlusSmall />}
disabledReason={
mappingTemplates.length && !selectedMappingTemplate
? 'Select a mapping template'
: undefined
}
onChange([...mappings, newMapping])
}}
>
Add mapping
</LemonButton>
onClick={() => {
if (selectedMappingTemplate) {
const mappingTemplate = mappingTemplates.find(
(t) => t.name === selectedMappingTemplate
)
if (mappingTemplate) {
const { name, ...mapping } = mappingTemplate
const inputs = mapping.inputs_schema
? Object.fromEntries(
mapping.inputs_schema
.filter((m) => m.default !== undefined)
.map((m) => [
m.key,
{ value: structuredClone(m.default) },
])
)
: {}
onChange([...mappings, { ...mapping, inputs }])
}
setSelectedMappingTemplate(null)
return
}

const newMapping = {
inputs_schema: [],
inputs: {},
filters: {
events: [
{
id: getDefaultEventName(),
name: getDefaultEventName(),
type: EntityTypes.EVENTS,
order: 0,
properties: [],
},
],
actions: [],
},
}
onChange([...mappings, newMapping])
}}
>
Add mapping
</LemonButton>
</div>
</div>
</>
)
Expand Down
5 changes: 5 additions & 0 deletions frontend/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4659,6 +4659,10 @@ export interface HogFunctionMappingType {
inputs?: Record<string, HogFunctionInputType> | null
filters?: HogFunctionFiltersType | null
}
export interface HogFunctionMappingTemplateType extends HogFunctionMappingType {
name: string
include_by_default?: boolean
}

export type HogFunctionTypeType =
| 'destination'
Expand Down Expand Up @@ -4715,6 +4719,7 @@ export type HogFunctionTemplateType = Pick<
> & {
status: HogFunctionTemplateStatus
sub_templates?: HogFunctionSubTemplateType[]
mapping_templates?: HogFunctionMappingTemplateType[]
}

export type HogFunctionIconResponse = {
Expand Down
15 changes: 13 additions & 2 deletions posthog/api/hog_function_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@
from rest_framework.exceptions import NotFound

from posthog.cdp.templates import HOG_FUNCTION_TEMPLATES
from posthog.cdp.templates.hog_function_template import HogFunctionMapping, HogFunctionTemplate, HogFunctionSubTemplate
from posthog.cdp.templates.hog_function_template import (
HogFunctionMapping,
HogFunctionMappingTemplate,
HogFunctionTemplate,
HogFunctionSubTemplate,
)
from rest_framework_dataclasses.serializers import DataclassSerializer


Expand All @@ -18,14 +23,20 @@ class Meta:
dataclass = HogFunctionMapping


class HogFunctionMappingTemplateSerializer(DataclassSerializer):
class Meta:
dataclass = HogFunctionMappingTemplate


class HogFunctionSubTemplateSerializer(DataclassSerializer):
class Meta:
dataclass = HogFunctionSubTemplate


class HogFunctionTemplateSerializer(DataclassSerializer):
sub_templates = HogFunctionSubTemplateSerializer(many=True, required=False)
mapping_templates = HogFunctionMappingTemplateSerializer(many=True, required=False)
mappings = HogFunctionMappingSerializer(many=True, required=False)
sub_templates = HogFunctionSubTemplateSerializer(many=True, required=False)

class Meta:
dataclass = HogFunctionTemplate
Expand Down
1 change: 1 addition & 0 deletions posthog/api/test/test_hog_function.py
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,7 @@ def test_creates_with_template_id(self, *args):
"filters": None,
"masking": None,
"mappings": None,
"mapping_templates": None,
"sub_templates": response.json()["template"]["sub_templates"],
}

Expand Down
1 change: 1 addition & 0 deletions posthog/api/test/test_hog_function_templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"filters": template.filters,
"masking": template.masking,
"mappings": template.mappings,
"mapping_templates": template.mapping_templates,
"icon_url": template.icon_url,
}

Expand Down
6 changes: 5 additions & 1 deletion posthog/cdp/site_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,10 @@ def get_transpiled_function(hog_function: HogFunction) -> str:
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"

mapping_code += f"if ({mapping_filters_code}) {{"
mapping_code += "(function (){" # IIFE so that the code below has different globals than the filters above
mapping_code += "const newInputs = structuredClone(inputs); const __getGlobal = (key) => key === 'inputs' ? newInputs : globals[key];\n"

for schema in mapping_inputs_schema:
if "key" in schema and schema["key"] not in mapping_inputs:
Expand All @@ -80,6 +83,7 @@ def get_transpiled_function(hog_function: HogFunction) -> str:
else:
mapping_code += f"newInputs[{json.dumps(key)}] = {json.dumps(value)};\n"
mapping_code += "source.onEvent({ inputs: newInputs, posthog });"
mapping_code += "})();"
mapping_code += "}\n"

# We are exposing an init function which is what the client will use to actually run this setup code.
Expand Down
15 changes: 10 additions & 5 deletions posthog/cdp/templates/_internal/template_blank.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from posthog.cdp.templates.hog_function_template import HogFunctionMapping, HogFunctionTemplate
from posthog.cdp.templates.hog_function_template import HogFunctionMappingTemplate, HogFunctionTemplate

blank_site_destination: HogFunctionTemplate = HogFunctionTemplate(
status="client-side",
Expand Down Expand Up @@ -50,8 +50,11 @@
"required": True,
},
],
mappings=[
HogFunctionMapping(
mappings=[],
mapping_templates=[
HogFunctionMappingTemplate(
name="Aquisition",
include_by_default=True,
filters={"events": [{"id": "$pageview", "type": "events"}]},
inputs_schema=[
{
Expand All @@ -76,7 +79,8 @@
},
],
),
HogFunctionMapping(
HogFunctionMappingTemplate(
name="Conversion",
filters={"events": [{"id": "$autocapture", "type": "events"}]},
inputs_schema=[
{
Expand All @@ -101,7 +105,8 @@
},
],
),
HogFunctionMapping(
HogFunctionMappingTemplate(
name="Retention",
filters={"events": [{"id": "$pageleave", "type": "events"}]},
inputs_schema=[
{
Expand Down
10 changes: 10 additions & 0 deletions posthog/cdp/templates/hog_function_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,15 @@ class HogFunctionMapping:
inputs_schema: Optional[list[dict]] = None


@dataclasses.dataclass(frozen=True)
class HogFunctionMappingTemplate:
name: str
include_by_default: Optional[bool] = None
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"]
Expand All @@ -54,6 +63,7 @@ class HogFunctionTemplate:
sub_templates: Optional[list[HogFunctionSubTemplate]] = None
filters: Optional[dict] = None
mappings: Optional[list[HogFunctionMapping]] = None
mapping_templates: Optional[list[HogFunctionMappingTemplate]] = None
masking: Optional[dict] = None
icon_url: Optional[str] = None

Expand Down

0 comments on commit aad8698

Please sign in to comment.