diff --git a/frontend/public/services/june.png b/frontend/public/services/june.png new file mode 100644 index 00000000000000..020339e36b3eb1 Binary files /dev/null and b/frontend/public/services/june.png differ diff --git a/posthog/cdp/templates/__init__.py b/posthog/cdp/templates/__init__.py index 5de759fb95d166..d8c414f54dd034 100644 --- a/posthog/cdp/templates/__init__.py +++ b/posthog/cdp/templates/__init__.py @@ -6,6 +6,7 @@ from .intercom.template_intercom import template as intercom, TemplateIntercomMigrator from .sendgrid.template_sendgrid import template as sendgrid, TemplateSendGridMigrator from .clearbit.template_clearbit import template as clearbit +from .june.template_june import template as june from .posthog.template_posthog import template as posthog, TemplatePostHogMigrator from .aws_kinesis.template_aws_kinesis import template as aws_kinesis from .discord.template_discord import template as discord @@ -56,6 +57,7 @@ google_pubsub, hubspot, intercom, + june, klaviyo_event, klaviyo_user, knock, diff --git a/posthog/cdp/templates/june/template_june.py b/posthog/cdp/templates/june/template_june.py new file mode 100644 index 00000000000000..650e1add36f1d9 --- /dev/null +++ b/posthog/cdp/templates/june/template_june.py @@ -0,0 +1,145 @@ +from posthog.cdp.templates.hog_function_template import HogFunctionTemplate + +template: HogFunctionTemplate = HogFunctionTemplate( + status="beta", + id="template-june", + name="June.so", + description="Send events to June.so ", + icon_url="/static/services/june.png", + category=["Analytics"], + hog=""" +let type := 'track' + +if (event.event in ('$identify', '$set')) { + type := 'identify' +} else if (event.event in ('$pageview', '$screen')) { + type := 'page' +} + +let context := { + 'app': {}, + 'campaign': {}, + 'device': {}, + 'os': {}, + 'referrer': {}, + 'screen': {} +} + +if (not empty(event.properties.$app_build)) context.app.build := event.properties.$app_build +if (not empty(event.properties.$app_version)) context.app.version := event.properties.$app_version +if (not empty(event.properties.$app_name)) context.app.name := event.properties.$app_name +if (not empty(event.properties.utm_campaign)) context.campaign.name := event.properties.utm_campaign +if (not empty(event.properties.utm_content)) context.campaign.content := event.properties.utm_content +if (not empty(event.properties.utm_medium)) context.campaign.medium := event.properties.utm_medium +if (not empty(event.properties.utm_source)) context.campaign.source := event.properties.utm_source +if (not empty(event.properties.utm_term)) context.campaign.term := event.properties.utm_term +if (not empty(event.properties.$device_id)) context.device.id := event.properties.$device_id +if (not empty(event.properties.$device_manufacturer)) context.device.manufacturer := event.properties.$device_manufacturer +if (not empty(event.properties.$device_model)) context.device.model := event.properties.$device_model +if (not empty(event.properties.$os_name)) context.device.name := event.properties.$os_name +if (not empty(event.properties.$os_version)) context.device.version := event.properties.$os_version +if (not empty(event.properties.$device_type)) context.device.type := event.properties.$device_type +if (not empty(event.properties.$ip)) context.ip := event.properties.$ip +if (not empty(event.properties.$browser_language)) context.locale := event.properties.$browser_language +if (not empty(event.properties.$os)) context.os.name := event.properties.$os +if (not empty(event.properties.$os_version)) context.os.version := event.properties.$os_version +if (not empty(event.properties.$referrer)) context.referrer.url := event.properties.$referrer +if (not empty(event.properties.$screen_height)) context.screen.height := event.properties.$screen_height +if (not empty(event.properties.$screen_width)) context.screen.width := event.properties.$screen_width +if (not empty(event.properties.$geoip_time_zone)) context.timezone := event.properties.$geoip_time_zone +if (not empty(event.properties.$raw_user_agent)) context.userAgent := event.properties.$raw_user_agent + +let properties := {} + +if (not empty(event.properties.$current_url)) properties.url := event.properties.$current_url +if (not empty(event.properties.$pathname)) properties.path := event.properties.$pathname +if (not empty(event.properties.title)) properties.title := event.properties.title +if (not empty(event.properties.$referrer)) properties.referrer := event.properties.$referrer +if (not empty(event.properties.$current_url)) { + if (not empty(splitByString('?', event.properties.$current_url)[2])) { + properties.search := f'?{splitByString('?', event.properties.$current_url)[2]}' + } +} + +let traits := {} + +for (let key, value in inputs.properties) { + if (not empty(value)) { + traits[key] := value + } +} + +if (inputs.include_all_properties) { + for (let key, value in (type == 'identify' ? person.properties : event.properties)) { + if (not empty(value) and not key like '$%') { + traits[key] := value + } + } +} + +let body := { + 'properties': properties, + 'traits': traits, + 'timestamp': event.timestamp, + 'context': context, + 'messageId': event.uuid +} + +if (type == 'track') body.event := event.event +if (event.properties.$is_identified) { + body.userId := event.distinct_id + if (not empty(event.properties.$anon_distinct_id)) body.anonymousId := event.properties.$anon_distinct_id +} else { + body.anonymousId := event.distinct_id +} + +let res := fetch(f'https://api.june.so/sdk/{type}', { + 'method': 'POST', + 'headers': { + 'Authorization': f'Basic {inputs.apiKey}', + 'Content-Type': 'application/json' + }, + 'body': body +}) + +if (res.status >= 400) { + throw Error(f'Error from api.june.so (status {res.status}): {res.body}') +} +""".strip(), + inputs_schema=[ + { + "key": "apiKey", + "type": "string", + "label": "June.so Write API key", + "secret": True, + "required": True, + }, + { + "key": "include_all_properties", + "type": "boolean", + "label": "Include all properties as attributes", + "description": "If set, all event properties will be included as traits. Individual traits can be overridden below. For identify events the Person properties will be used.", + "default": False, + "secret": False, + "required": True, + }, + { + "key": "properties", + "type": "dictionary", + "label": "Trait mapping", + "description": "Map of June.so traits and their values. You can use the filters section to filter out unwanted events.", + "default": { + "email": "{person.properties.email}", + "name": "{person.properties.name}", + "phone": "{person.properties.phone}", + }, + "secret": False, + "required": False, + }, + ], + filters={ + "events": [], + "actions": [], + "filter_test_accounts": False, + }, +) diff --git a/posthog/cdp/templates/june/test_june_template.py b/posthog/cdp/templates/june/test_june_template.py new file mode 100644 index 00000000000000..43e4d42b63ee5f --- /dev/null +++ b/posthog/cdp/templates/june/test_june_template.py @@ -0,0 +1,343 @@ +from inline_snapshot import snapshot +from posthog.cdp.templates.helpers import BaseHogFunctionTemplateTest +from posthog.cdp.templates.june.template_june import ( + template as template_june, +) + + +def create_inputs(**kwargs): + inputs = { + "apiKey": "abcdef123456", + "include_all_properties": False, + "properties": {"name": "Max AI", "email": "max@posthog.com"}, + } + inputs.update(kwargs) + + return inputs + + +class TestTemplateJune(BaseHogFunctionTemplateTest): + template = template_june + + def test_function_works(self): + self.run_function( + inputs=create_inputs(), + globals={ + "event": { + "event": "$pageview", + "uuid": "151234234", + "distinct_id": "abc123", + "timestamp": "2024-10-24T23:03:50.941Z", + "properties": { + "$is_identified": True, + "$app_build": "1.0.0", + "$app_version": "2.0", + "$app_name": "PostHog", + "utm_campaign": "test1", + "utm_content": "test2", + "utm_medium": "test3", + "utm_source": "test4", + "utm_term": "test5", + "$device_id": "test6", + "$device_manufacturer": "test7", + "$device_model": "test8", + "$os_name": "test9", + "$os_version": "test10", + "$device_type": "test11", + "$ip": "test12", + "$browser_language": "test13", + "$os": "test14", + "$referrer": "test15", + "$screen_height": "test16", + "$screen_width": "test17", + "$geoip_time_zone": "test18", + "$raw_user_agent": "test19", + "$current_url": "https://hedgebox.net/faq?billing", + "$pathname": "/faq", + "title": "Hedgebox", + }, + }, + }, + ) + + assert self.get_mock_fetch_calls()[0] == snapshot( + ( + "https://api.june.so/sdk/page", + { + "method": "POST", + "headers": { + "Authorization": "Basic abcdef123456", + "Content-Type": "application/json", + }, + "body": { + "properties": { + "url": "https://hedgebox.net/faq?billing", + "path": "/faq", + "title": "Hedgebox", + "referrer": "test15", + "search": "?billing", + }, + "traits": {"name": "Max AI", "email": "max@posthog.com"}, + "timestamp": "2024-10-24T23:03:50.941Z", + "context": { + "app": {"build": "1.0.0", "version": "2.0", "name": "PostHog"}, + "campaign": { + "name": "test1", + "content": "test2", + "medium": "test3", + "source": "test4", + "term": "test5", + }, + "device": { + "id": "test6", + "manufacturer": "test7", + "model": "test8", + "name": "test9", + "version": "test10", + "type": "test11", + }, + "os": {"name": "test14", "version": "test10"}, + "referrer": {"url": "test15"}, + "screen": { + "height": "test16", + "width": "test17", + }, + "ip": "test12", + "locale": "test13", + "timezone": "test18", + "userAgent": "test19", + }, + "messageId": "151234234", + "userId": "abc123", + }, + }, + ) + ) + + def test_body_includes_all_properties_if_set(self): + self.run_function( + inputs=create_inputs(include_all_properties=True), + globals={ + "event": { + "event": "$pageview", + "uuid": "151234234", + "distinct_id": "abc123", + "timestamp": "2024-10-24T23:03:50.941Z", + "properties": { + "$is_identified": True, + "$current_url": "https://hedgebox.net/faq?billing", + "$pathname": "/faq", + "title": "Hedgebox", + }, + }, + }, + ) + + assert self.get_mock_fetch_calls()[0] == snapshot( + ( + "https://api.june.so/sdk/page", + { + "method": "POST", + "headers": { + "Authorization": "Basic abcdef123456", + "Content-Type": "application/json", + }, + "body": { + "properties": { + "url": "https://hedgebox.net/faq?billing", + "path": "/faq", + "title": "Hedgebox", + "search": "?billing", + }, + "traits": {"name": "Max AI", "email": "max@posthog.com", "title": "Hedgebox"}, + "timestamp": "2024-10-24T23:03:50.941Z", + "context": { + "app": {}, + "campaign": {}, + "device": {}, + "os": {}, + "referrer": {}, + "screen": {}, + }, + "messageId": "151234234", + "userId": "abc123", + }, + }, + ) + ) + + self.run_function( + inputs=create_inputs(include_all_properties=False), + globals={ + "event": { + "event": "$pageview", + "uuid": "151234234", + "distinct_id": "abc123", + "timestamp": "2024-10-24T23:03:50.941Z", + "properties": { + "$is_identified": True, + "$current_url": "https://hedgebox.net/faq?billing", + "$pathname": "/faq", + "title": "Hedgebox", + }, + }, + }, + ) + + assert self.get_mock_fetch_calls()[0] == snapshot( + ( + "https://api.june.so/sdk/page", + { + "method": "POST", + "headers": { + "Authorization": "Basic abcdef123456", + "Content-Type": "application/json", + }, + "body": { + "properties": { + "url": "https://hedgebox.net/faq?billing", + "path": "/faq", + "title": "Hedgebox", + "search": "?billing", + }, + "traits": { + "name": "Max AI", + "email": "max@posthog.com", + }, + "timestamp": "2024-10-24T23:03:50.941Z", + "context": { + "app": {}, + "campaign": {}, + "device": {}, + "os": {}, + "referrer": {}, + "screen": {}, + }, + "messageId": "151234234", + "userId": "abc123", + }, + }, + ) + ) + + def test_automatic_type_mapping(self): + for event_name, expected_type in [ + ("$identify", "identify"), + ("$set", "identify"), + ("$pageview", "page"), + ("$screen", "page"), + ("$autocapture", "track"), + ("custom", "track"), + ]: + self.run_function( + inputs=create_inputs(), + globals={ + "event": {"event": event_name, "properties": {"$current_url": "https://example.com"}}, + }, + ) + + assert self.get_mock_fetch_calls()[0][0] == "https://api.june.so/sdk/" + expected_type + + def test_identified_tracking(self): + self.run_function( + inputs=create_inputs(), + globals={ + "event": { + "event": "$pageview", + "uuid": "151234234", + "distinct_id": "abc123", + "timestamp": "2024-10-24T23:03:50.941Z", + "properties": { + "$is_identified": True, + "$current_url": "https://hedgebox.net/faq?billing", + "$pathname": "/faq", + "title": "Hedgebox", + }, + }, + }, + ) + + assert self.get_mock_fetch_calls()[0] == snapshot( + ( + "https://api.june.so/sdk/page", + { + "method": "POST", + "headers": { + "Authorization": "Basic abcdef123456", + "Content-Type": "application/json", + }, + "body": { + "properties": { + "url": "https://hedgebox.net/faq?billing", + "path": "/faq", + "title": "Hedgebox", + "search": "?billing", + }, + "traits": {"name": "Max AI", "email": "max@posthog.com"}, + "timestamp": "2024-10-24T23:03:50.941Z", + "context": { + "app": {}, + "campaign": {}, + "device": {}, + "os": {}, + "referrer": {}, + "screen": {}, + }, + "messageId": "151234234", + "userId": "abc123", + }, + }, + ) + ) + + self.run_function( + inputs=create_inputs(), + globals={ + "event": { + "event": "$pageview", + "uuid": "151234234", + "distinct_id": "abc123", + "timestamp": "2024-10-24T23:03:50.941Z", + "properties": { + "$is_identified": False, + "$current_url": "https://hedgebox.net/faq?billing", + "$pathname": "/faq", + "title": "Hedgebox", + "$anon_distinct_id": "12345678abc", + }, + }, + }, + ) + + assert self.get_mock_fetch_calls()[0] == snapshot( + ( + "https://api.june.so/sdk/page", + { + "method": "POST", + "headers": { + "Authorization": "Basic abcdef123456", + "Content-Type": "application/json", + }, + "body": { + "properties": { + "url": "https://hedgebox.net/faq?billing", + "path": "/faq", + "title": "Hedgebox", + "search": "?billing", + }, + "traits": {"name": "Max AI", "email": "max@posthog.com"}, + "timestamp": "2024-10-24T23:03:50.941Z", + "context": { + "app": {}, + "campaign": {}, + "device": {}, + "os": {}, + "referrer": {}, + "screen": {}, + }, + "messageId": "151234234", + "anonymousId": "abc123", + }, + }, + ) + )