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 index 770be28c1a0e5..1eaddf28cc8a7 100644 Binary files a/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-new-hog-function--light.png and b/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-new-hog-function--light.png differ diff --git a/frontend/public/transformations/geoip.png b/frontend/public/transformations/geoip.png new file mode 100644 index 0000000000000..45ab21b98ec71 Binary files /dev/null and b/frontend/public/transformations/geoip.png differ diff --git a/frontend/public/transformations/user-agent.png b/frontend/public/transformations/user-agent.png index 52941df428ae9..1a160d9ed28ec 100644 Binary files a/frontend/public/transformations/user-agent.png and b/frontend/public/transformations/user-agent.png differ diff --git a/frontend/src/scenes/pipeline/hogfunctions/HogFunctionConfiguration.tsx b/frontend/src/scenes/pipeline/hogfunctions/HogFunctionConfiguration.tsx index 86dce22800f2b..c1eec7364379f 100644 --- a/frontend/src/scenes/pipeline/hogfunctions/HogFunctionConfiguration.tsx +++ b/frontend/src/scenes/pipeline/hogfunctions/HogFunctionConfiguration.tsx @@ -100,7 +100,7 @@ export function HogFunctionConfiguration({ return } - const isLegacyPlugin = hogFunction?.template?.id?.startsWith('plugin-') + const isLegacyPlugin = (template?.id || hogFunction?.template?.id)?.startsWith('plugin-') const headerButtons = ( <> @@ -167,7 +167,7 @@ export function HogFunctionConfiguration({ const showFilters = displayOptions.showFilters ?? - ['destination', 'internal_destination', 'site_destination', 'broadcast', 'transformation'].includes(type) + ['destination', 'internal_destination', 'site_destination', 'broadcast'].includes(type) const showExpectedVolume = displayOptions.showExpectedVolume ?? ['destination', 'site_destination'].includes(type) const showStatus = displayOptions.showStatus ?? ['destination', 'internal_destination', 'email', 'transformation'].includes(type) @@ -266,8 +266,7 @@ export function HogFunctionConfiguration({ {isLegacyPlugin ? ( - This destination is one of our legacy plugins. It will be deprecated and you - should instead upgrade + This is part of our legacy plugins and will eventually be deprecated. ) : hogFunction?.template && !hogFunction.template.id.startsWith('template-blank-') ? ( ] = ...") [var-annotated] -posthog/temporal/tests/batch_exports/test_snowflake_batch_export_workflow.py:0: error: Need type annotation for "_execute_async_calls" (hint: "_execute_async_calls: list[] = ...") [var-annotated] -posthog/temporal/tests/batch_exports/test_snowflake_batch_export_workflow.py:0: error: Need type annotation for "_cursors" (hint: "_cursors: list[] = ...") [var-annotated] -posthog/temporal/tests/batch_exports/test_snowflake_batch_export_workflow.py:0: error: List item 0 has incompatible type "tuple[str, str, int, int, int, int, str, int]"; expected "tuple[str, str, int, int, str, str, str, str]" [list-item] +posthog/temporal/tests/batch_exports/test_run_updates.py:0: error: Unused "type: ignore" comment [unused-ignore] +posthog/temporal/tests/batch_exports/test_run_updates.py:0: error: Unused "type: ignore" comment [unused-ignore] +posthog/temporal/tests/batch_exports/test_run_updates.py:0: error: Unused "type: ignore" comment [unused-ignore] +posthog/temporal/tests/batch_exports/test_run_updates.py:0: error: Unused "type: ignore" comment [unused-ignore] +posthog/temporal/tests/batch_exports/test_batch_exports.py:0: error: TypedDict key must be a string literal; expected one of ("_timestamp", "created_at", "distinct_id", "elements", "elements_chain", ...) [literal-required] posthog/api/test/test_capture.py:0: error: Statement is unreachable [unreachable] posthog/api/test/test_capture.py:0: error: Incompatible return value type (got "_MonkeyPatchedWSGIResponse", expected "HttpResponse") [return-value] posthog/api/test/test_capture.py:0: error: Module has no attribute "utc" [attr-defined] @@ -803,6 +798,10 @@ posthog/api/test/test_capture.py:0: error: Dict entry 0 has incompatible type "s posthog/api/test/test_capture.py:0: error: Dict entry 0 has incompatible type "str": "float"; expected "str": "int" [dict-item] posthog/api/test/test_capture.py:0: error: Dict entry 0 has incompatible type "str": "float"; expected "str": "int" [dict-item] posthog/api/test/test_capture.py:0: error: Dict entry 0 has incompatible type "str": "float"; expected "str": "int" [dict-item] +posthog/temporal/tests/batch_exports/test_snowflake_batch_export_workflow.py:0: error: Need type annotation for "_execute_calls" (hint: "_execute_calls: list[] = ...") [var-annotated] +posthog/temporal/tests/batch_exports/test_snowflake_batch_export_workflow.py:0: error: Need type annotation for "_execute_async_calls" (hint: "_execute_async_calls: list[] = ...") [var-annotated] +posthog/temporal/tests/batch_exports/test_snowflake_batch_export_workflow.py:0: error: Need type annotation for "_cursors" (hint: "_cursors: list[] = ...") [var-annotated] +posthog/temporal/tests/batch_exports/test_snowflake_batch_export_workflow.py:0: error: List item 0 has incompatible type "tuple[str, str, int, int, int, int, str, int]"; expected "tuple[str, str, int, int, str, str, str, str]" [list-item] posthog/temporal/tests/batch_exports/test_redshift_batch_export_workflow.py:0: error: Incompatible types in assignment (expression has type "str | int", variable has type "int") [assignment] posthog/api/test/batch_exports/conftest.py:0: error: Signature of "run" incompatible with supertype "Worker" [override] posthog/api/test/batch_exports/conftest.py:0: note: Superclass: diff --git a/plugin-server/src/cdp/cdp-api.ts b/plugin-server/src/cdp/cdp-api.ts index 859f1f898d2e9..a2c4839f1e946 100644 --- a/plugin-server/src/cdp/cdp-api.ts +++ b/plugin-server/src/cdp/cdp-api.ts @@ -8,6 +8,7 @@ import { FetchExecutorService } from './services/fetch-executor.service' import { HogExecutorService, MAX_ASYNC_STEPS } from './services/hog-executor.service' import { HogFunctionManagerService } from './services/hog-function-manager.service' import { HogWatcherService, HogWatcherState } from './services/hog-watcher.service' +import { HOG_FUNCTION_TEMPLATES } from './templates' import { HogFunctionInvocationResult, HogFunctionQueueParametersFetchRequest, HogFunctionType, LogEntry } from './types' export class CdpApi { @@ -42,10 +43,15 @@ export class CdpApi { router.post('/api/projects/:team_id/hog_functions/:id/invocations', asyncHandler(this.postFunctionInvocation)) router.get('/api/projects/:team_id/hog_functions/:id/status', asyncHandler(this.getFunctionStatus())) router.patch('/api/projects/:team_id/hog_functions/:id/status', asyncHandler(this.patchFunctionStatus())) + router.get('/api/hog_function_templates', this.getHogFunctionTemplates) return router } + private getHogFunctionTemplates = (req: express.Request, res: express.Response): void => { + res.json(HOG_FUNCTION_TEMPLATES) + } + private getFunctionStatus = () => async (req: express.Request, res: express.Response): Promise => { diff --git a/plugin-server/src/cdp/hog-transformations/hog-transformer.service.test.ts b/plugin-server/src/cdp/hog-transformations/hog-transformer.service.test.ts index 0c0cac890de60..9baa2930fcb9e 100644 --- a/plugin-server/src/cdp/hog-transformations/hog-transformer.service.test.ts +++ b/plugin-server/src/cdp/hog-transformations/hog-transformer.service.test.ts @@ -57,10 +57,12 @@ describe('HogTransformer', () => { hub.mmdb = Reader.openBuffer(brotliDecompressSync(mmdbBrotliContents)) hogTransformer = new HogTransformerService(hub) + await hogTransformer.start() }) afterEach(async () => { await closeHub(hub) + await hogTransformer.stop() jest.spyOn(hogTransformer['pluginExecutor'], 'execute') }) @@ -81,7 +83,7 @@ describe('HogTransformer', () => { // Start the transformer after inserting functions because it is // starting the hogfunction manager which updates the cache - await hogTransformer.start() + await hogTransformer['hogFunctionManager'].reloadAllHogFunctions() const event: PluginEvent = createPluginEvent({}, teamId) const result = await hogTransformer.transformEvent(event) @@ -188,7 +190,7 @@ describe('HogTransformer', () => { await insertHogFunction(hub.db.postgres, teamId, defaultTransformationFunction) await insertHogFunction(hub.db.postgres, teamId, geoIpTransformationFunction) - await hogTransformer.start() + await hogTransformer['hogFunctionManager'].reloadAllHogFunctions() const createHogFunctionInvocationSpy = jest.spyOn(hogTransformer as any, 'createHogFunctionInvocation') @@ -267,7 +269,7 @@ describe('HogTransformer', () => { await insertHogFunction(hub.db.postgres, teamId, deletingTransformationFunction) await insertHogFunction(hub.db.postgres, teamId, addingTransformationFunction) - await hogTransformer.start() + await hogTransformer['hogFunctionManager'].reloadAllHogFunctions() const createHogFunctionInvocationSpy = jest.spyOn(hogTransformer as any, 'createHogFunctionInvocation') @@ -366,7 +368,7 @@ describe('HogTransformer', () => { await insertHogFunction(hub.db.postgres, teamId, thirdTransformationFunction) await insertHogFunction(hub.db.postgres, teamId, secondTransformationFunction) await insertHogFunction(hub.db.postgres, teamId, firstTransformationFunction) - await hogTransformer.start() + await hogTransformer['hogFunctionManager'].reloadAllHogFunctions() const createHogFunctionInvocationSpy = jest.spyOn(hogTransformer as any, 'createHogFunctionInvocation') @@ -410,7 +412,7 @@ describe('HogTransformer', () => { }) await insertHogFunction(hub.db.postgres, teamId, filterOutPlugin) - await hogTransformer.start() + await hogTransformer['hogFunctionManager'].reloadAllHogFunctions() // Set up the spy after hogTransformer is initialized executeSpy = jest.spyOn(hogTransformer['pluginExecutor'], 'execute') diff --git a/plugin-server/src/cdp/hog-transformations/hog-transformer.service.ts b/plugin-server/src/cdp/hog-transformations/hog-transformer.service.ts index 420f0af0d2cf2..8b8c5bedb5296 100644 --- a/plugin-server/src/cdp/hog-transformations/hog-transformer.service.ts +++ b/plugin-server/src/cdp/hog-transformations/hog-transformer.service.ts @@ -94,6 +94,10 @@ export class HogTransformerService { await this.hogFunctionManager.start(hogTypes) } + public async stop(): Promise { + await this.hogFunctionManager.stop() + } + private produceAppMetric(metric: HogFunctionAppMetric): Promise { const appMetric: AppMetric2Type = { app_source: 'hog_function', diff --git a/plugin-server/src/cdp/legacy-plugins/_transformations/language-url-splitter-app/template.ts b/plugin-server/src/cdp/legacy-plugins/_transformations/language-url-splitter-app/template.ts index ab786cf78c13d..26c423e6ff281 100644 --- a/plugin-server/src/cdp/legacy-plugins/_transformations/language-url-splitter-app/template.ts +++ b/plugin-server/src/cdp/legacy-plugins/_transformations/language-url-splitter-app/template.ts @@ -6,7 +6,7 @@ export const template: HogFunctionTemplate = { id: 'plugin-language-url-splitter-app', name: 'Language URL splitter', description: 'Splits the language from the URL', - icon_url: 'https://raw.githubusercontent.com/posthog/language-url-splitter-app/main/logo.png', + icon_url: '/static/hedgehog/builder-hog-01.png', category: ['Transformation'], hog: `return event`, inputs_schema: [ diff --git a/plugin-server/src/cdp/legacy-plugins/_transformations/property-filter-plugin/template.ts b/plugin-server/src/cdp/legacy-plugins/_transformations/property-filter-plugin/template.ts index 4f5d7d64754fa..4ed18296873a2 100644 --- a/plugin-server/src/cdp/legacy-plugins/_transformations/property-filter-plugin/template.ts +++ b/plugin-server/src/cdp/legacy-plugins/_transformations/property-filter-plugin/template.ts @@ -6,7 +6,7 @@ export const template: HogFunctionTemplate = { id: 'plugin-property-filter-plugin', name: 'Property Filter', description: 'This plugin will set all configured properties to null inside an ingested event.', - icon_url: 'https://raw.githubusercontent.com/posthog/posthog-property-filter-plugin/main/logo.png', + icon_url: 'https://raw.githubusercontent.com/posthog/property-filter-plugin/dev/logo.png', category: ['Transformation'], hog: `return event`, inputs_schema: [ diff --git a/plugin-server/src/cdp/legacy-plugins/_transformations/semver-flattener-plugin/template.ts b/plugin-server/src/cdp/legacy-plugins/_transformations/semver-flattener-plugin/template.ts index 1b6acc7cac708..b90fedceb287d 100644 --- a/plugin-server/src/cdp/legacy-plugins/_transformations/semver-flattener-plugin/template.ts +++ b/plugin-server/src/cdp/legacy-plugins/_transformations/semver-flattener-plugin/template.ts @@ -6,7 +6,7 @@ export const template: HogFunctionTemplate = { id: 'plugin-semver-flattener-plugin', name: 'SemVer Flattener', description: 'This plugin will flatten semver versions in the specified properties.', - icon_url: 'https://raw.githubusercontent.com/posthog/posthog-semver-flattener-plugin/main/logo.png', + icon_url: '/static/transformations/semver-flattener.png', category: ['Transformation'], hog: `return event`, inputs_schema: [ diff --git a/plugin-server/src/cdp/legacy-plugins/_transformations/user-agent-plugin/template.ts b/plugin-server/src/cdp/legacy-plugins/_transformations/user-agent-plugin/template.ts index 313c7d85afa59..cb177efbc3384 100644 --- a/plugin-server/src/cdp/legacy-plugins/_transformations/user-agent-plugin/template.ts +++ b/plugin-server/src/cdp/legacy-plugins/_transformations/user-agent-plugin/template.ts @@ -7,7 +7,7 @@ export const template: HogFunctionTemplate = { name: 'User Agent Populator', description: "Enhances events with user agent details. User Agent plugin allows you to populate events with the $browser, $browser_version for PostHog Clients that don't typically populate these properties", - icon_url: 'https://raw.githubusercontent.com/posthog/useragent-plugin/main/logo.png', + icon_url: '/static/transformations/user-agent.png', category: ['Transformation'], hog: `return event`, inputs_schema: [ diff --git a/plugin-server/src/cdp/templates/_transformations/geoip/geoip.template.ts b/plugin-server/src/cdp/templates/_transformations/geoip/geoip.template.ts index 4f95ceef54413..cc11f03b89575 100644 --- a/plugin-server/src/cdp/templates/_transformations/geoip/geoip.template.ts +++ b/plugin-server/src/cdp/templates/_transformations/geoip/geoip.template.ts @@ -6,7 +6,7 @@ export const template: HogFunctionTemplate = { id: 'template-geoip', name: 'GeoIP', description: 'Adds geoip data to the event', - icon_url: '/static/hedgehog/builder-hog-01.png', + icon_url: '/static/transformations/geoip.png', category: ['Custom'], hog: ` // Define the properties to be added to the event diff --git a/plugin-server/src/cdp/templates/index.ts b/plugin-server/src/cdp/templates/index.ts index e69de29bb2d1d..9ae554aaac68e 100644 --- a/plugin-server/src/cdp/templates/index.ts +++ b/plugin-server/src/cdp/templates/index.ts @@ -0,0 +1,36 @@ +import { template as downsamplingPlugin } from '../legacy-plugins/_transformations/downsampling-plugin/template' +import { template as languageUrlSplitterTemplate } from '../legacy-plugins/_transformations/language-url-splitter-app/template' +import { template as posthogAppUrlParametersToEventPropertiesTemplate } from '../legacy-plugins/_transformations/posthog-app-url-parameters-to-event-properties/template' +import { template as posthogFilterOutTemplate } from '../legacy-plugins/_transformations/posthog-filter-out-plugin/template' +import { template as posthogUrlNormalizerTemplate } from '../legacy-plugins/_transformations/posthog-url-normalizer-plugin/template' +import { template as propertyFilterTemplate } from '../legacy-plugins/_transformations/property-filter-plugin/template' +import { template as semverFlattenerTemplate } from '../legacy-plugins/_transformations/semver-flattener-plugin/template' +import { template as taxonomyTemplate } from '../legacy-plugins/_transformations/taxonomy-plugin/template' +import { template as timestampParserTemplate } from '../legacy-plugins/_transformations/timestamp-parser-plugin/template' +import { template as userAgentTemplate } from '../legacy-plugins/_transformations/user-agent-plugin/template' +import { template as webhookTemplate } from './_destinations/webhook/webhook.template' +import { template as defaultTransformationTemplate } from './_transformations/default/default.template' +import { template as geoipTemplate } from './_transformations/geoip/geoip.template' +import { HogFunctionTemplate } from './types' + +export const HOG_FUNCTION_TEMPLATES_DESTINATIONS: HogFunctionTemplate[] = [webhookTemplate] + +export const HOG_FUNCTION_TEMPLATES_TRANSFORMATIONS: HogFunctionTemplate[] = [ + defaultTransformationTemplate, + geoipTemplate, + downsamplingPlugin, + languageUrlSplitterTemplate, + posthogAppUrlParametersToEventPropertiesTemplate, + posthogFilterOutTemplate, + posthogUrlNormalizerTemplate, + propertyFilterTemplate, + semverFlattenerTemplate, + taxonomyTemplate, + timestampParserTemplate, + userAgentTemplate, +] + +export const HOG_FUNCTION_TEMPLATES: HogFunctionTemplate[] = [ + ...HOG_FUNCTION_TEMPLATES_DESTINATIONS, + ...HOG_FUNCTION_TEMPLATES_TRANSFORMATIONS, +] diff --git a/plugin-server/src/ingestion/ingestion-consumer.test.ts b/plugin-server/src/ingestion/ingestion-consumer.test.ts index 4de471713e0ac..abc16201f89e4 100644 --- a/plugin-server/src/ingestion/ingestion-consumer.test.ts +++ b/plugin-server/src/ingestion/ingestion-consumer.test.ts @@ -323,6 +323,11 @@ describe('IngestionConsumer', () => { }) describe('event batching', () => { + beforeEach(async () => { + ingester = new IngestionConsumer(hub) + await ingester.start() + }) + it('should batch events based on the distinct_id', async () => { const messages = createKafkaMessages([ createEvent({ distinct_id: 'distinct-id-1' }), diff --git a/plugin-server/src/ingestion/ingestion-consumer.ts b/plugin-server/src/ingestion/ingestion-consumer.ts index f7d3ae81523a6..0fbf01db1d171 100644 --- a/plugin-server/src/ingestion/ingestion-consumer.ts +++ b/plugin-server/src/ingestion/ingestion-consumer.ts @@ -121,7 +121,8 @@ export class IngestionConsumer { await this.batchConsumer?.stop() status.info('🔁', `${this.name} - stopping kafka producer`) await this.kafkaProducer?.disconnect() - + status.info('🔁', `${this.name} - stopping hog transformer`) + await this.hogTransformer.stop() status.info('👍', `${this.name} - stopped!`) } diff --git a/posthog/api/hog_function.py b/posthog/api/hog_function.py index 30648a2164b04..c7e3ca2ca54a2 100644 --- a/posthog/api/hog_function.py +++ b/posthog/api/hog_function.py @@ -13,14 +13,13 @@ from posthog.api.app_metrics2 import AppMetricsMixin from posthog.api.forbid_destroy_model import ForbidDestroyModel -from posthog.api.hog_function_template import HogFunctionTemplateSerializer +from posthog.api.hog_function_template import HogFunctionTemplateSerializer, HogFunctionTemplates from posthog.api.log_entries import LogEntryMixin from posthog.api.routing import TeamAndOrgViewSetMixin from posthog.api.shared import UserBasicSerializer from posthog.cdp.filters import compile_filters_bytecode, compile_filters_expr from posthog.cdp.services.icons import CDPIconsService -from posthog.cdp.templates import HOG_FUNCTION_TEMPLATES_BY_ID from posthog.cdp.templates._internal.template_legacy_plugin import create_legacy_plugin_template from posthog.cdp.validation import compile_hog, generate_template_bytecode, validate_inputs, validate_inputs_schema from posthog.cdp.site_functions import get_transpiled_function @@ -156,7 +155,7 @@ def validate(self, attrs): is_create = self.context.get("view") and self.context["view"].action == "create" template_id = attrs.get("template_id", instance.template_id if instance else None) - template = HOG_FUNCTION_TEMPLATES_BY_ID.get(template_id, None) + template = HogFunctionTemplates.template(template_id) if template_id else None if template_id and template_id.startswith("plugin-"): template = create_legacy_plugin_template(template_id) diff --git a/posthog/api/hog_function_template.py b/posthog/api/hog_function_template.py index a8a24145a02cf..5015b5731dc86 100644 --- a/posthog/api/hog_function_template.py +++ b/posthog/api/hog_function_template.py @@ -1,3 +1,5 @@ +from datetime import datetime, timedelta +from posthoganalytics import capture_exception import structlog from django_filters.rest_framework import DjangoFilterBackend from rest_framework import viewsets, permissions @@ -5,13 +7,15 @@ from rest_framework.response import Response from rest_framework.exceptions import NotFound -from posthog.cdp.templates import HOG_FUNCTION_SUB_TEMPLATES, HOG_FUNCTION_TEMPLATES, ALL_HOG_FUNCTION_TEMPLATES_BY_ID +from posthog.cdp.templates import HOG_FUNCTION_TEMPLATES from posthog.cdp.templates.hog_function_template import ( HogFunctionMapping, HogFunctionMappingTemplate, HogFunctionTemplate, HogFunctionSubTemplate, + derive_sub_templates, ) +from posthog.plugins.plugin_server_api import get_hog_function_templates from rest_framework_dataclasses.serializers import DataclassSerializer @@ -42,6 +46,77 @@ class Meta: dataclass = HogFunctionTemplate +class HogFunctionTemplates: + _cache_until: datetime | None = None + _cached_templates: list[HogFunctionTemplate] = [] + _cached_templates_by_id: dict[str, HogFunctionTemplate] = {} + _cached_sub_templates: list[HogFunctionTemplate] = [] + _cached_sub_templates_by_id: dict[str, HogFunctionTemplate] = {} + + @classmethod + def templates(cls): + cls._load_templates() + return cls._cached_templates + + @classmethod + def sub_templates(cls): + cls._load_templates() + return cls._cached_sub_templates + + @classmethod + def template(cls, template_id: str): + cls._load_templates() + return cls._cached_templates_by_id.get(template_id, cls._cached_sub_templates_by_id.get(template_id)) + + @classmethod + def _load_templates(cls): + if cls._cache_until and datetime.now() < cls._cache_until: + return + + # First we load and convert all nodejs templates to python templates + nodejs_templates: list[HogFunctionTemplate] = [] + + try: + response = get_hog_function_templates() + + if response.status_code != 200: + raise Exception("Failed to fetch hog function templates from the node service") + + nodejs_templates_json = response.json() + for template_data in nodejs_templates_json: + try: + serializer = HogFunctionTemplateSerializer(data=template_data) + serializer.is_valid(raise_exception=True) + template = serializer.save() + nodejs_templates.append(template) + except Exception as e: + logger.error( + "Failed to convert template", + template_id=template_data.get("id"), + error=str(e), + exc_info=True, + ) + capture_exception(e) + raise + except Exception as e: + capture_exception(e) + # Continue on so as not to block the user + + templates = [ + *HOG_FUNCTION_TEMPLATES, + *nodejs_templates, + ] + sub_templates = derive_sub_templates(templates=templates) + + # If we failed to get the templates, we cache for 30 seconds to avoid hammering the node service + # If we got the templates, we cache for 5 minutes as these change infrequently + cls._cache_until = datetime.now() + timedelta(seconds=30 if not nodejs_templates else 300) + cls._cached_templates = templates + cls._cached_sub_templates = sub_templates + cls._cached_templates_by_id = {template.id: template for template in templates} + cls._cached_sub_templates_by_id = {template.id: template for template in sub_templates} + + # NOTE: There is nothing currently private about these values class PublicHogFunctionTemplateViewSet(viewsets.GenericViewSet): filter_backends = [DjangoFilterBackend] @@ -59,7 +134,7 @@ def list(self, request: Request, *args, **kwargs): elif "types" in request.GET: types = self.request.GET.get("types", "destination").split(",") - templates_list = HOG_FUNCTION_SUB_TEMPLATES if sub_template_id else HOG_FUNCTION_TEMPLATES + templates_list = HogFunctionTemplates.sub_templates() if sub_template_id else HogFunctionTemplates.templates() matching_templates = [] @@ -81,7 +156,7 @@ def list(self, request: Request, *args, **kwargs): return self.get_paginated_response(serializer.data) def retrieve(self, request: Request, *args, **kwargs): - item = ALL_HOG_FUNCTION_TEMPLATES_BY_ID.get(kwargs["pk"], None) + item = HogFunctionTemplates.template(kwargs["pk"]) if not item: raise NotFound(f"Template with id {kwargs['pk']} not found.") diff --git a/posthog/api/test/__data__/hog_function_templates.json b/posthog/api/test/__data__/hog_function_templates.json new file mode 100644 index 0000000000000..ad0dc299dddef --- /dev/null +++ b/posthog/api/test/__data__/hog_function_templates.json @@ -0,0 +1,547 @@ +[ + { + "status": "beta", + "type": "destination", + "id": "template-webhook", + "name": "HTTP Webhook", + "description": "Sends a webhook templated by the incoming event data", + "icon_url": "/static/posthog-icon.svg", + "category": ["Custom"], + "hog": "\nlet payload := {\n 'headers': inputs.headers,\n 'body': inputs.body,\n 'method': inputs.method\n}\n\nif (inputs.debug) {\n print('Request', inputs.url, payload)\n}\n\nlet res := fetch(inputs.url, payload);\n\nif (inputs.debug) {\n print('Response', res.status, res.body);\n}\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, + "default": { + "Content-Type": "application/json" + } + }, + { + "key": "debug", + "type": "boolean", + "label": "Log responses", + "description": "Logs the response of http calls for debugging.", + "secret": false, + "required": false, + "default": false + } + ], + "sub_templates": [ + { + "id": "early-access-feature-enrollment", + "name": "HTTP Webhook on feature enrollment", + "filters": { + "events": [ + { + "id": "$feature_enrollment_update", + "type": "events" + } + ] + } + }, + { + "id": "survey-response", + "name": "HTTP Webhook on survey response", + "filters": { + "events": [ + { + "id": "survey sent", + "type": "events", + "properties": [ + { + "key": "$survey_response", + "type": "event", + "value": "is_set", + "operator": "is_set" + } + ] + } + ] + } + }, + { + "id": "activity-log", + "name": "HTTP Webhook on team activity", + "filters": { + "events": [ + { + "id": "$activity_log_entry_created", + "type": "events" + } + ] + }, + "type": "internal_destination" + } + ] + }, + { + "status": "alpha", + "type": "transformation", + "id": "template-blank-transformation", + "name": "Custom transformation", + "description": "This is a starter template for custom transformations", + "icon_url": "/static/hedgehog/builder-hog-01.png", + "category": ["Custom"], + "hog": "\n// This is a blank template for custom transformations\n// The function receives 'event' as a global object and expects it to be returned\n// If you return null then the event will be discarded\nreturn event\n ", + "inputs_schema": [] + }, + { + "status": "beta", + "type": "transformation", + "id": "template-geoip", + "name": "GeoIP", + "description": "Adds geoip data to the event", + "icon_url": "/static/hedgehog/builder-hog-01.png", + "category": ["Custom"], + "hog": "\n// Define the properties to be added to the event\nlet geoipProperties := {\n 'city_name': null,\n 'city_confidence': null,\n 'subdivision_2_name': null,\n 'subdivision_2_code': null,\n 'subdivision_1_name': null,\n 'subdivision_1_code': null,\n 'country_name': null,\n 'country_code': null,\n 'continent_name': null,\n 'continent_code': null,\n 'postal_code': null,\n 'latitude': null,\n 'longitude': null,\n 'accuracy_radius': null,\n 'time_zone': null\n}\n// Check if the event has an IP address l\nif (event.properties?.$geoip_disable or empty(event.properties?.$ip)) {\n print('geoip disabled or no ip', event.properties, event.properties?.$ip)\n return event\n}\nlet ip := event.properties.$ip\nif (ip == '127.0.0.1') {\n print('spoofing ip for local development', ip)\n ip := '89.160.20.129'\n}\nlet response := geoipLookup(ip)\nif (not response) {\n print('geoip lookup failed for ip', ip)\n return event\n}\nlet location := {}\nif (response.city) {\n location['city_name'] := response.city.names?.en\n}\nif (response.country) {\n location['country_name'] := response.country.names?.en\n location['country_code'] := response.country.isoCode\n}\nif (response.continent) {\n location['continent_name'] := response.continent.names?.en\n location['continent_code'] := response.continent.code\n}\nif (response.postal) {\n location['postal_code'] := response.postal.code\n}\nif (response.location) {\n location['latitude'] := response.location?.latitude\n location['longitude'] := response.location?.longitude\n location['accuracy_radius'] := response.location?.accuracyRadius\n location['time_zone'] := response.location?.timeZone\n}\nif (response.subdivisions) {\n for (let index, subdivision in response.subdivisions) {\n location[f'subdivision_{index + 1}_code'] := subdivision.isoCode\n location[f'subdivision_{index + 1}_name'] := subdivision.names?.en\n }\n}\nprint('geoip location data for ip:', location) \nlet returnEvent := event\nreturnEvent.properties := returnEvent.properties ?? {}\nreturnEvent.properties.$set := returnEvent.properties.$set ?? {}\nreturnEvent.properties.$set_once := returnEvent.properties.$set_once ?? {}\nfor (let key, value in geoipProperties) {\n if (value != null) {\n returnEvent.properties.$set[f'$geoip_{key}'] := value\n returnEvent.properties.$set_once[f'$initial_geoip_{key}'] := value\n }\n returnEvent.properties.$set[f'$geoip_{key}'] := value\n returnEvent.properties.$set_once[f'$initial_geoip_{key}'] := value\n}\nfor (let key, value in location) {\n returnEvent.properties[f'$geoip_{key}'] := value\n returnEvent.properties.$set[f'$geoip_{key}'] := value\n returnEvent.properties.$set_once[f'$initial_geoip_{key}'] := value\n}\nreturn returnEvent\n ", + "inputs_schema": [] + }, + { + "status": "alpha", + "type": "transformation", + "id": "plugin-downsampling-plugin", + "name": "Downsample", + "description": "Reduces event volume coming into PostHog", + "icon_url": "https://raw.githubusercontent.com/posthog/downsampling-plugin/main/logo.png", + "category": ["Custom"], + "hog": "return event", + "inputs_schema": [ + { + "type": "string", + "key": "percentage", + "label": "% of events to keep", + "default": "100", + "required": false + }, + { + "type": "choice", + "key": "samplingMethod", + "label": "Sampling method", + "choices": [ + { + "value": "Random sampling", + "label": "Random sampling" + }, + { + "value": "Distinct ID aware sampling", + "label": "Distinct ID aware sampling" + } + ], + "default": "Distinct ID aware sampling", + "required": false + } + ] + }, + { + "status": "alpha", + "type": "transformation", + "id": "plugin-language-url-splitter-app", + "name": "Language URL splitter", + "description": "Splits the language from the URL", + "icon_url": "https://raw.githubusercontent.com/posthog/language-url-splitter-app/main/logo.png", + "category": ["Transformation"], + "hog": "return event", + "inputs_schema": [ + { + "key": "pattern", + "label": "Pattern", + "type": "string", + "default": "^/([a-z]{2})(?=/|#|\\?|$)", + "description": "Initialized with `const regexp = new RegExp($pattern)`", + "required": true + }, + { + "key": "matchGroup", + "label": "Match group", + "type": "string", + "default": "1", + "description": "Used in: `const value = regexp.match($pathname)[$matchGroup]`", + "required": true + }, + { + "key": "property", + "label": "Property", + "type": "string", + "default": "locale", + "description": "Name of the event property we will store the matched value in", + "required": true + }, + { + "key": "replacePattern", + "label": "Replacement pattern", + "type": "string", + "default": "^(/[a-z]{2})(/|(?=/|#|\\?|$))", + "description": "Initialized with `new RegExp($pattern)`, leave empty to disable path cleanup.", + "required": true + }, + { + "key": "replaceKey", + "label": "Replacement key", + "type": "string", + "default": "$pathname", + "description": "Where to store the updated path. Keep as `$pathname` to override.", + "required": true + }, + { + "key": "replaceValue", + "label": "Replacement value", + "type": "string", + "default": "/", + "description": "`properties[key] = $pathname.replace(pattern, value)`", + "required": true + } + ] + }, + { + "status": "alpha", + "type": "transformation", + "id": "plugin-posthog-app-url-parameters-to-event-properties", + "name": "URL parameters to event properties", + "description": "Converts URL query parameters to event properties", + "icon_url": "https://raw.githubusercontent.com/posthog/posthog-app-url-parameters-to-event-properties/main/logo.png", + "category": ["Transformation"], + "hog": "return event", + "inputs_schema": [ + { + "key": "parameters", + "label": "URL query parameters to convert", + "type": "string", + "default": "", + "description": "Comma separated list of URL query parameters to capture. Leaving this blank will capture nothing." + }, + { + "key": "prefix", + "label": "Prefix", + "type": "string", + "default": "", + "description": "Add a prefix to the property name e.g. set it to 'prefix_' to get followerId -\u003E prefix_followerId" + }, + { + "key": "suffix", + "label": "Suffix", + "type": "string", + "default": "", + "description": "Add a suffix to the property name e.g. set it to '_suffix' to get followerId -\u003E followerId_suffix" + }, + { + "key": "ignoreCase", + "label": "Ignore the case of URL parameters", + "type": "choice", + "choices": [ + { + "value": "true", + "label": "true" + }, + { + "value": "false", + "label": "false" + } + ], + "default": "false", + "description": "Ignores the case of parameters e.g. when set to true than followerId would match FollowerId, followerID, FoLlOwErId and similar" + }, + { + "key": "setAsUserProperties", + "label": "Add to user properties", + "type": "choice", + "choices": [ + { + "value": "true", + "label": "true" + }, + { + "value": "false", + "label": "false" + } + ], + "default": "false", + "description": "Additionally adds the property to the user properties" + }, + { + "key": "setAsInitialUserProperties", + "label": "Add to user initial properties", + "type": "choice", + "choices": [ + { + "value": "true", + "label": "true" + }, + { + "value": "false", + "label": "false" + } + ], + "default": "false", + "description": "Additionally adds the property to the user initial properties. This will add a prefix of 'initial_' before the already fully composed property e.g. initial_prefix_followerId_suffix" + }, + { + "key": "alwaysJson", + "label": "Always JSON stringify the property data", + "type": "choice", + "choices": [ + { + "value": "true", + "label": "true" + }, + { + "value": "false", + "label": "false" + } + ], + "default": "false", + "description": "If set, always store the resulting data as a JSON array. (Otherwise, single parameters get stored as-is, and multi-value parameters get stored as a JSON array.)" + } + ] + }, + { + "status": "alpha", + "type": "transformation", + "id": "plugin-posthog-filter-out-plugin", + "name": "Filter Out Plugin", + "description": "Filter out events where property values satisfy the given condition", + "icon_url": "https://raw.githubusercontent.com/posthog/posthog-filter-out-plugin/main/logo.png", + "category": ["Transformation"], + "hog": "return event", + "inputs_schema": [ + { + "key": "filters", + "label": "Filters to apply", + "type": "string", + "description": "A JSON file containing an array of filters to apply. See the README for more information.", + "required": false + }, + { + "key": "eventsToDrop", + "label": "Events to filter out", + "type": "string", + "description": "A comma-separated list of event names to filter out (e.g. $pageview,$autocapture)", + "required": false + }, + { + "key": "keepUndefinedProperties", + "label": "Keep event if any of the filtered properties are undefined?", + "type": "choice", + "choices": [ + { + "value": "Yes", + "label": "Yes" + }, + { + "value": "No", + "label": "No" + } + ], + "default": "No" + } + ] + }, + { + "status": "alpha", + "type": "transformation", + "id": "plugin-posthog-url-normalizer-plugin", + "name": "URL Normalizer", + "description": "Normalize the format of urls in your application allowing you to more easily compare them in insights.", + "icon_url": "https://raw.githubusercontent.com/posthog/posthog-url-normalizer-plugin/main/logo.png", + "category": ["Transformation"], + "hog": "return event", + "inputs_schema": [] + }, + { + "status": "alpha", + "type": "transformation", + "id": "plugin-property-filter-plugin", + "name": "Property Filter", + "description": "This plugin will set all configured properties to null inside an ingested event.", + "icon_url": "https://raw.githubusercontent.com/posthog/posthog-property-filter-plugin/main/logo.png", + "category": ["Transformation"], + "hog": "return event", + "inputs_schema": [ + { + "key": "properties", + "label": "Properties to filter out", + "type": "string", + "description": "A comma-separated list of properties to filter out (e.g. $ip, $current_url)", + "default": "", + "required": true + } + ] + }, + { + "status": "alpha", + "type": "transformation", + "id": "plugin-semver-flattener-plugin", + "name": "SemVer Flattener", + "description": "This plugin will flatten semver versions in the specified properties.", + "icon_url": "https://raw.githubusercontent.com/posthog/semver-flattener-plugin/main/logo.png", + "category": ["Transformation"], + "hog": "return event", + "inputs_schema": [ + { + "key": "properties", + "label": "comma separated properties to explode version number from", + "type": "string", + "description": "my_version_number,app_version", + "default": "", + "required": true + } + ] + }, + { + "status": "alpha", + "type": "transformation", + "id": "plugin-taxonomy-plugin", + "name": "Taxonomy", + "description": "Standardize your event names into a single pattern.", + "icon_url": "https://raw.githubusercontent.com/posthog/taxonomy-plugin/main/logo.png", + "category": ["Transformation"], + "hog": "return event", + "inputs_schema": [ + { + "key": "defaultNamingConvention", + "label": "Select your default naming pattern", + "type": "choice", + "choices": [ + { + "value": "camelCase", + "label": "camelCase" + }, + { + "value": "PascalCase", + "label": "PascalCase" + }, + { + "value": "snake_case", + "label": "snake_case" + }, + { + "value": "kebab-case", + "label": "kebab-case" + }, + { + "value": "spaces in between", + "label": "spaces in between" + } + ], + "default": "camelCase", + "required": true + } + ] + }, + { + "status": "alpha", + "type": "transformation", + "id": "plugin-timestamp-parser-plugin", + "name": "Timestamp Parser", + "description": "Parse your event timestamps into useful date properties.", + "icon_url": "https://raw.githubusercontent.com/posthog/timestamp-parser-plugin/main/logo.png", + "category": ["Transformation"], + "hog": "return event", + "inputs_schema": [] + }, + { + "status": "alpha", + "type": "transformation", + "id": "plugin-user-agent-plugin", + "name": "User Agent Populator", + "description": "Enhances events with user agent details. User Agent plugin allows you to populate events with the $browser, $browser_version for PostHog Clients that don't typically populate these properties", + "icon_url": "https://raw.githubusercontent.com/posthog/useragent-plugin/main/logo.png", + "category": ["Transformation"], + "hog": "return event", + "inputs_schema": [ + { + "key": "overrideUserAgentDetails", + "label": "Can override existing browser related properties of event?", + "type": "string", + "description": "If the ingested event already have $browser $browser_version properties in combination with $useragent the $browser, $browser_version properties will be re-populated with the value of $useragent", + "default": "false", + "required": false + }, + { + "key": "enableSegmentAnalyticsJs", + "label": "Automatically read segment_userAgent property, automatically sent by Segment via analytics.js?", + "type": "choice", + "description": "Segment's analytics.js library automatically sends a useragent property that Posthog sees as segment_userAgent. Enabling this causes this plugin to parse that property", + "choices": [ + { + "value": "false", + "label": "false" + }, + { + "value": "true", + "label": "true" + } + ], + "default": "false", + "required": false + }, + { + "key": "debugMode", + "type": "choice", + "description": "Enable debug mode to log when the plugin is unable to extract values from the user agent", + "choices": [ + { + "value": "false", + "label": "false" + }, + { + "value": "true", + "label": "true" + } + ], + "default": "false", + "required": false + } + ] + } +] diff --git a/posthog/api/test/test_hog_function.py b/posthog/api/test/test_hog_function.py index 790bb2fa641c4..9fa5da11b0764 100644 --- a/posthog/api/test/test_hog_function.py +++ b/posthog/api/test/test_hog_function.py @@ -8,6 +8,8 @@ from rest_framework import status from common.hogvm.python.operation import HOGQL_BYTECODE_VERSION +from posthog.api.test.test_hog_function_templates import MOCK_NODE_TEMPLATES +from posthog.api.hog_function_template import HogFunctionTemplates from posthog.constants import AvailableFeature from posthog.models.action.action import Action from posthog.models.hog_functions.hog_function import DEFAULT_STATE, HogFunction @@ -71,6 +73,13 @@ def get_db_field_value(field, model_id): class TestHogFunctionAPIWithoutAvailableFeature(ClickhouseTestMixin, APIBaseTest, QueryMatchingTest): + def setUp(self): + super().setUp() + with patch("posthog.api.hog_function_template.get_hog_function_templates") as mock_get_templates: + mock_get_templates.return_value.status_code = 200 + mock_get_templates.return_value.json.return_value = MOCK_NODE_TEMPLATES + HogFunctionTemplates._load_templates() # Cache templates to simplify tests + def _create_slack_function(self, data: Optional[dict] = None): payload = { "name": "Slack", @@ -173,6 +182,11 @@ def setUp(self): ] self.organization.save() + with patch("posthog.api.hog_function_template.get_hog_function_templates") as mock_get_templates: + mock_get_templates.return_value.status_code = 200 + mock_get_templates.return_value.json.return_value = MOCK_NODE_TEMPLATES + HogFunctionTemplates._load_templates() # Cache templates to simplify tests + def _get_function_activity( self, function_id: Optional[int] = None, diff --git a/posthog/api/test/test_hog_function_templates.py b/posthog/api/test/test_hog_function_templates.py index c24e8fb9ba37b..d0883e265b069 100644 --- a/posthog/api/test/test_hog_function_templates.py +++ b/posthog/api/test/test_hog_function_templates.py @@ -1,11 +1,18 @@ -from unittest.mock import ANY +import json +import os +from unittest.mock import ANY, patch from inline_snapshot import snapshot from rest_framework import status +from posthog.api.hog_function_template import HogFunctionTemplates from posthog.cdp.templates.hog_function_template import derive_sub_templates from posthog.test.base import APIBaseTest, ClickhouseTestMixin, QueryMatchingTest from posthog.cdp.templates.slack.template_slack import template +MOCK_NODE_TEMPLATES = json.loads( + open(os.path.join(os.path.dirname(__file__), "__data__/hog_function_templates.json")).read() +) + # NOTE: We check this as a sanity check given that this is a public API so we want to explicitly define what is exposed EXPECTED_FIRST_RESULT = { "sub_templates": ANY, @@ -42,6 +49,14 @@ def test_derive_sub_templates(self): class TestHogFunctionTemplates(ClickhouseTestMixin, APIBaseTest, QueryMatchingTest): + def setUp(self): + super().setUp() + + with patch("posthog.api.hog_function_template.get_hog_function_templates") as mock_get_templates: + mock_get_templates.return_value.status_code = 200 + mock_get_templates.return_value.json.return_value = MOCK_NODE_TEMPLATES + HogFunctionTemplates._load_templates() # Cache templates to simplify tests + def test_list_function_templates(self): response = self.client.get("/api/projects/@current/hog_function_templates/") diff --git a/posthog/cdp/templates/__init__.py b/posthog/cdp/templates/__init__.py index e1b732eab312b..ca81f8915585e 100644 --- a/posthog/cdp/templates/__init__.py +++ b/posthog/cdp/templates/__init__.py @@ -52,7 +52,6 @@ from ._internal.template_blank import blank_site_destination, blank_site_app from .snapchat_ads.template_snapchat_ads import template as snapchat_ads from .snapchat_ads.template_pixel import template_snapchat_pixel as snapchat_pixel -from ._transformations.template_pass_through import template as pass_through_transformation HOG_FUNCTION_TEMPLATES = [ @@ -108,18 +107,10 @@ hogdesk, notification_bar, pineapple_mode, - pass_through_transformation, debug_posthog, ] -# This is a list of sub templates that are generated by merging the subtemplate with it's template -HOG_FUNCTION_SUB_TEMPLATES = derive_sub_templates(HOG_FUNCTION_TEMPLATES) - -HOG_FUNCTION_TEMPLATES_BY_ID = {template.id: template for template in HOG_FUNCTION_TEMPLATES} -HOG_FUNCTION_SUB_TEMPLATES_BY_ID = {template.id: template for template in HOG_FUNCTION_SUB_TEMPLATES} -ALL_HOG_FUNCTION_TEMPLATES_BY_ID = {**HOG_FUNCTION_TEMPLATES_BY_ID, **HOG_FUNCTION_SUB_TEMPLATES_BY_ID} - HOG_FUNCTION_MIGRATORS = { TemplateCustomerioMigrator.plugin_url: TemplateCustomerioMigrator, TemplateIntercomMigrator.plugin_url: TemplateIntercomMigrator, @@ -133,5 +124,3 @@ TemplateLoopsMigrator.plugin_url: TemplateLoopsMigrator, TemplateAvoMigrator.plugin_url: TemplateAvoMigrator, } - -__all__ = ["HOG_FUNCTION_TEMPLATES", "HOG_FUNCTION_TEMPLATES_BY_ID", "ALL_HOG_FUNCTION_TEMPLATES_BY_ID"] diff --git a/posthog/cdp/templates/_transformations/template_pass_through.py b/posthog/cdp/templates/_transformations/template_pass_through.py deleted file mode 100644 index 5a4e88e003d31..0000000000000 --- a/posthog/cdp/templates/_transformations/template_pass_through.py +++ /dev/null @@ -1,18 +0,0 @@ -from posthog.cdp.templates.hog_function_template import HogFunctionTemplate - -template: HogFunctionTemplate = HogFunctionTemplate( - status="alpha", - type="transformation", - id="template-blank-transformation", - name="Custom transformation", - description="This is a starter template for custom transformations", - icon_url="/static/hedgehog/builder-hog-01.png", - category=["Custom"], - hog=""" -// This is a blank template for custom transformations -// The function receives `event` as a global object and expects it to be returned -// If you return null then the event will be discarded -return event -""".strip(), - inputs_schema=[], -) diff --git a/posthog/cdp/templates/hog_function_template.py b/posthog/cdp/templates/hog_function_template.py index f76deacc3d4e4..132b36fa3abb0 100644 --- a/posthog/cdp/templates/hog_function_template.py +++ b/posthog/cdp/templates/hog_function_template.py @@ -1,5 +1,5 @@ import dataclasses -from typing import Literal, Optional, get_args, TYPE_CHECKING +from typing import Literal, Optional, TYPE_CHECKING if TYPE_CHECKING: @@ -10,7 +10,6 @@ SubTemplateId = Literal["early-access-feature-enrollment", "survey-response", "activity-log"] -SUB_TEMPLATE_ID: tuple[SubTemplateId, ...] = get_args(SubTemplateId) HogFunctionTemplateType = Literal[ "destination", @@ -83,6 +82,11 @@ def migrate(cls, obj: PluginConfig) -> dict: def derive_sub_templates(templates: list[HogFunctionTemplate]) -> list[HogFunctionTemplate]: + """ + Given a list of templates, derive the sub templates from them. + Sub templates just override certain params of the parent template. + This allows the API to filter for templates based on a SubTemplateId such as ones designed for surveys. + """ sub_templates = [] for template in templates: for sub_template in template.sub_templates or []: diff --git a/posthog/models/hog_functions/hog_function.py b/posthog/models/hog_functions/hog_function.py index 18fcf7582b870..a4d0d515fedf4 100644 --- a/posthog/models/hog_functions/hog_function.py +++ b/posthog/models/hog_functions/hog_function.py @@ -96,12 +96,20 @@ class Meta: @property def template(self) -> Optional[HogFunctionTemplate]: - from posthog.cdp.templates import ALL_HOG_FUNCTION_TEMPLATES_BY_ID + from posthog.api.hog_function_template import HogFunctionTemplates - if self.template_id and self.template_id.startswith("plugin-"): + if not self.template_id: + return None + + template = HogFunctionTemplates.template(self.template_id) + + if template: + return template + + if self.template_id.startswith("plugin-"): return create_legacy_plugin_template(self.template_id) - return ALL_HOG_FUNCTION_TEMPLATES_BY_ID.get(self.template_id, None) + return None @property def filter_action_ids(self) -> list[int]: diff --git a/posthog/plugins/plugin_server_api.py b/posthog/plugins/plugin_server_api.py index ef6b312ba874c..40c322bcf70a3 100644 --- a/posthog/plugins/plugin_server_api.py +++ b/posthog/plugins/plugin_server_api.py @@ -90,3 +90,7 @@ def patch_hog_function_status(team_id: int, hog_function_id: UUIDT, state: int) CDP_FUNCTION_EXECUTOR_API_URL + f"/api/projects/{team_id}/hog_functions/{hog_function_id}/status", json={"state": state}, ) + + +def get_hog_function_templates() -> requests.Response: + return requests.get(CDP_FUNCTION_EXECUTOR_API_URL + f"/api/hog_function_templates")