diff --git a/frontend/__snapshots__/scenes-app-data-management-actions--action--dark.png b/frontend/__snapshots__/scenes-app-data-management-actions--action--dark.png index fb7bd425b713e..fea9a4ae56bfb 100644 Binary files a/frontend/__snapshots__/scenes-app-data-management-actions--action--dark.png and b/frontend/__snapshots__/scenes-app-data-management-actions--action--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-data-management-actions--action--light.png b/frontend/__snapshots__/scenes-app-data-management-actions--action--light.png index f8bd3bfcea0de..bb3ea90d71f04 100644 Binary files a/frontend/__snapshots__/scenes-app-data-management-actions--action--light.png and b/frontend/__snapshots__/scenes-app-data-management-actions--action--light.png differ diff --git a/frontend/__snapshots__/scenes-app-pipeline--pipeline-destinations-page--dark.png b/frontend/__snapshots__/scenes-app-pipeline--pipeline-destinations-page--dark.png index 8f99baefde958..536e789b3e2bf 100644 Binary files a/frontend/__snapshots__/scenes-app-pipeline--pipeline-destinations-page--dark.png and b/frontend/__snapshots__/scenes-app-pipeline--pipeline-destinations-page--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-pipeline--pipeline-destinations-page--light.png b/frontend/__snapshots__/scenes-app-pipeline--pipeline-destinations-page--light.png index 9f4a1338692d2..14d5a816fd326 100644 Binary files a/frontend/__snapshots__/scenes-app-pipeline--pipeline-destinations-page--light.png and b/frontend/__snapshots__/scenes-app-pipeline--pipeline-destinations-page--light.png differ diff --git a/frontend/__snapshots__/scenes-app-pipeline--pipeline-destinations-page-without-pipelines--dark.png b/frontend/__snapshots__/scenes-app-pipeline--pipeline-destinations-page-without-pipelines--dark.png index 06f17e37c4f76..e7ee037d2db5a 100644 Binary files a/frontend/__snapshots__/scenes-app-pipeline--pipeline-destinations-page-without-pipelines--dark.png and b/frontend/__snapshots__/scenes-app-pipeline--pipeline-destinations-page-without-pipelines--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-pipeline--pipeline-destinations-page-without-pipelines--light.png b/frontend/__snapshots__/scenes-app-pipeline--pipeline-destinations-page-without-pipelines--light.png index fb849710587b3..a3dbad2a6d17c 100644 Binary files a/frontend/__snapshots__/scenes-app-pipeline--pipeline-destinations-page-without-pipelines--light.png and b/frontend/__snapshots__/scenes-app-pipeline--pipeline-destinations-page-without-pipelines--light.png differ diff --git a/frontend/__snapshots__/scenes-app-pipeline--pipeline-landing-page--dark.png b/frontend/__snapshots__/scenes-app-pipeline--pipeline-landing-page--dark.png index 18f14839fb087..b014d3bb100ea 100644 Binary files a/frontend/__snapshots__/scenes-app-pipeline--pipeline-landing-page--dark.png and b/frontend/__snapshots__/scenes-app-pipeline--pipeline-landing-page--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-pipeline--pipeline-landing-page--light.png b/frontend/__snapshots__/scenes-app-pipeline--pipeline-landing-page--light.png index a131b2e67755b..770e545cd3f1c 100644 Binary files a/frontend/__snapshots__/scenes-app-pipeline--pipeline-landing-page--light.png and b/frontend/__snapshots__/scenes-app-pipeline--pipeline-landing-page--light.png differ diff --git a/frontend/__snapshots__/scenes-app-pipeline--pipeline-landing-page-iff-legacy-sources--dark.png b/frontend/__snapshots__/scenes-app-pipeline--pipeline-landing-page-iff-legacy-sources--dark.png index 5544580e1e0d4..e5628dfae5411 100644 Binary files a/frontend/__snapshots__/scenes-app-pipeline--pipeline-landing-page-iff-legacy-sources--dark.png and b/frontend/__snapshots__/scenes-app-pipeline--pipeline-landing-page-iff-legacy-sources--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-pipeline--pipeline-landing-page-iff-legacy-sources--light.png b/frontend/__snapshots__/scenes-app-pipeline--pipeline-landing-page-iff-legacy-sources--light.png index 276e305964386..be52b52f8aacf 100644 Binary files a/frontend/__snapshots__/scenes-app-pipeline--pipeline-landing-page-iff-legacy-sources--light.png and b/frontend/__snapshots__/scenes-app-pipeline--pipeline-landing-page-iff-legacy-sources--light.png differ diff --git a/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-new-destination--dark.png b/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-new-destination--dark.png index 76771bf061c1e..c6b643c6cda12 100644 Binary files a/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-new-destination--dark.png and b/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-new-destination--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-new-destination--light.png b/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-new-destination--light.png index 6fca7debb5679..1572b985e5539 100644 Binary files a/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-new-destination--light.png and b/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-new-destination--light.png differ diff --git a/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-new-destination-without-data-pipelines--dark.png b/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-new-destination-without-data-pipelines--dark.png index aa70bf1c85fab..37894a2f4ab4a 100644 Binary files a/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-new-destination-without-data-pipelines--dark.png and b/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-new-destination-without-data-pipelines--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-new-destination-without-data-pipelines--light.png b/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-new-destination-without-data-pipelines--light.png index 21205a84677c2..43825f87dc339 100644 Binary files a/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-new-destination-without-data-pipelines--light.png and b/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-new-destination-without-data-pipelines--light.png differ diff --git a/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-new-hog-function--dark.png b/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-new-hog-function--dark.png new file mode 100644 index 0000000000000..8ef8f708a388a Binary files /dev/null and b/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-new-hog-function--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-new-hog-function--light.png b/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-new-hog-function--light.png new file mode 100644 index 0000000000000..c7b134087fa21 Binary files /dev/null and b/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-new-hog-function--light.png differ diff --git a/frontend/__snapshots__/scenes-app-pipeline--pipeline-overview-page--dark.png b/frontend/__snapshots__/scenes-app-pipeline--pipeline-overview-page--dark.png index 18f14839fb087..b014d3bb100ea 100644 Binary files a/frontend/__snapshots__/scenes-app-pipeline--pipeline-overview-page--dark.png and b/frontend/__snapshots__/scenes-app-pipeline--pipeline-overview-page--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-pipeline--pipeline-overview-page--light.png b/frontend/__snapshots__/scenes-app-pipeline--pipeline-overview-page--light.png index a131b2e67755b..770e545cd3f1c 100644 Binary files a/frontend/__snapshots__/scenes-app-pipeline--pipeline-overview-page--light.png and b/frontend/__snapshots__/scenes-app-pipeline--pipeline-overview-page--light.png differ diff --git a/frontend/__snapshots__/scenes-other-settings--settings-project--dark.png b/frontend/__snapshots__/scenes-other-settings--settings-project--dark.png index b23bcaf3f36ef..4291e7f0cdb42 100644 Binary files a/frontend/__snapshots__/scenes-other-settings--settings-project--dark.png and b/frontend/__snapshots__/scenes-other-settings--settings-project--dark.png differ diff --git a/frontend/__snapshots__/scenes-other-settings--settings-project--light.png b/frontend/__snapshots__/scenes-other-settings--settings-project--light.png index b49e93a747555..fd7764f2a27ad 100644 Binary files a/frontend/__snapshots__/scenes-other-settings--settings-project--light.png and b/frontend/__snapshots__/scenes-other-settings--settings-project--light.png differ diff --git a/frontend/__snapshots__/scenes-other-settings--settings-project-with-replay-features--dark.png b/frontend/__snapshots__/scenes-other-settings--settings-project-with-replay-features--dark.png index 73d771a0fb66f..0ff1f941ef600 100644 Binary files a/frontend/__snapshots__/scenes-other-settings--settings-project-with-replay-features--dark.png and b/frontend/__snapshots__/scenes-other-settings--settings-project-with-replay-features--dark.png differ diff --git a/frontend/__snapshots__/scenes-other-settings--settings-project-with-replay-features--light.png b/frontend/__snapshots__/scenes-other-settings--settings-project-with-replay-features--light.png index 70873fbadb468..25d1bbff0c62b 100644 Binary files a/frontend/__snapshots__/scenes-other-settings--settings-project-with-replay-features--light.png and b/frontend/__snapshots__/scenes-other-settings--settings-project-with-replay-features--light.png differ diff --git a/frontend/src/lib/constants.tsx b/frontend/src/lib/constants.tsx index 879c36b5acd63..d1274deee4fcc 100644 --- a/frontend/src/lib/constants.tsx +++ b/frontend/src/lib/constants.tsx @@ -192,7 +192,6 @@ export const FEATURE_FLAGS = { SETTINGS_PERSONS_JOIN_MODE: 'settings-persons-join-mode', // owner: @robbie-c SETTINGS_PERSONS_ON_EVENTS_HIDDEN: 'settings-persons-on-events-hidden', // owner: @Twixes HOG: 'hog', // owner: @mariusandra - HOG_FUNCTIONS: 'hog-functions', // owner: #team-cdp HOG_FUNCTIONS_LINKED: 'hog-functions-linked', // owner: #team-cdp PERSONLESS_EVENTS_NOT_SUPPORTED: 'personless-events-not-supported', // owner: @raquelmsmith ALERTS: 'alerts', // owner: @anirudhpillai #team-product-analytics diff --git a/frontend/src/mocks/fixtures/_hogFunctionTemplates.json b/frontend/src/mocks/fixtures/_hogFunctionTemplates.json new file mode 100644 index 0000000000000..547c01d09ddc0 --- /dev/null +++ b/frontend/src/mocks/fixtures/_hogFunctionTemplates.json @@ -0,0 +1,1520 @@ +{ + "count": 26, + "next": null, + "previous": null, + "results": [ + { + "sub_templates": [ + { + "id": "early_access_feature_enrollment", + "name": "Post to Slack on feature enrollment", + "description": "Posts a message to Slack when a user enrolls or un-enrolls in an early access feature", + "filters": { "events": [{ "id": "$feature_enrollment_update", "type": "events" }] }, + "masking": null, + "inputs": { + "text": "*{person.name}* {event.properties.$feature_enrollment ? 'enrolled in' : 'un-enrolled from'} the early access feature for '{event.properties.$feature_flag}'", + "blocks": [ + { + "text": { + "text": "*{person.name}* {event.properties.$feature_enrollment ? 'enrolled in' : 'un-enrolled from'} the early access feature for '{event.properties.$feature_flag}'", + "type": "mrkdwn" + }, + "type": "section" + }, + { + "type": "actions", + "elements": [ + { + "url": "{person.url}", + "text": { "text": "View Person in PostHog", "type": "plain_text" }, + "type": "button" + } + ] + } + ] + } + }, + { + "id": "survey_response", + "name": "Post to Slack on survey response", + "description": "Posts a message to Slack when a user responds to a survey", + "filters": { + "events": [ + { + "id": "survey sent", + "type": "events", + "properties": [ + { + "key": "$survey_response", + "type": "event", + "value": "is_set", + "operator": "is_set" + } + ] + } + ] + }, + "masking": null, + "inputs": { + "text": "*{person.name}* responded to survey *{event.properties.$survey_name}*", + "blocks": [ + { + "text": { + "text": "*{person.name}* responded to survey *{event.properties.$survey_name}*", + "type": "mrkdwn" + }, + "type": "section" + }, + { + "type": "actions", + "elements": [ + { + "url": "{project.url}/surveys/{event.properties.$survey_id}", + "text": { "text": "View Survey", "type": "plain_text" }, + "type": "button" + }, + { + "url": "{person.url}", + "text": { "text": "View Person", "type": "plain_text" }, + "type": "button" + } + ] + } + ] + } + } + ], + "status": "free", + "id": "template-slack", + "name": "Slack", + "description": "Sends a message to a slack channel", + "hog": "let res := fetch('https://slack.com/api/chat.postMessage', {\n 'body': {\n 'channel': inputs.channel,\n 'icon_emoji': inputs.icon_emoji,\n 'username': inputs.username,\n 'blocks': inputs.blocks,\n 'text': inputs.text\n },\n 'method': 'POST',\n 'headers': {\n 'Authorization': f'Bearer {inputs.slack_workspace.access_token}',\n 'Content-Type': 'application/json'\n }\n});\n\nif (res.status != 200 or not res.body.ok) {\n throw Error(f'Failed to post message to Slack: {res.status}: {res.body}');\n}", + "inputs_schema": [ + { + "key": "slack_workspace", + "type": "integration", + "integration": "slack", + "label": "Slack workspace", + "secret": false, + "required": true + }, + { + "key": "channel", + "type": "integration_field", + "integration_key": "slack_workspace", + "integration_field": "slack_channel", + "label": "Channel to post to", + "description": "Select the channel to post to (e.g. #general). The PostHog app must be installed in the workspace.", + "secret": false, + "required": true + }, + { + "key": "icon_emoji", + "type": "string", + "label": "Emoji icon", + "default": ":hedgehog:", + "required": false, + "secret": false + }, + { + "key": "username", + "type": "string", + "label": "Bot name", + "default": "PostHog", + "required": false, + "secret": false + }, + { + "key": "blocks", + "type": "json", + "label": "Blocks", + "description": "(see https://api.slack.com/block-kit/building)", + "default": [ + { + "text": { "text": "*{person.name}* triggered event: '{event.event}'", "type": "mrkdwn" }, + "type": "section" + }, + { + "type": "actions", + "elements": [ + { + "url": "{person.url}", + "text": { "text": "View Person in PostHog", "type": "plain_text" }, + "type": "button" + }, + { + "url": "{source.url}", + "text": { "text": "Message source", "type": "plain_text" }, + "type": "button" + } + ] + } + ], + "secret": false, + "required": false + }, + { + "key": "text", + "type": "string", + "label": "Plain text message", + "description": "Optional fallback message if blocks are not provided or supported", + "default": "*{person.name}* triggered event: '{event.event}'", + "secret": false, + "required": false + } + ], + "category": ["Customer Success"], + "filters": null, + "masking": null, + "icon_url": "/static/services/slack.png" + }, + { + "sub_templates": [ + { + "id": "early_access_feature_enrollment", + "name": "HTTP Webhook on feature enrollment", + "description": null, + "filters": { "events": [{ "id": "$feature_enrollment_update", "type": "events" }] }, + "masking": null, + "inputs": null + }, + { + "id": "survey_response", + "name": "HTTP Webhook on survey response", + "description": null, + "filters": { + "events": [ + { + "id": "survey sent", + "type": "events", + "properties": [ + { + "key": "$survey_response", + "type": "event", + "value": "is_set", + "operator": "is_set" + } + ] + } + ] + }, + "masking": null, + "inputs": null + } + ], + "status": "beta", + "id": "template-webhook", + "name": "HTTP Webhook", + "description": "Sends a webhook templated by the incoming event data", + "hog": "let res := fetch(inputs.url, {\n 'headers': inputs.headers,\n 'body': inputs.body,\n 'method': inputs.method\n});\n\nif (inputs.debug) {\n print('Response', res.status, res.body);\n}", + "inputs_schema": [ + { "key": "url", "type": "string", "label": "Webhook URL", "secret": false, "required": true }, + { + "key": "method", + "type": "choice", + "label": "Method", + "secret": false, + "choices": [ + { "label": "POST", "value": "POST" }, + { "label": "PUT", "value": "PUT" }, + { "label": "PATCH", "value": "PATCH" }, + { "label": "GET", "value": "GET" }, + { "label": "DELETE", "value": "DELETE" } + ], + "default": "POST", + "required": false + }, + { + "key": "body", + "type": "json", + "label": "JSON Body", + "default": { "event": "{event}", "person": "{person}" }, + "secret": false, + "required": false + }, + { "key": "headers", "type": "dictionary", "label": "Headers", "secret": false, "required": false }, + { + "key": "debug", + "type": "boolean", + "label": "Log responses", + "description": "Logs the response of http calls for debugging.", + "secret": false, + "required": false, + "default": false + } + ], + "category": ["Custom"], + "filters": null, + "masking": null, + "icon_url": "/static/posthog-icon.svg" + }, + { + "sub_templates": null, + "status": "beta", + "id": "template-activecampaign", + "name": "ActiveCampaign", + "description": "Creates a new contact in ActiveCampaign whenever an event is triggered.", + "hog": "if (empty(inputs.email)) {\n print('`email` input is empty. Not creating a contact.')\n return\n}\n\nlet contact := {\n 'email': inputs.email,\n 'fieldValues': [],\n}\n\nif (not empty(inputs.firstName)) contact.firstName := inputs.firstName\nif (not empty(inputs.lastName)) contact.lastName := inputs.lastName\nif (not empty(inputs.phone)) contact.phone := inputs.phone\n\nfor (let key, value in inputs.attributes) {\n if (not empty(value)) {\n contact.fieldValues := arrayPushBack(contact.fieldValues, {'field': key, 'value': value})\n }\n}\n\nlet res := fetch(f'https://{inputs.accountName}.api-us1.com/api/3/contact/sync', {\n 'method': 'POST',\n 'headers': {\n 'content-type': 'application/json',\n 'Api-Token': inputs.apiKey\n },\n 'body': {\n 'contact': contact\n }\n})\n\nif (res.status >= 400) {\n print(f'Error from {inputs.accountName}.api-us1.com api:', res.status, res.body)\n} else {\n print('Contact has been created or updated successfully!')\n}", + "inputs_schema": [ + { + "key": "accountName", + "type": "string", + "label": "Account name", + "description": "Usually in the form of .activehosted.com. You can use this page to figure our your account name: https://www.activecampaign.com/login/lookup.php", + "default": "", + "secret": false, + "required": true + }, + { + "key": "apiKey", + "type": "string", + "label": "Your ActiveCampaign API Key", + "description": "See the docs here: https://help.activecampaign.com/hc/en-us/articles/207317590-Getting-started-with-the-API#h_01HJ6REM2YQW19KYPB189726ST", + "default": "", + "secret": true, + "required": true + }, + { + "key": "email", + "type": "string", + "label": "Email of the user", + "description": "Where to find the email for the contact to be created. You can use the filters section to filter out unwanted emails or internal users.", + "default": "{person.properties.email}", + "secret": false, + "required": true + }, + { + "key": "firstName", + "type": "string", + "label": "First name of the user", + "description": "Where to find the first name for the contact to be created.", + "default": "{person.properties.firstName}", + "secret": false, + "required": true + }, + { + "key": "lastName", + "type": "string", + "label": "Last name of the user", + "description": "Where to find the last name for the contact to be created.", + "default": "{person.properties.lastName}", + "secret": false, + "required": true + }, + { + "key": "phone", + "type": "string", + "label": "Phone number of the user", + "description": "Where to find the phone number for the contact to be created.", + "default": "{person.properties.phone}", + "secret": false, + "required": true + }, + { + "key": "attributes", + "type": "dictionary", + "label": "Additional person fields", + "description": "Map any values to ActiveCampaign person fields. (fieldId:value)", + "default": { "1": "{person.properties.company}", "2": "{person.properties.website}" }, + "secret": false, + "required": true + } + ], + "category": ["Email Marketing"], + "filters": { + "events": [ + { "id": "$identify", "name": "$identify", "type": "events", "order": 0 }, + { "id": "$set", "name": "$set", "type": "events", "order": 1 } + ], + "actions": [], + "filter_test_accounts": true + }, + "masking": null, + "icon_url": "/static/services/activecampaign.png" + }, + { + "sub_templates": null, + "status": "beta", + "id": "template-avo", + "name": "Avo", + "description": "Send events to Avo", + "hog": "if (empty(inputs.apiKey) or empty(inputs.environment)) {\n print('API Key and environment has to be set. Skipping...')\n return\n}\n\nlet avoEvent := {\n 'apiKey': inputs.apiKey,\n 'env': inputs.environment,\n 'appName': inputs.appName,\n 'sessionId': event.properties.$session_id ?? generateUUIDv4(),\n 'createdAt': toString(toDateTime(toUnixTimestamp(now()))),\n 'avoFunction': false,\n 'eventId': null,\n 'eventHash': null,\n 'appVersion': '1.0.0',\n 'libVersion': '1.0.0',\n 'libPlatform': 'node',\n 'trackingId': '',\n 'samplingRate': 1,\n 'type': 'event',\n 'eventName': event.event,\n 'messageId': event.uuid,\n 'eventProperties': []\n}\n\nfun getPropValueType(propValue) {\n let propType := typeof(propValue)\n if (propValue == null) {\n return 'null'\n } else if (propType == 'string') {\n return 'string'\n } else if (propType == 'integer') {\n return 'int'\n } else if (propType == 'float') {\n return 'float'\n } else if (propType == 'boolean') {\n return 'boolean'\n } else if (propType == 'object') {\n return 'object'\n } else if (propType == 'array') {\n return 'list'\n } else {\n return propType\n }\n}\n\nfor (let key, value in event.properties) {\n let excludeProperties := arrayMap(x -> trim(x), splitByString(',', inputs.excludeProperties))\n let includeProperties := arrayMap(x -> trim(x), splitByString(',', inputs.includeProperties))\n let isExcluded := has(excludeProperties, key)\n let isIncluded := includeProperties[1] == '' or has(includeProperties, key)\n\n if (not (key like '$%' or isExcluded or not isIncluded)) {\n avoEvent.eventProperties := arrayPushBack(avoEvent.eventProperties, { 'propertyName': key, 'propertyType': getPropValueType(value) })\n }\n}\n\nfetch('https://api.avo.app/inspector/posthog/v1/track', {\n 'method': 'POST',\n 'headers': {\n 'env': inputs.environment,\n 'api-key': inputs.apiKey,\n 'content-type': 'application/json',\n 'accept': 'application/json',\n },\n 'body': [avoEvent]\n})", + "inputs_schema": [ + { + "key": "apiKey", + "type": "string", + "label": "Avo API Key", + "description": "Avo source API key", + "default": "", + "secret": true, + "required": true + }, + { + "key": "environment", + "type": "string", + "label": "Environment", + "description": "Environment name", + "default": "dev", + "secret": false, + "required": false + }, + { + "key": "appName", + "type": "string", + "label": "App name", + "description": "App name", + "default": "PostHog", + "secret": false, + "required": false + }, + { + "key": "excludeProperties", + "type": "string", + "label": "Properties to exclude", + "description": "Comma-separated list of event properties that will not be sent to Avo.", + "default": "", + "secret": false, + "required": false + }, + { + "key": "includeProperties", + "type": "string", + "label": "Properties to include", + "description": "Comma separated list of event properties to send to Avo (will send all if left empty).", + "default": "", + "secret": false, + "required": false + } + ], + "category": ["Analytics"], + "filters": null, + "masking": null, + "icon_url": "/static/services/avo.png" + }, + { + "sub_templates": null, + "status": "beta", + "id": "template-aws-kinesis", + "name": "AWS Kinesis", + "description": "Put data to an AWS Kinesis stream", + "hog": "fun getPayload() {\n let region := inputs.aws_region\n let service := 'kinesis'\n let amzDate := formatDateTime(now(), '%Y%m%dT%H%i%sZ')\n let date := formatDateTime(now(), '%Y%m%d')\n\n let payload := jsonStringify({\n 'StreamName': inputs.aws_kinesis_stream_arn,\n 'PartitionKey': inputs.aws_kinesis_partition_key ?? generateUUIDv4(),\n 'Data': base64Encode(jsonStringify(inputs.payload)),\n })\n\n let requestHeaders := {\n 'Content-Type': 'application/x-amz-json-1.1',\n 'X-Amz-Target': 'Kinesis_20131202.PutRecord',\n 'X-Amz-Date': amzDate,\n 'Host': f'kinesis.{region}.amazonaws.com',\n }\n\n let canonicalHeaderParts := []\n for (let key, value in requestHeaders) {\n let val := replaceAll(trim(value), '\\\\s+', ' ')\n canonicalHeaderParts := arrayPushBack(canonicalHeaderParts, f'{lower(key)}:{val}')\n }\n let canonicalHeaders := arrayStringConcat(arraySort(canonicalHeaderParts), '\\n') || '\\n'\n\n let signedHeaderParts := []\n for (let key, value in requestHeaders) {\n signedHeaderParts := arrayPushBack(signedHeaderParts, lower(key))\n }\n let signedHeaders := arrayStringConcat(arraySort(signedHeaderParts), ';')\n\n let canonicalRequest := arrayStringConcat([\n 'POST',\n '/',\n '',\n canonicalHeaders,\n signedHeaders,\n sha256Hex(payload),\n ], '\\n')\n\n let credentialScope := f'{date}/{region}/{service}/aws4_request'\n let stringToSign := arrayStringConcat([\n 'AWS4-HMAC-SHA256',\n amzDate,\n credentialScope,\n sha256Hex(canonicalRequest),\n ], '\\n')\n\n let signature := sha256HmacChainHex([\n f'AWS4{inputs.aws_secret_access_key}', date, region, service, 'aws4_request', stringToSign\n ])\n\n let authorizationHeader :=\n f'AWS4-HMAC-SHA256 Credential={inputs.aws_access_key_id}/{credentialScope}, ' ||\n f'SignedHeaders={signedHeaders}, ' ||\n f'Signature={signature}'\n\n requestHeaders['Authorization'] := authorizationHeader\n\n return {\n 'headers': requestHeaders,\n 'body': payload,\n 'method': 'POST'\n }\n}\n\nlet res := fetch(f'https://kinesis.{inputs.aws_region}.amazonaws.com', getPayload())\n\nif (res.status >= 200 and res.status < 300) {\n print('Event sent successfully!')\n} else {\n print('Error sending event:', res.status, res.body)\n}", + "inputs_schema": [ + { + "key": "aws_access_key_id", + "type": "string", + "label": "AWS Access Key ID", + "secret": true, + "required": true + }, + { + "key": "aws_secret_access_key", + "type": "string", + "label": "AWS Secret Access Key", + "secret": true, + "required": true + }, + { + "key": "aws_region", + "type": "string", + "label": "AWS Region", + "secret": false, + "required": true, + "default": "us-east-1" + }, + { + "key": "aws_kinesis_stream_arn", + "type": "string", + "label": "Kinesis Stream ARN", + "secret": false, + "required": true + }, + { + "key": "aws_kinesis_partition_key", + "type": "string", + "label": "Kinesis Partition Key", + "description": "If not provided, a random UUID will be generated.", + "default": "{event.uuid}", + "secret": false, + "required": false + }, + { + "key": "payload", + "type": "json", + "label": "Message Payload", + "default": { "event": "{event}", "person": "{person}" }, + "secret": false, + "required": false + } + ], + "category": ["Analytics"], + "filters": null, + "masking": null, + "icon_url": "/static/services/aws-kinesis.png" + }, + { + "sub_templates": null, + "status": "beta", + "id": "template-braze", + "name": "Braze", + "description": "Send events to Braze", + "hog": "let getPayload := () -> [{\n 'attributes': inputs.attributes,\n 'events': [inputs.event]\n}]\n\nlet res := fetch(f'{inputs.brazeEndpoint}/users/track', {\n 'method': 'POST',\n 'headers': {\n 'Authorization': f'Bearer {inputs.apiKey}',\n 'Content-Type': 'application/json'\n },\n 'body': getPayload()\n})\n\nif (res.status >= 200 and res.status < 300) {\n print('Event sent successfully!')\n} else {\n throw Error(f'Error sending event: {res.status} {res.body}')\n}", + "inputs_schema": [ + { + "key": "brazeEndpoint", + "type": "choice", + "label": "Braze REST Endpoint", + "description": "The endpoint identifier where your Braze instance is located, see the docs here: https://www.braze.com/docs/api/basics", + "choices": [ + { "label": "US-01", "value": "https://rest.iad-01.braze.com" }, + { "label": "US-02", "value": "https://rest.iad-02.braze.com" }, + { "label": "US-03", "value": "https://rest.iad-03.braze.com" }, + { "label": "US-04", "value": "https://rest.iad-04.braze.com" }, + { "label": "US-05", "value": "https://rest.iad-05.braze.com" }, + { "label": "US-06", "value": "https://rest.iad-06.braze.com" }, + { "label": "US-08", "value": "https://rest.iad-08.braze.com" }, + { "label": "EU-01", "value": "https://rest.fra-01.braze.eu" }, + { "label": "EU-02", "value": "https://rest.fra-02.braze.eu" } + ], + "default": "", + "secret": false, + "required": true + }, + { + "key": "apiKey", + "type": "string", + "label": "Your Braze API Key", + "description": "See the docs here: https://www.braze.com/docs/api/api_key/", + "default": "", + "secret": true, + "required": true + }, + { + "key": "attributes", + "type": "json", + "label": "Attributes to set", + "default": { "email": "{person.properties.email}" }, + "secret": false, + "required": true + }, + { + "key": "event", + "type": "json", + "label": "Event payload", + "default": { + "properties": "{event.properties}", + "external_id": "{event.distinct_id}", + "name": "{event.event}", + "time": "{event.timestamp}" + }, + "secret": false, + "required": true + } + ], + "category": ["Customer Success"], + "filters": null, + "masking": null, + "icon_url": "/static/services/braze.png" + }, + { + "sub_templates": null, + "status": "alpha", + "id": "template-clearbit", + "name": "Clearbit", + "description": "Loads data from the Clearbit API and tracks an additional event with the enriched data if found. Once enriched, the person will not be enriched again.", + "hog": "let api_key := inputs.api_key\nlet email := inputs.email\n\nif (empty(email) or event.event == '$set' or person.properties.clearbit_enriched) {\n return false\n}\n\nlet response := fetch(f'https://person-stream.clearbit.com/v2/combined/find?email={email}', {\n 'method': 'GET',\n 'headers': {\n 'Authorization': f'Bearer {api_key}'\n }\n})\nif (response.status == 200 and not empty(response.body.person)) {\n print('Clearbit data found - sending event to PostHog')\n postHogCapture({\n 'event': '$set',\n 'distinct_id': event.distinct_id,\n 'properties': {\n '$lib': 'hog_function',\n '$hog_function_source': source.url,\n '$set_once': {\n 'person': response.body.person,\n 'company': response.body.company,\n 'clearbit_enriched': true\n }\n }\n })\n} else {\n print('No Clearbit data found')\n}", + "inputs_schema": [ + { "key": "api_key", "type": "string", "label": "Clearbit API Key", "secret": true, "required": true }, + { + "key": "email", + "type": "string", + "label": "Email of the user", + "description": "Where to find the email for the user to be checked with Clearbit", + "default": "{person.properties.email}", + "secret": false, + "required": true + } + ], + "category": ["Analytics"], + "filters": null, + "masking": null, + "icon_url": "/static/services/clearbit.png" + }, + { + "sub_templates": null, + "status": "beta", + "id": "template-customerio", + "name": "Customer.io", + "description": "Identify or track events against customers in Customer.io", + "hog": "let action := inputs.action\nlet name := event.event\n\nlet hasIdentifier := false\n\nfor (let key, value in inputs.identifiers) {\n if (not empty(value)) {\n hasIdentifier := true\n }\n}\n\nif (not hasIdentifier) {\n print('No identifier set. Skipping as at least 1 identifier is needed.')\n return\n}\n\nif (action == 'automatic') {\n if (event.event in ('$identify', '$set')) {\n action := 'identify'\n name := null\n } else if (event.event == '$pageview') {\n action := 'page'\n name := event.properties.$current_url\n } else if (event.event == '$screen') {\n action := 'screen'\n name := event.properties.$screen_name\n } else {\n action := 'event'\n }\n}\n\nlet attributes := inputs.include_all_properties ? action == 'identify' ? person.properties : event.properties : {}\nif (inputs.include_all_properties and action != 'identify' and not empty(event.elements_chain)) {\n attributes['$elements_chain'] := event.elements_chain\n}\nlet timestamp := toInt(toUnixTimestamp(toDateTime(event.timestamp)))\n\nfor (let key, value in inputs.attributes) {\n attributes[key] := value\n}\n\nlet res := fetch(f'https://{inputs.host}/api/v2/entity', {\n 'method': 'POST',\n 'headers': {\n 'User-Agent': 'PostHog Customer.io App',\n 'Authorization': f'Basic {base64Encode(f'{inputs.site_id}:{inputs.token}')}',\n 'Content-Type': 'application/json'\n },\n 'body': {\n 'type': 'person',\n 'action': action,\n 'name': name,\n 'identifiers': inputs.identifiers,\n 'attributes': attributes,\n 'timestamp': timestamp\n }\n})\n\nif (res.status >= 400) {\n throw Error(f'Error from customer.io api: {res.status}: {res.body}');\n}", + "inputs_schema": [ + { + "key": "site_id", + "type": "string", + "label": "Customer.io site ID", + "secret": false, + "required": true + }, + { + "key": "token", + "type": "string", + "label": "Customer.io API Key", + "description": "You can find your API key in your Customer.io account settings (https://fly.customer.io/settings/api_credentials)", + "secret": true, + "required": true + }, + { + "key": "host", + "type": "choice", + "choices": [ + { "label": "US (track.customer.io)", "value": "track.customer.io" }, + { "label": "EU (track-eu.customer.io)", "value": "track-eu.customer.io" } + ], + "label": "Customer.io region", + "description": "Use the EU variant if your Customer.io account is based in the EU region", + "default": "track.customer.io", + "secret": false, + "required": true + }, + { + "key": "identifiers", + "type": "dictionary", + "label": "Identifiers", + "description": "You can choose to fill this from an `email` property or an `id` property. If the value is empty nothing will be sent. See here for more information: https://customer.io/docs/api/track/#operation/entity", + "default": { "email": "{person.properties.email}" }, + "secret": false, + "required": true + }, + { + "key": "action", + "type": "choice", + "label": "Action", + "description": "Choose the action to be tracked. Automatic will convert $identify, $pageview and $screen to identify, page and screen automatically - otherwise defaulting to event", + "default": "automatic", + "choices": [ + { "label": "Automatic", "value": "automatic" }, + { "label": "Identify", "value": "identify" }, + { "label": "Event", "value": "event" }, + { "label": "Page", "value": "page" }, + { "label": "Screen", "value": "screen" }, + { "label": "Delete", "value": "delete" } + ], + "secret": false, + "required": true + }, + { + "key": "include_all_properties", + "type": "boolean", + "label": "Include all properties as attributes", + "description": "If set, all event properties will be included as attributes. Individual attributes can be overridden below. For identify events the Person properties will be used.", + "default": false, + "secret": false, + "required": true + }, + { + "key": "attributes", + "type": "dictionary", + "label": "Attribute mapping", + "description": "Map of Customer.io attributes and their values. You can use the filters section to filter out unwanted events.", + "default": { + "email": "{person.properties.email}", + "lastname": "{person.properties.lastname}", + "firstname": "{person.properties.firstname}" + }, + "secret": false, + "required": false + } + ], + "category": ["Email Marketing"], + "filters": { + "events": [ + { "id": "$identify", "name": "$identify", "type": "events", "order": 0 }, + { "id": "$pageview", "name": "$pageview", "type": "events", "order": 0 } + ], + "actions": [], + "filter_test_accounts": true + }, + "masking": null, + "icon_url": "/static/services/customerio.png" + }, + { + "sub_templates": null, + "status": "beta", + "id": "template-engage-so", + "name": "Engage.so", + "description": "Send events to Engage.so", + "hog": "fetch('https://api.engage.so/posthog', {\n 'method': 'POST',\n 'headers': {\n 'Authorization': f'Basic {base64Encode(f'{inputs.public_key}:{inputs.private_key}')}',\n 'Content-Type': 'application/json'\n },\n 'body': event\n})", + "inputs_schema": [ + { + "key": "public_key", + "type": "string", + "label": "Public key", + "description": "Get your public key from your Engage dashboard (Settings -> Account)", + "secret": true, + "required": true + }, + { + "key": "private_key", + "type": "string", + "label": "Private key", + "description": "Get your private key from your Engage dashboard (Settings -> Account)", + "secret": true, + "required": true + } + ], + "category": ["Email Marketing"], + "filters": { + "events": [ + { "id": "$identify", "name": "$identify", "type": "events", "order": 0 }, + { "id": "$set", "name": "$set", "type": "events", "order": 1 }, + { "id": "$groupidentify", "name": "$groupidentify", "type": "events", "order": 2 }, + { "id": "$unset", "name": "$unset", "type": "events", "order": 3 }, + { "id": "$create_alias", "name": "$create_alias", "type": "events", "order": 4 } + ], + "actions": [], + "filter_test_accounts": true + }, + "masking": null, + "icon_url": "/static/services/engage.png" + }, + { + "sub_templates": null, + "status": "beta", + "id": "template-gleap", + "name": "Gleap", + "description": "Updates a contact in Gleap", + "hog": "let action := inputs.action\nlet name := event.event\n\nif (empty(inputs.userId)) {\n print('No User ID set. Skipping...')\n return\n}\n\nlet attributes := inputs.include_all_properties ? person.properties : {}\n\nattributes['userId'] := inputs.userId\n\nfor (let key, value in inputs.attributes) {\n if (not empty(value)) {\n attributes[key] := value\n }\n}\n\nlet res := fetch(f'https://api.gleap.io/admin/identify', {\n 'method': 'POST',\n 'headers': {\n 'User-Agent': 'PostHog Gleap.io App',\n 'Api-Token': inputs.apiKey,\n 'Content-Type': 'application/json'\n },\n 'body': attributes\n})\n\nif (res.status >= 400) {\n print('Error from gleap.io api:', res.status, res.body)\n}", + "inputs_schema": [ + { "key": "apiKey", "type": "string", "label": "Gleap.io API Key", "secret": true, "required": true }, + { + "key": "userId", + "type": "string", + "label": "User ID", + "description": "You can choose to fill this from an `email` property or an `id` property. If the value is empty nothing will be sent. See here for more information: https://docs.gleap.io/server/rest-api", + "default": "{person.id}", + "secret": false, + "required": true + }, + { + "key": "include_all_properties", + "type": "boolean", + "label": "Include all properties as attributes", + "description": "If set, all person properties will be included as attributes. Individual attributes can be overridden below.", + "default": false, + "secret": false, + "required": true + }, + { + "key": "attributes", + "type": "dictionary", + "label": "Attribute mapping", + "description": "Map of Gleap.io attributes 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 + } + ], + "category": ["Customer Success"], + "filters": { + "events": [ + { "id": "$identify", "name": "$identify", "type": "events", "order": 0 }, + { "id": "$set", "name": "$set", "type": "events", "order": 1 } + ], + "actions": [], + "filter_test_accounts": true + }, + "masking": null, + "icon_url": "/static/services/gleap.png" + }, + { + "sub_templates": null, + "status": "beta", + "id": "template-google-cloud-storage", + "name": "Google Cloud Storage", + "description": "Send data to GCS. This creates a file per event.", + "hog": "let res := fetch(f'https://storage.googleapis.com/upload/storage/v1/b/{encodeURLComponent(inputs.bucketName)}/o?uploadType=media&name={encodeURLComponent(inputs.filename)}', {\n 'method': 'POST',\n 'headers': {\n 'Authorization': f'Bearer {inputs.auth.access_token}',\n 'Content-Type': 'application/json'\n },\n 'body': inputs.payload\n})\n\nif (res.status >= 200 and res.status < 300) {\n print('Event sent successfully!')\n} else {\n throw Error('Error sending event', res)\n}", + "inputs_schema": [ + { + "key": "auth", + "type": "integration", + "integration": "google-cloud-storage", + "label": "Google Cloud service account", + "secret": false, + "required": true + }, + { "key": "bucketName", "type": "string", "label": "Bucket name", "secret": false, "required": true }, + { + "key": "filename", + "type": "string", + "label": "Filename", + "default": "{toDate(event.timestamp)}/{event.timestamp}-{event.uuid}.json", + "secret": false, + "required": true + }, + { + "key": "payload", + "type": "string", + "label": "File contents", + "default": "{jsonStringify({ 'event': event, 'person': person })}", + "secret": false, + "required": true + } + ], + "category": ["Custom"], + "filters": null, + "masking": null, + "icon_url": "/static/services/google-cloud-storage.png" + }, + { + "sub_templates": null, + "status": "beta", + "id": "template-google-pubsub", + "name": "Google Pub/Sub", + "description": "Send data to a Google Pub/Sub topic", + "hog": "let headers := () -> {\n 'Authorization': f'Bearer {inputs.auth.access_token}',\n 'Content-Type': 'application/json'\n}\nlet message := () -> {\n 'messageId': event.uuid,\n 'data': base64Encode(jsonStringify(inputs.payload)),\n 'attributes': inputs.attributes\n}\nlet res := fetch(f'https://pubsub.googleapis.com/v1/{inputs.topicId}:publish', {\n 'method': 'POST',\n 'headers': headers(),\n 'body': jsonStringify({ 'messages': [message()] })\n})\n\nif (res.status >= 200 and res.status < 300) {\n print('Event sent successfully!')\n} else {\n throw Error('Error sending event', res)\n}", + "inputs_schema": [ + { + "key": "auth", + "type": "integration", + "integration": "google-pubsub", + "label": "Google Cloud service account", + "secret": false, + "required": true + }, + { "key": "topicId", "type": "string", "label": "Topic name", "secret": false, "required": true }, + { + "key": "payload", + "type": "json", + "label": "Message Payload", + "default": { "event": "{event}", "person": "{person}" }, + "secret": false, + "required": false + }, + { + "key": "attributes", + "type": "json", + "label": "Attributes", + "default": {}, + "secret": false, + "required": false + } + ], + "category": ["Custom"], + "filters": null, + "masking": null, + "icon_url": "/static/services/google-cloud.png" + }, + { + "sub_templates": null, + "status": "beta", + "id": "template-hubspot", + "name": "Hubspot", + "description": "Creates a new contact in Hubspot whenever an event is triggered.", + "hog": "let properties := inputs.properties\nproperties.email := inputs.email\n\nif (empty(properties.email)) {\n print('`email` input is empty. Not creating a contact.')\n return\n}\n\nlet headers := {\n 'Authorization': f'Bearer {inputs.oauth.access_token}',\n 'Content-Type': 'application/json'\n}\n\nlet res := fetch('https://api.hubapi.com/crm/v3/objects/contacts', {\n 'method': 'POST',\n 'headers': headers,\n 'body': {\n 'properties': properties\n }\n})\n\nif (res.status == 409) {\n let existingId := replaceOne(res.body.message, 'Contact already exists. Existing ID: ', '')\n let updateRes := fetch(f'https://api.hubapi.com/crm/v3/objects/contacts/{existingId}', {\n 'method': 'PATCH',\n 'headers': headers,\n 'body': {\n 'properties': properties\n }\n })\n\n if (updateRes.status != 200 or updateRes.body.status == 'error') {\n print('Error updating contact:', updateRes.body)\n return\n }\n print('Contact updated successfully!')\n return\n} else if (res.status >= 300 or res.body.status == 'error') {\n print('Error creating contact:', res.body)\n return\n} else {\n print('Contact created successfully!')\n}", + "inputs_schema": [ + { + "key": "oauth", + "type": "integration", + "integration": "hubspot", + "label": "Hubspot connection", + "secret": false, + "required": true + }, + { + "key": "email", + "type": "string", + "label": "Email of the user", + "description": "Where to find the email for the contact to be created. You can use the filters section to filter out unwanted emails or internal users.", + "default": "{person.properties.email}", + "secret": false, + "required": true + }, + { + "key": "properties", + "type": "dictionary", + "label": "Property mapping", + "description": "Map any event properties to Hubspot properties.", + "default": { + "firstname": "{person.properties.firstname}", + "lastname": "{person.properties.lastname}", + "company": "{person.properties.company}", + "phone": "{person.properties.phone}", + "website": "{person.properties.website}" + }, + "secret": false, + "required": true + } + ], + "category": ["CRM", "Customer Success"], + "filters": { + "events": [{ "id": "$identify", "name": "$identify", "type": "events", "order": 0 }], + "actions": [], + "filter_test_accounts": true + }, + "masking": null, + "icon_url": "/static/services/hubspot.png" + }, + { + "sub_templates": null, + "status": "beta", + "id": "template-Intercom", + "name": "Intercom", + "description": "Send events and contact information to Intercom", + "hog": "if (empty(inputs.email)) {\n print('`email` input is empty. Skipping.')\n return\n}\n\nlet res := fetch(f'https://{inputs.host}/events', {\n 'method': 'POST',\n 'headers': {\n 'Authorization': f'Bearer {inputs.access_token}',\n 'Content-Type': 'application/json',\n 'Accept': 'application/json'\n },\n 'body': {\n 'event_name': event.event,\n 'created_at': toInt(toUnixTimestamp(toDateTime(event.timestamp))),\n 'email': inputs.email,\n 'id': event.distinct_id,\n }\n})\n\nif (res.status >= 200 and res.status < 300) {\n print('Event sent successfully!')\n return\n}\n\nif (res.status == 404) {\n print('No existing contact found for email')\n return\n}\n\nprint('Error sending event:', res.status, res.body)", + "inputs_schema": [ + { + "key": "access_token", + "type": "string", + "label": "Intercom access token", + "description": "Create an Intercom app (https://developers.intercom.com/docs/build-an-integration/learn-more/authentication), then go to Configure > Authentication to find your token.", + "secret": true, + "required": true + }, + { + "key": "host", + "type": "choice", + "choices": [ + { "label": "US (api.intercom.io)", "value": "api.intercom.io" }, + { "label": "EU (api.eu.intercom.com)", "value": "api.eu.intercom.com" } + ], + "label": "Data region", + "description": "Use the EU variant if your Intercom account is based in the EU region", + "default": "api.intercom.io", + "secret": false, + "required": true + }, + { + "key": "email", + "type": "string", + "label": "Email of the user", + "description": "Where to find the email for the contact to be created. You can use the filters section to filter out unwanted emails or internal users.", + "default": "{person.properties.email}", + "secret": false, + "required": true + } + ], + "category": ["Customer Success"], + "filters": { + "events": [{ "id": "$identify", "name": "$identify", "type": "events", "order": 0 }], + "actions": [], + "filter_test_accounts": true + }, + "masking": null, + "icon_url": "/static/services/intercom.png" + }, + { + "sub_templates": null, + "status": "beta", + "id": "template-knock", + "name": "Knock", + "description": "Send events to Knock", + "hog": "if (empty(inputs.userId)) {\n print('No User ID set. Skipping...')\n return\n}\n\nlet body := {\n 'type': 'track',\n 'event': event.event,\n 'userId': inputs.userId,\n 'properties': inputs.include_all_properties ? event.properties : {},\n 'messageId': event.uuid,\n 'timestamp': event.timestamp\n}\nif (inputs.include_all_properties and not empty(event.elements_chain)) {\n body['properties']['$elements_chain'] := event.elements_chain\n}\n\nfor (let key, value in inputs.attributes) {\n if (not empty(value)) {\n body['properties'][key] := value\n }\n}\n\nlet res := fetch(inputs.webhookUrl, {\n 'method': 'POST',\n 'headers': {\n 'Content-Type': 'application/json'\n },\n 'body': body\n})\n\nif (res.status >= 400) {\n print('Error from knock.app api:', res.status, res.body)\n}", + "inputs_schema": [ + { + "key": "webhookUrl", + "type": "string", + "label": "Knock.app webhook destination URL", + "secret": false, + "required": true + }, + { + "key": "userId", + "type": "string", + "label": "User ID", + "description": "You can choose to fill this from an `email` property or an `id` property. If the value is empty nothing will be sent. See here for more information: https://docs.gleap.io/server/rest-api", + "default": "{person.id}", + "secret": false, + "required": true + }, + { + "key": "include_all_properties", + "type": "boolean", + "label": "Include all properties as attributes", + "description": "If set, all event properties will be included as attributes. Individual attributes can be overridden below.", + "default": false, + "secret": false, + "required": true + }, + { + "key": "attributes", + "type": "dictionary", + "label": "Attribute mapping", + "description": "Map of Knock.app attributes and their values. You can use the filters section to filter out unwanted events.", + "default": { "price": "{event.properties.price}" }, + "secret": false, + "required": false + } + ], + "category": ["SMS & Push Notifications"], + "filters": null, + "masking": null, + "icon_url": "/static/services/knock.png" + }, + { + "sub_templates": null, + "status": "beta", + "id": "template-loops", + "name": "Loops", + "description": "Send events to Loops", + "hog": "let apiKey := inputs.apiKey\n\nlet payload := {\n 'userId': event.distinct_id,\n 'eventName': event.event == '$set' ? '$identify' : event.event,\n 'email': person.properties.email\n}\nfor (let key, value in person.properties) {\n payload[key] := value\n}\nfetch('https://app.loops.so/api/v1/events/send', {\n 'method': 'POST',\n 'headers': {\n 'Content-Type': 'application/json',\n 'Authorization': f'Bearer {apiKey}',\n },\n 'body': payload\n})", + "inputs_schema": [ + { + "key": "apiKey", + "type": "string", + "label": "Loops API Key", + "description": "Loops API Key", + "default": "", + "secret": true, + "required": true + } + ], + "category": ["Email Marketing"], + "filters": { + "events": [ + { "id": "$identify", "name": "$identify", "type": "events", "order": 0 }, + { "id": "$set", "name": "$set", "type": "events", "order": 1 } + ], + "actions": [], + "filter_test_accounts": true + }, + "masking": null, + "icon_url": "/static/services/loops.png" + }, + { + "sub_templates": null, + "status": "alpha", + "id": "template-mailgun-send-email", + "name": "Mailgun", + "description": "Send emails using the Mailgun HTTP API", + "hog": "if (empty(inputs.template.to)) {\n return false\n}\n\nfun multiPartFormEncode(data) {\n let boundary := f'---011000010111000001101001'\n let bodyBoundary := f'--{boundary}\\r\\n'\n let body := bodyBoundary\n\n for (let key, value in data) {\n if (not empty(value)) {\n body := f'{body}Content-Disposition: form-data; name=\"{key}\"\\r\\n\\r\\n{value}\\r\\n{bodyBoundary}'\n }\n }\n\n return {\n 'body': body,\n 'contentType': f'multipart/form-data; boundary={boundary}'\n }\n}\n\nlet form := multiPartFormEncode({\n 'from': inputs.template.from,\n 'to': inputs.template.to,\n 'subject': inputs.template.subject,\n 'text': inputs.template.text,\n 'html': inputs.template.html\n})\n\nlet res := fetch(f'https://{inputs.host}/v3/{inputs.domain_name}/messages', {\n 'method': 'POST',\n 'headers': {\n 'Authorization': f'Basic {base64Encode(f'api:{inputs.api_key}')}',\n 'Content-Type': form.contentType\n },\n 'body': form.body\n})\n\nif (res.status >= 400) {\n print('Error from Mailgun API:', res.status, res.body)\n}", + "inputs_schema": [ + { + "key": "domain_name", + "type": "string", + "label": "Mailgun Domain Name", + "description": "The domain name of the Mailgun account", + "secret": false, + "required": true + }, + { "key": "api_key", "type": "string", "label": "Mailgun API Key", "secret": true, "required": true }, + { + "key": "host", + "type": "choice", + "choices": [ + { "label": "US (api.mailgun.net)", "value": "api.mailgun.net" }, + { "label": "EU (api.eu.mailgun.net)", "value": "api.eu.mailgun.net" } + ], + "label": "Region", + "default": "api.eu.mailgun.net", + "secret": false, + "required": true + }, + { + "key": "template", + "type": "email", + "label": "Email template", + "default": { "to": "{person.properties.email}" }, + "secret": false, + "required": true + } + ], + "category": ["Email Marketing"], + "filters": { + "events": [{ "id": "", "name": "", "type": "events", "order": 0 }], + "actions": [], + "filter_test_accounts": true + }, + "masking": null, + "icon_url": "/static/services/mailgun.png" + }, + { + "sub_templates": null, + "status": "beta", + "id": "template-mailjet-create-contact", + "name": "Mailjet", + "description": "Add contacts to Mailjet", + "hog": "if (empty(inputs.email)) {\n return false\n}\n\nfetch(f'https://api.mailjet.com/v3/REST/contact/', {\n 'method': 'POST',\n 'headers': {\n 'Authorization': f'Bearer {inputs.api_key}',\n 'Content-Type': 'application/json'\n },\n 'body': {\n 'Email': inputs.email,\n 'Name': inputs.name,\n 'IsExcludedFromCampaigns': inputs.is_excluded_from_campaigns\n }\n})", + "inputs_schema": [ + { "key": "api_key", "type": "string", "label": "Mailjet API Key", "secret": true, "required": true }, + { + "key": "email", + "type": "string", + "label": "Email of the user", + "description": "Where to find the email for the user to be checked with Mailjet", + "default": "{person.properties.email}", + "secret": false, + "required": true + }, + { + "key": "name", + "type": "string", + "label": "Name", + "description": "Name of the contact", + "default": "{person.properties.first_name} {person.properties.last_name}", + "secret": false, + "required": false + }, + { + "key": "is_excluded_from_campaigns", + "type": "boolean", + "label": "Is excluded from campaigns", + "description": "Whether the contact should be excluded from campaigns", + "default": false, + "secret": false, + "required": false + } + ], + "category": ["Email Marketing"], + "filters": { + "events": [{ "id": "$identify", "name": "$identify", "type": "events", "order": 0 }], + "actions": [], + "filter_test_accounts": true + }, + "masking": null, + "icon_url": "/static/services/mailjet.png" + }, + { + "sub_templates": null, + "status": "beta", + "id": "template-mailjet-update-contact-list", + "name": "Mailjet", + "description": "Update a Mailjet contact list", + "hog": "if (empty(inputs.email)) {\n return false\n}\n\nfetch(f'https://api.mailjet.com/v3/REST/contact/{inputs.email}/managecontactlists', {\n 'method': 'POST',\n 'headers': {\n 'Authorization': f'Bearer {inputs.api_key}',\n 'Content-Type': 'application/json'\n },\n 'body': {\n 'ContactsLists':[\n {\n 'Action': inputs.action,\n 'ListID': inputs.contact_list_id\n },\n ]\n }\n})", + "inputs_schema": [ + { "key": "api_key", "type": "string", "label": "Mailjet API Key", "secret": true, "required": true }, + { + "key": "email", + "type": "string", + "label": "Email of the user", + "description": "Where to find the email for the user to be checked with Mailjet", + "default": "{person.properties.email}", + "secret": false, + "required": true + }, + { + "key": "contact_list_id", + "type": "string", + "label": "Contact list ID", + "description": "ID of the contact list", + "secret": false, + "required": true + }, + { + "key": "action", + "type": "choice", + "label": "Action", + "secret": false, + "default": "addnoforce", + "required": true, + "choices": [ + { "label": "Add", "value": "addnoforce" }, + { "label": "Add (force)", "value": "addforce" }, + { "label": "Remove", "value": "remove" }, + { "label": "Unsubscribe", "value": "unsub" } + ] + } + ], + "category": ["Email Marketing"], + "filters": { + "events": [{ "id": "$identify", "name": "$identify", "type": "events", "order": 0 }], + "actions": [], + "filter_test_accounts": true + }, + "masking": null, + "icon_url": "/static/services/mailjet.png" + }, + { + "sub_templates": null, + "status": "beta", + "id": "template-posthog-replicator", + "name": "PostHog", + "description": "Send a copy of the incoming data in realtime to another PostHog instance", + "hog": "let host := inputs.host\nlet token := inputs.token\nlet include_all_properties := inputs.include_all_properties\nlet propertyOverrides := inputs.properties\nlet properties := include_all_properties ? event.properties : {}\n\nfor (let key, value in propertyOverrides) {\n properties[key] := value\n}\n\nfetch(f'{host}/e', {\n 'method': 'POST',\n 'headers': {\n 'Content-Type': 'application/json'\n },\n 'body': {\n 'token': token,\n 'event': event.event,\n 'timestamp': event.timestamp,\n 'distinct_id': event.distinct_id,\n 'elements_chain': event.elements_chain,\n 'properties': properties\n }\n})", + "inputs_schema": [ + { + "key": "host", + "type": "string", + "label": "PostHog host", + "description": "For cloud accounts this is either https://us.i.posthog.com or https://eu.i.posthog.com", + "default": "https://us.i.posthog.com", + "secret": false, + "required": true + }, + { "key": "token", "type": "string", "label": "PostHog API key", "secret": false, "required": true }, + { + "key": "include_all_properties", + "type": "boolean", + "label": "Include all properties by default", + "description": "If set, all event properties will be included in the payload. Individual properties can be overridden below.", + "default": true, + "secret": false, + "required": true + }, + { + "key": "properties", + "type": "dictionary", + "label": "Property overrides", + "description": "Provided values will override the event properties.", + "default": {}, + "secret": false, + "required": false + } + ], + "category": ["Custom", "Analytics"], + "filters": null, + "masking": null, + "icon_url": "/static/posthog-icon.svg" + }, + { + "sub_templates": null, + "status": "alpha", + "id": "template-rudderstack", + "name": "RudderStack", + "description": "Send data to RudderStack", + "hog": "fun getPayload() {\n let rudderPayload := {\n 'context': {\n 'app': {\n 'name': 'PostHogPlugin',\n },\n 'os': {},\n 'page': {},\n 'screen': {},\n 'library': {},\n },\n 'channel': 's2s',\n 'type': 'track',\n 'properties': {},\n }\n\n if (not empty(event.properties.$os)) rudderPayload.context.os.name := event.properties.$os\n if (not empty(event.properties.$browser)) rudderPayload.context.browser := event.properties.$browser\n if (not empty(event.properties.$browser_version)) rudderPayload.context.browser_version := event.properties.$browser_version\n if (not empty(event.properties.$host)) rudderPayload.context.page.host := event.properties.$host\n if (not empty(event.properties.$current_url)) rudderPayload.context.page.url := event.properties.$current_url\n if (not empty(event.properties.$path)) rudderPayload.context.page.path := event.properties.$path\n if (not empty(event.properties.$referrer)) rudderPayload.context.page.referrer := event.properties.$referrer\n if (not empty(event.properties.$initial_referrer)) rudderPayload.context.page.initial_referrer := event.properties.$initial_referrer\n if (not empty(event.properties.$referring_domain)) rudderPayload.context.page.referring_domain := event.properties.$referring_domain\n if (not empty(event.properties.$initial_referring_domain)) rudderPayload.context.page.initial_referring_domain := event.properties.$initial_referring_domain\n if (not empty(event.properties.$screen_height)) rudderPayload.context.screen.height := event.properties.$screen_height\n if (not empty(event.properties.$screen_width)) rudderPayload.context.screen.width := event.properties.$screen_width\n if (not empty(event.properties.$lib)) rudderPayload.context.library.name := event.properties.$lib\n if (not empty(event.properties.$lib_version)) rudderPayload.context.library.version := event.properties.$lib_version\n if (not empty(event.$ip)) rudderPayload.context.ip := event.$ip\n if (not empty(event.properties.$active_feature_flags)) rudderPayload.context.active_feature_flags := event.properties.$active_feature_flags\n if (not empty(event.properties.token)) rudderPayload.context.token := event.properties.token\n if (not empty(event.uuid)) rudderPayload.messageId := event.uuid\n if (not empty(event.timestamp)) rudderPayload.originalTimestamp := event.timestamp\n if (not empty(inputs.identifier)) rudderPayload.userId := inputs.identifier\n if (not empty(event.properties.$anon_distinct_id ?? event.properties.$device_id ?? event.properties.distinct_id)) rudderPayload.anonymousId := event.properties.$anon_distinct_id ?? event.properties.$device_id ?? event.properties.distinct_id\n\n if (event.event in ('$identify', '$set')) {\n rudderPayload.type := 'identify'\n if (not empty(event.properties.$set)) rudderPayload.context.trait := event.properties.$set\n if (not empty(event.properties.$set)) rudderPayload.traits := event.properties.$set\n } else if (event.event == '$create_alias') {\n rudderPayload.type := 'alias'\n if (not empty(event.properties.alias)) rudderPayload.userId := event.properties.alias\n if (not empty(event.distinct_id)) rudderPayload.previousId := event.distinct_id\n } else if (event.event == '$pageview') {\n rudderPayload.type := 'page'\n if (not empty(event.properties.name)) rudderPayload.name := event.properties.name\n if (not empty(event.properties.$host)) rudderPayload.properties.host := event.properties.$host\n if (not empty(event.properties.$current_url)) rudderPayload.properties.url := event.properties.$current_url\n if (not empty(event.properties.$pathname)) rudderPayload.properties.path := event.properties.$pathname\n if (not empty(event.properties.$referrer)) rudderPayload.properties.referrer := event.properties.$referrer\n if (not empty(event.properties.$initial_referrer)) rudderPayload.properties.initial_referrer := event.properties.$initial_referrer\n if (not empty(event.properties.$referring_domain)) rudderPayload.properties.referring_domain := event.properties.$referring_domain\n if (not empty(event.properties.$initial_referring_domain)) rudderPayload.properties.initial_referring_domain := event.properties.$initial_referring_domain\n } else if (event.event == '$autocapture') {\n rudderPayload.type := 'track'\n if (not empty(event.properties.$event_type)) rudderPayload.event := event.properties.$event_type\n } else {\n rudderPayload.type := 'track'\n if (not empty(event.event)) rudderPayload.event := event.event\n }\n\n for (let key, value in event.properties) {\n if (value != null and not key like '$%') {\n rudderPayload.properties[key] := value\n }\n }\n\n return {\n 'method': 'POST',\n 'headers': {\n 'Content-Type': 'application/json',\n 'Authorization': f'Basic {base64Encode(f'{inputs.token}:')}',\n },\n 'body': {\n 'batch': [rudderPayload],\n 'sentAt': now()\n }\n }\n}\n\nfetch(f'{inputs.host}/v1/batch', getPayload())", + "inputs_schema": [ + { + "key": "host", + "type": "string", + "label": "Rudderstack host", + "description": "The Rudderstack destination instance", + "default": "https://hosted.rudderlabs.com", + "secret": false, + "required": true + }, + { + "key": "token", + "type": "string", + "label": "Write API key", + "description": "RudderStack Source Writekey", + "secret": true, + "required": true + }, + { + "key": "identifier", + "type": "string", + "label": "Identifier", + "default": "{person.id}", + "secret": false, + "required": true + } + ], + "category": ["Custom"], + "filters": null, + "masking": null, + "icon_url": "/static/services/rudderstack.png" + }, + { + "sub_templates": null, + "status": "alpha", + "id": "template-salesforce-create", + "name": "Salesforce", + "description": "Create objects in Salesforce", + "hog": "let getPayload := () -> {\n let properties := {}\n if (inputs.include_all_event_properties) {\n if (not empty(event.elements_chain)) {\n properties['$elements_chain'] := event.elements_chain\n }\n for (let key, value in event.properties) {\n properties[key] := value\n }\n }\n if (inputs.include_all_person_properties) {\n for (let key, value in person.properties) {\n properties[key] := value\n }\n }\n for (let key, value in inputs.properties) {\n properties[key] := value\n }\n return properties\n}\n\nlet res := fetch(f'{inputs.oauth.instance_url}/services/data/v61.0/sobjects/{inputs.path}', {\n 'body': getPayload(),\n 'method': 'POST',\n 'headers': {\n 'Authorization': f'Bearer {inputs.oauth.access_token}',\n 'Content-Type': 'application/json'\n }\n});\n\nif (res.status >= 400) {\n print('Bad response:', res.status, res.body)\n}", + "inputs_schema": [ + { + "key": "oauth", + "type": "integration", + "integration": "salesforce", + "label": "Salesforce account", + "secret": false, + "required": true + }, + { + "key": "path", + "type": "string", + "label": "Object path", + "description": "The path to the object you want to create.", + "default": "Contact", + "secret": false, + "required": true + }, + { + "key": "include_all_event_properties", + "type": "boolean", + "label": "Include all event properties as attributes", + "description": "If set, all event properties will be included as attributes. Individual attributes can be overridden below.", + "default": false, + "secret": false, + "required": true + }, + { + "key": "include_all_person_properties", + "type": "boolean", + "label": "Include all person properties as attributes", + "description": "If set, all person properties will be included as attributes. Individual attributes can be overridden below.", + "default": false, + "secret": false, + "required": true + }, + { + "key": "properties", + "type": "json", + "label": "Additional properties", + "description": "Additional properties for the Salesforce Object.", + "default": { "email": "{person.properties.email}" }, + "secret": false, + "required": true + } + ], + "category": ["CRM", "Customer Success"], + "filters": { + "events": [{ "id": "$identify", "name": "$identify", "type": "events", "order": 0 }], + "actions": [], + "filter_test_accounts": true + }, + "masking": null, + "icon_url": "/static/services/salesforce.png" + }, + { + "sub_templates": null, + "status": "alpha", + "id": "template-salesforce-update", + "name": "Salesforce", + "description": "Update objects in Salesforce", + "hog": "let getPayload := () -> {\n let properties := {}\n if (inputs.include_all_event_properties) {\n for (let key, value in event.properties) {\n properties[key] := value\n }\n }\n if (inputs.include_all_person_properties) {\n for (let key, value in person.properties) {\n properties[key] := value\n }\n }\n for (let key, value in inputs.properties) {\n properties[key] := value\n }\n return properties\n}\n\nlet res := fetch(f'{inputs.oauth.instance_url}/services/data/v61.0/sobjects/{inputs.path}', {\n 'body': getPayload(),\n 'method': 'PATCH',\n 'headers': {\n 'Authorization': f'Bearer {inputs.oauth.access_token}',\n 'Content-Type': 'application/json'\n }\n});\n\nif (res.status >= 400) {\n print('Bad response:', res.status, res.body)\n}", + "inputs_schema": [ + { + "key": "oauth", + "type": "integration", + "integration": "salesforce", + "label": "Salesforce account", + "secret": false, + "required": true + }, + { + "key": "path", + "type": "string", + "label": "Object path", + "description": "The path to the object you want to create or update. This can be a standard object like 'Contact' for creating records or `Lead/Email/{person.properties.email}` for updating a lead by email. See https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/dome_upsert.htm for more information.", + "default": "Leads/Email/{person.properties.email}", + "secret": false, + "required": true + }, + { + "key": "include_all_event_properties", + "type": "boolean", + "label": "Include all event properties as attributes", + "description": "If set, all event properties will be included as attributes. Individual attributes can be overridden below.", + "default": false, + "secret": false, + "required": true + }, + { + "key": "include_all_person_properties", + "type": "boolean", + "label": "Include all person properties as attributes", + "description": "If set, all person properties will be included as attributes. Individual attributes can be overridden below.", + "default": false, + "secret": false, + "required": true + }, + { + "key": "properties", + "type": "json", + "label": "Additional properties", + "description": "Additional properties for the Salesforce Object.", + "default": { "email": "{person.properties.email}", "browser": "{event.properties.$browser}" }, + "secret": false, + "required": true + } + ], + "category": ["CRM", "Customer Success"], + "filters": { + "events": [{ "id": "$identify", "name": "$identify", "type": "events", "order": 0 }], + "actions": [], + "filter_test_accounts": true + }, + "masking": null, + "icon_url": "/static/services/salesforce.png" + }, + { + "sub_templates": null, + "status": "beta", + "id": "template-sendgrid", + "name": "Sendgrid", + "description": "Update marketing contacts in Sendgrid", + "hog": "if (empty(inputs.email)) {\n print('`email` input is empty. Not updating contacts.')\n return\n}\n\nlet contact := {\n 'email': inputs.email,\n}\n\nfor (let key, value in inputs.properties) {\n if (not empty(value)) {\n contact[key] := value\n }\n}\n\nlet headers := {\n 'Authorization': f'Bearer {inputs.api_key}',\n 'Content-Type': 'application/json'\n}\n\nif (not empty(inputs.custom_fields)) {\n let response := fetch('https://api.sendgrid.com/v3/marketing/field_definitions', {\n 'method': 'GET',\n 'headers': headers\n })\n if (response.status != 200) {\n throw Error(f'Could not fetch custom fields. Status: {response.status}')\n }\n contact['custom_fields'] := {}\n for (let obj in response.body?.custom_fields ?? {}) {\n let inputValue := inputs.custom_fields[obj.name]\n if (not empty(inputValue)) {\n contact['custom_fields'][obj.id] := inputValue\n }\n }\n}\n\nlet res := fetch('https://api.sendgrid.com/v3/marketing/contacts', {\n 'method': 'PUT',\n 'headers': headers,\n 'body': { 'contacts': [contact] }\n})\n\nif (res.status > 300) {\n print('Error updating contact:', res.status, res.body)\n}", + "inputs_schema": [ + { + "key": "api_key", + "type": "string", + "label": "Sendgrid API Key", + "description": "See https://app.sendgrid.com/settings/api_keys", + "secret": true, + "required": true + }, + { + "key": "email", + "type": "string", + "label": "The email of the user", + "default": "{person.properties.email}", + "secret": false, + "required": true + }, + { + "key": "properties", + "type": "dictionary", + "label": "Reserved fields", + "description": "The following field names are allowed: address_line_1, address_line_2, alternate_emails, anonymous_id, city, country, email, external_id, facebook, first_name, last_name, phone_number_id, postal_code, state_province_region, unique_name, whatsapp.", + "default": { + "first_name": "{person.properties.first_name}", + "last_name": "{person.properties.last_name}", + "city": "{person.properties.city}", + "country": "{person.properties.country}", + "postal_code": "{person.properties.postal_code}" + }, + "secret": false, + "required": true + }, + { + "key": "custom_fields", + "type": "dictionary", + "label": "Custom fields", + "description": "Configure custom fields in SendGrid before using them here: https://mc.sendgrid.com/custom-fields", + "default": {}, + "secret": false, + "required": false + } + ], + "category": ["Email Marketing"], + "filters": { + "events": [{ "id": "$identify", "name": "$identify", "type": "events", "order": 0 }], + "actions": [], + "filter_test_accounts": true + }, + "masking": null, + "icon_url": "/static/services/sendgrid.png" + }, + { + "sub_templates": null, + "status": "free", + "id": "template-zapier", + "name": "Zapier", + "description": "Sends a webhook templated by the incoming event data", + "hog": "let res := fetch(f'https://hooks.zapier.com/{inputs.hook}', {\n 'method': 'POST',\n 'body': inputs.body\n});\n\nif (inputs.debug) {\n print('Response', res.status, res.body);\n}", + "inputs_schema": [ + { + "key": "hook", + "type": "string", + "label": "Zapier hook path", + "description": "The path of the Zapier webhook. You can create your own or use our native Zapier integration https://zapier.com/apps/posthog/integrations", + "secret": false, + "required": true + }, + { + "key": "body", + "type": "json", + "label": "JSON Body", + "default": { + "hook": { + "id": "{source.url}", + "event": "{event}", + "target": "https://hooks.zapier.com/{inputs.hook}" + }, + "data": { + "eventUuid": "{event.uuid}", + "event": "{event.event}", + "teamId": "{project.id}", + "distinctId": "{event.distinct_id}", + "properties": "{event.properties}", + "elementsChain": "{event.elementsChain}", + "timestamp": "{event.timestamp}", + "person": { "uuid": "{person.id}", "properties": "{person.properties}" } + } + }, + "secret": false, + "required": false + }, + { + "key": "debug", + "type": "boolean", + "label": "Log responses", + "description": "Logs the response of http calls for debugging.", + "secret": false, + "required": false, + "default": false + } + ], + "category": ["Custom"], + "filters": null, + "masking": null, + "icon_url": "/static/services/zapier.png" + }, + { + "sub_templates": null, + "status": "beta", + "id": "template-zendesk", + "name": "Zendesk", + "description": "Update contacts in Zendesk", + "hog": "if (empty(inputs.email) or empty(inputs.name)) {\n print('`email` or `name` input is empty. Not creating a contact.')\n return\n}\n\nlet body := {\n 'user': {\n 'email': inputs.email,\n 'name': inputs.name,\n 'skip_verify_email': true,\n 'user_fields': {}\n }\n}\n\nfor (let key, value in inputs.attributes) {\n if (not empty(value) and key != 'email' and key != 'name') {\n body.user.user_fields[key] := value\n }\n}\n\nfetch(f'https://{inputs.subdomain}.zendesk.com/api/v2/users/create_or_update', {\n 'headers': {\n 'Authorization': f'Basic {base64Encode(f'{inputs.admin_email}/token:{inputs.token}')}',\n 'Content-Type': 'application/json'\n },\n 'body': body,\n 'method': 'POST'\n});", + "inputs_schema": [ + { + "key": "subdomain", + "type": "string", + "label": "Zendesk subdomain", + "description": "Generally, Your Zendesk URL has two parts: a subdomain name you chose when you set up your account, followed by zendesk.com (for example: mycompany.zendesk.com). Please share the subdomain name with us so we can set up your account.", + "secret": false, + "required": true + }, + { + "key": "admin_email", + "type": "string", + "label": "API user email", + "secret": true, + "required": true, + "description": "Enter the email of an admin in Zendesk. Activity using the API key will be attributed to this user." + }, + { + "key": "token", + "type": "string", + "label": "API token", + "secret": true, + "required": true, + "hint": "Enter your Zendesk API Token" + }, + { + "key": "email", + "type": "string", + "label": "User email", + "default": "{person.properties.email}", + "secret": false, + "required": true, + "hint": "The email of the user you want to create or update." + }, + { + "key": "name", + "type": "string", + "label": "User name", + "default": "{person.properties.name}", + "secret": false, + "required": true, + "hint": "The name of the user you want to create or update." + }, + { + "key": "attributes", + "type": "dictionary", + "label": "Attribute mapping", + "description": "Map of Zendesk user fields and their values. You'll need to create User fields in Zendesk for these to work.", + "default": { "phone": "{person.properties.phone}", "plan": "{person.properties.plan}" }, + "secret": false, + "required": false + } + ], + "category": ["Customer Success"], + "filters": { + "events": [ + { "id": "$identify", "name": "$identify", "type": "events", "order": 0 }, + { "id": "$set", "name": "$set", "type": "events", "order": 1 } + ], + "actions": [], + "filter_test_accounts": true + }, + "masking": null, + "icon_url": "/static/services/zendesk.png" + } + ] +} diff --git a/frontend/src/mocks/handlers.ts b/frontend/src/mocks/handlers.ts index 8303c7ac4cc15..3a09eec93f7ac 100644 --- a/frontend/src/mocks/handlers.ts +++ b/frontend/src/mocks/handlers.ts @@ -16,6 +16,7 @@ import { SharingConfigurationType } from '~/types' import { getAvailableProductFeatures } from './features' import { billingJson } from './fixtures/_billing' +import _hogFunctionTemplates from './fixtures/_hogFunctionTemplates.json' import * as statusPageAllOK from './fixtures/_status_page_all_ok.json' import { Mocks, MockSignature, mocksToHandlers } from './utils' @@ -27,6 +28,14 @@ export const toPaginatedResponse = (results: any[]): typeof EMPTY_PAGINATED_RESP previous: null, }) +const hogFunctionTemplateRetrieveMock: MockSignature = (req, res, ctx) => { + const hogFunctionTemplate = _hogFunctionTemplates.results.find((conf) => conf.id === req.params.id) + if (!_hogFunctionTemplates) { + return res(ctx.status(404)) + } + return res(ctx.json({ ...hogFunctionTemplate })) +} + // this really returns MaybePromise> // but MSW doesn't export MaybePromise 🤷 function posthogCORSResponse(req: RestRequest, res: ResponseComposition, ctx: RestContext): any { @@ -135,6 +144,9 @@ export const defaultMocks: Mocks = { eligible: false, }, 'https://status.posthog.com/api/v2/summary.json': statusPageAllOK, + '/api/projects/:team_id/hog_function_templates': _hogFunctionTemplates, + '/api/projects/:team_id/hog_function_templates/:id': hogFunctionTemplateRetrieveMock, + '/api/projects/:team_id/hog_functions': EMPTY_PAGINATED_RESPONSE, }, post: { 'https://us.i.posthog.com/e/': (req, res, ctx): MockSignature => posthogCORSResponse(req, res, ctx), diff --git a/frontend/src/scenes/actions/ActionEdit.tsx b/frontend/src/scenes/actions/ActionEdit.tsx index 0d6cad88c12d8..c79497c4242f5 100644 --- a/frontend/src/scenes/actions/ActionEdit.tsx +++ b/frontend/src/scenes/actions/ActionEdit.tsx @@ -6,7 +6,6 @@ import { router } from 'kea-router' import { EditableField } from 'lib/components/EditableField/EditableField' import { ObjectTags } from 'lib/components/ObjectTags/ObjectTags' import { PageHeader } from 'lib/components/PageHeader' -import { useFeatureFlag } from 'lib/hooks/useFeatureFlag' import { IconPlayCircle } from 'lib/lemon-ui/icons' import { LemonButton } from 'lib/lemon-ui/LemonButton' import { LemonField } from 'lib/lemon-ui/LemonField' @@ -35,8 +34,6 @@ export function ActionEdit({ action: loadedAction, id }: ActionEditLogicProps): const slackEnabled = currentTeam?.slack_incoming_webhook - const hogFunctionsEnabled = useFeatureFlag('HOG_FUNCTIONS') - const deleteButton = (): JSX.Element => ( - {!hogFunctionsEnabled || action.post_to_slack ? ( + {action.post_to_slack ? (

Webhook delivery

- {hogFunctionsEnabled && ( - <> - - LemonDialog.open({ - title: 'Upgrade webhook', - width: '30rem', - description: - 'This will create a new Destination in the upgraded system. The action will have its webhook disabled. There will be slight difference in the placeholder tags, so double check that everything works as expected.', - secondaryButton: { - type: 'secondary', - children: 'Cancel', - }, - primaryButton: { - type: 'primary', - onClick: () => migrateToHogFunction(), - children: 'Upgrade', - }, - }), - disabledReason: hasCohortFilters - ? 'Can not upgrade because action has a cohort filter.' - : migrationLoading - ? 'Loading...' - : actionChanged - ? 'Please save the action first' - : undefined, - }} - > - Action Webhooks have been replaced by the new and improved{' '} - Pipeline Destinations.{' '} - {!hasCohortFilters && !actionChanged ? 'Click to upgrade.' : ''} - - - )} + + LemonDialog.open({ + title: 'Upgrade webhook', + width: '30rem', + description: + 'This will create a new Destination in the upgraded system. The action will have its webhook disabled. There will be slight difference in the placeholder tags, so double check that everything works as expected.', + secondaryButton: { + type: 'secondary', + children: 'Cancel', + }, + primaryButton: { + type: 'primary', + onClick: () => migrateToHogFunction(), + children: 'Upgrade', + }, + }), + disabledReason: hasCohortFilters + ? 'Can not upgrade because action has a cohort filter.' + : migrationLoading + ? 'Loading...' + : actionChanged + ? 'Please save the action first' + : undefined, + }} + > + Action Webhooks have been replaced by the new and improved Pipeline Destinations.{' '} + {!hasCohortFilters && !actionChanged ? 'Click to upgrade.' : ''} + {({ value, onChange }) => ( diff --git a/frontend/src/scenes/actions/ActionHogFunctions.tsx b/frontend/src/scenes/actions/ActionHogFunctions.tsx index 5c916759260d9..056bdea24fd8e 100644 --- a/frontend/src/scenes/actions/ActionHogFunctions.tsx +++ b/frontend/src/scenes/actions/ActionHogFunctions.tsx @@ -1,6 +1,5 @@ import { LemonBanner } from '@posthog/lemon-ui' import { useValues } from 'kea' -import { useFeatureFlag } from 'lib/hooks/useFeatureFlag' import { actionEditLogic } from 'scenes/actions/actionEditLogic' import { actionLogic } from 'scenes/actions/actionLogic' import { LinkedHogFunctions } from 'scenes/pipeline/hogfunctions/list/LinkedHogFunctions' @@ -12,9 +11,7 @@ export function ActionHogFunctions(): JSX.Element | null { const { hasCohortFilters, actionChanged, showCohortDisablesFunctionsWarning } = useValues( actionEditLogic({ id: action?.id, action }) ) - const hogFunctionsEnabled = useFeatureFlag('HOG_FUNCTIONS') - - if (!action || !hogFunctionsEnabled) { + if (!action) { return null } diff --git a/frontend/src/scenes/data-management/definition/DefinitionView.tsx b/frontend/src/scenes/data-management/definition/DefinitionView.tsx index d13d77b55543e..57432ffd91a8e 100644 --- a/frontend/src/scenes/data-management/definition/DefinitionView.tsx +++ b/frontend/src/scenes/data-management/definition/DefinitionView.tsx @@ -7,7 +7,6 @@ import { ObjectTags } from 'lib/components/ObjectTags/ObjectTags' import { PageHeader } from 'lib/components/PageHeader' import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' import { UserActivityIndicator } from 'lib/components/UserActivityIndicator/UserActivityIndicator' -import { useFeatureFlag } from 'lib/hooks/useFeatureFlag' import { IconPlayCircle } from 'lib/lemon-ui/icons' import { LemonButton } from 'lib/lemon-ui/LemonButton' import { LemonDialog } from 'lib/lemon-ui/LemonDialog' @@ -37,7 +36,6 @@ export function DefinitionView(props: DefinitionLogicProps = {}): JSX.Element { const { definition, definitionLoading, definitionMissing, hasTaxonomyFeatures, singular, isEvent, isProperty } = useValues(logic) const { deleteDefinition } = useActions(logic) - const hogFunctionsEnabled = useFeatureFlag('HOG_FUNCTIONS') if (definitionLoading) { return @@ -200,24 +198,20 @@ export function DefinitionView(props: DefinitionLogicProps = {}): JSX.Element { <> - {hogFunctionsEnabled && ( - <> - -

Connected destinations

-

Get notified via Slack, webhooks or more whenever this event is captured.

+ +

Connected destinations

+

Get notified via Slack, webhooks or more whenever this event is captured.

- - - )} +

Matching events

This is the list of recent events that match this definition.

diff --git a/frontend/src/scenes/pipeline/Pipeline.stories.tsx b/frontend/src/scenes/pipeline/Pipeline.stories.tsx index 336daa50f8bdd..ce45d28639c7c 100644 --- a/frontend/src/scenes/pipeline/Pipeline.stories.tsx +++ b/frontend/src/scenes/pipeline/Pipeline.stories.tsx @@ -69,7 +69,7 @@ export default { '/api/projects/:team_id/pipeline_frontend_apps_configs/:id': pluginConfigRetrieveMock, '/api/organizations/:organization_id/pipeline_import_apps/': empty, '/api/projects/:team_id/pipeline_import_apps_configs/': empty, - + '/api/projects/:team_id/integrations/': empty, '/api/projects/:team_id/app_metrics/:plugin_config_id?date_from=-7d': require('./__mocks__/pluginMetrics.json'), '/api/projects/:team_id/app_metrics/:plugin_config_id/error_details?error_type=Error': require('./__mocks__/pluginErrorDetails.json'), }, @@ -219,6 +219,13 @@ export function PipelineNodeNewBigQueryWithoutPipelines(): JSX.Element { return } +export function PipelineNodeNewHogFunction(): JSX.Element { + useEffect(() => { + router.actions.push(urls.pipelineNodeNew(PipelineStage.Destination, 'hog-template-slack')) + }, []) + return +} + export function PipelineNodeEditConfiguration(): JSX.Element { useEffect(() => { router.actions.push( diff --git a/frontend/src/scenes/pipeline/PipelinePluginConfiguration.tsx b/frontend/src/scenes/pipeline/PipelinePluginConfiguration.tsx index 0042a95fafa98..0fc34660cfe2a 100644 --- a/frontend/src/scenes/pipeline/PipelinePluginConfiguration.tsx +++ b/frontend/src/scenes/pipeline/PipelinePluginConfiguration.tsx @@ -17,7 +17,6 @@ import { useActions, useValues } from 'kea' import { Form } from 'kea-forms' import { NotFound } from 'lib/components/NotFound' import { PageHeader } from 'lib/components/PageHeader' -import { useFeatureFlag } from 'lib/hooks/useFeatureFlag' import { LemonField } from 'lib/lemon-ui/LemonField' import { LemonMarkdown } from 'lib/lemon-ui/LemonMarkdown' import { CodeEditor } from 'lib/monaco/CodeEditor' @@ -56,8 +55,6 @@ export function PipelinePluginConfiguration({ } = useValues(logic) const { submitConfiguration, resetConfiguration, migrateToHogFunction } = useActions(logic) - const hasHogFunctions = useFeatureFlag('HOG_FUNCTIONS') - if (!stage) { return } @@ -139,7 +136,7 @@ export function PipelinePluginConfiguration({
- {hasHogFunctions && plugin?.hog_function_migration_available && ( + {plugin?.hog_function_migration_available && ( - - openSidePanel(SidePanelTab.FeaturePreviews), - children: 'Enable feature preview', - }} - > - We're excited to announce Destinations 3000 - the new version of our realtime destinations - that include a range of pre-built templates, native filtering, templating and even customizing the - code. - - -
{!hideSearch && ( ([ openFeedbackDialog: true, }), loaders({ - plugins: [ - {} as Record, - { - loadPlugins: async () => { - return loadPluginsFromUrl('api/organizations/@current/pipeline_destinations') - }, - }, - ], hogFunctionTemplates: [ {} as Record, { @@ -68,10 +59,7 @@ export const newDestinationsLogic = kea([ }), selectors(() => ({ - loading: [ - (s) => [s.pluginsLoading, s.hogFunctionTemplatesLoading], - (pluginsLoading, hogFunctionTemplatesLoading) => pluginsLoading || hogFunctionTemplatesLoading, - ], + loading: [(s) => [s.hogFunctionTemplatesLoading], (hogFunctionTemplatesLoading) => hogFunctionTemplatesLoading], batchExportServiceNames: [ (s) => [s.user, s.featureFlags], (user, featureFlags): BatchExportService['type'][] => { @@ -85,29 +73,14 @@ export const newDestinationsLogic = kea([ }, ], destinations: [ - (s) => [ - s.plugins, - s.hogFunctionTemplates, - s.batchExportServiceNames, - s.featureFlags, - router.selectors.hashParams, - ], - ( - plugins, - hogFunctionTemplates, - batchExportServiceNames, - featureFlags, - hashParams - ): NewDestinationItemType[] => { - const hogFunctionsEnabled = !!featureFlags[FEATURE_FLAGS.HOG_FUNCTIONS] - const hogTemplates = hogFunctionsEnabled ? Object.values(hogFunctionTemplates) : [] - + (s) => [s.hogFunctionTemplates, s.batchExportServiceNames, router.selectors.hashParams], + (hogFunctionTemplates, batchExportServiceNames, hashParams): NewDestinationItemType[] => { return [ - ...hogTemplates.map((hogFunction) => ({ + ...Object.values(hogFunctionTemplates).map((hogFunction) => ({ icon: , name: hogFunction.name, description: hogFunction.description, - backend: PipelineBackend.HogFunction, + backend: PipelineBackend.HogFunction as const, url: combineUrl( urls.pipelineNodeNew(PipelineStage.Destination, `hog-${hogFunction.id}`), {}, @@ -115,21 +88,11 @@ export const newDestinationsLogic = kea([ ).url, status: hogFunction.status, })), - ...Object.values(plugins) - .filter((x) => !hogFunctionsEnabled || !x.hog_function_migration_available) - .map((plugin) => ({ - icon: , - name: plugin.name, - description: plugin.description || '', - backend: PipelineBackend.Plugin, - url: urls.pipelineNodeNew(PipelineStage.Destination, `${plugin.id}`), - status: hogFunctionsEnabled ? ('deprecated' as const) : undefined, - })), ...batchExportServiceNames.map((service) => ({ icon: , name: humanizeBatchExportName(service), description: `${service} batch export`, - backend: PipelineBackend.BatchExport, + backend: PipelineBackend.BatchExport as const, url: urls.pipelineNodeNew(PipelineStage.Destination, `${service}`), })), ] @@ -169,7 +132,6 @@ export const newDestinationsLogic = kea([ })), afterMount(({ actions }) => { - actions.loadPlugins() actions.loadHogFunctionTemplates() }), ]) diff --git a/frontend/src/scenes/pipeline/hogfunctions/list/LinkedHogFunctions.tsx b/frontend/src/scenes/pipeline/hogfunctions/list/LinkedHogFunctions.tsx index 28de7b535e43f..7403e1f63776d 100644 --- a/frontend/src/scenes/pipeline/hogfunctions/list/LinkedHogFunctions.tsx +++ b/frontend/src/scenes/pipeline/hogfunctions/list/LinkedHogFunctions.tsx @@ -1,5 +1,4 @@ import { LemonButton } from '@posthog/lemon-ui' -import { useFeatureFlag } from 'lib/hooks/useFeatureFlag' import { useState } from 'react' import { HogFunctionFiltersType, HogFunctionSubTemplateIdType } from '~/types' @@ -18,13 +17,8 @@ export function LinkedHogFunctions({ subTemplateId, newDisabledReason, }: LinkedHogFunctionsProps): JSX.Element | null { - const hogFunctionsEnabled = useFeatureFlag('HOG_FUNCTIONS') const [showNewDestination, setShowNewDestination] = useState(false) - if (!hogFunctionsEnabled) { - return null - } - return showNewDestination ? (