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")