diff --git a/frontend/__snapshots__/scenes-other-billing--billing-with-credit-cta--light.png b/frontend/__snapshots__/scenes-other-billing--billing-with-credit-cta--light.png index 444c0bb0cdc24..8633f30a452d7 100644 Binary files a/frontend/__snapshots__/scenes-other-billing--billing-with-credit-cta--light.png and b/frontend/__snapshots__/scenes-other-billing--billing-with-credit-cta--light.png differ diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index f8adb51e539a5..d6760ce8d2abe 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -63,6 +63,7 @@ import { HogFunctionStatus, HogFunctionTemplateType, HogFunctionType, + HogFunctionTypeType, InsightModel, IntegrationType, ListOrganizationMembersParams, @@ -1744,7 +1745,10 @@ const api = { }, }, hogFunctions: { - async list(params?: { filters?: any }): Promise> { + async list(params?: { + filters?: any + type?: HogFunctionTypeType + }): Promise> { return await new ApiRequest().hogFunctions().withQueryString(params).get() }, async get(id: HogFunctionType['id']): Promise { @@ -1774,9 +1778,11 @@ const api = { ): Promise { return await new ApiRequest().hogFunction(id).withAction('metrics/totals').withQueryString(params).get() }, - - async listTemplates(): Promise> { - return await new ApiRequest().hogFunctionTemplates().get() + async listTemplates(type?: HogFunctionTypeType): Promise> { + return new ApiRequest() + .hogFunctionTemplates() + .withQueryString({ type: type ?? 'destination' }) + .get() }, async getTemplate(id: HogFunctionTemplateType['id']): Promise { return await new ApiRequest().hogFunctionTemplate(id).get() diff --git a/frontend/src/scenes/actions/ActionHogFunctions.tsx b/frontend/src/scenes/actions/ActionHogFunctions.tsx index 056bdea24fd8e..6128a2c1d664c 100644 --- a/frontend/src/scenes/actions/ActionHogFunctions.tsx +++ b/frontend/src/scenes/actions/ActionHogFunctions.tsx @@ -35,6 +35,7 @@ export function ActionHogFunctions(): JSX.Element | null { ) : null} Get notified via Slack, webhooks or more whenever this event is captured.

Notifications

Get notified when people opt in or out of your feature.

diff --git a/frontend/src/scenes/pipeline/destinations/destinationsLogic.tsx b/frontend/src/scenes/pipeline/destinations/destinationsLogic.tsx index 3d13151059182..6f87bd5668ad8 100644 --- a/frontend/src/scenes/pipeline/destinations/destinationsLogic.tsx +++ b/frontend/src/scenes/pipeline/destinations/destinationsLogic.tsx @@ -166,7 +166,7 @@ export const pipelineDestinationsLogic = kea([ { loadHogFunctions: async () => { // TODO: Support pagination? - return (await api.hogFunctions.list()).results + return (await api.hogFunctions.list({ type: 'destination' })).results }, deleteNodeHogFunction: async ({ destination }) => { diff --git a/frontend/src/scenes/pipeline/hogfunctions/HogFunctionConfiguration.tsx b/frontend/src/scenes/pipeline/hogfunctions/HogFunctionConfiguration.tsx index e16f5cadedc7a..d0e60b03d8944 100644 --- a/frontend/src/scenes/pipeline/hogfunctions/HogFunctionConfiguration.tsx +++ b/frontend/src/scenes/pipeline/hogfunctions/HogFunctionConfiguration.tsx @@ -36,7 +36,12 @@ import { HogFunctionTest, HogFunctionTestPlaceholder } from './HogFunctionTest' const EVENT_THRESHOLD_ALERT_LEVEL = 8000 -export function HogFunctionConfiguration({ templateId, id }: { templateId?: string; id?: string }): JSX.Element { +export interface HogFunctionConfigurationProps { + templateId?: string | null + id?: string | null +} + +export function HogFunctionConfiguration({ templateId, id }: HogFunctionConfigurationProps): JSX.Element { const logicProps = { templateId, id } const logic = hogFunctionConfigurationLogic(logicProps) const { diff --git a/frontend/src/scenes/pipeline/hogfunctions/hogFunctionConfigurationLogic.test.ts b/frontend/src/scenes/pipeline/hogfunctions/hogFunctionConfigurationLogic.test.ts index 79cfaf648f8bc..3c9bef43c45d8 100644 --- a/frontend/src/scenes/pipeline/hogfunctions/hogFunctionConfigurationLogic.test.ts +++ b/frontend/src/scenes/pipeline/hogfunctions/hogFunctionConfigurationLogic.test.ts @@ -63,6 +63,7 @@ const HOG_TEMPLATE: HogFunctionTemplateType = { ], status: 'beta', id: 'template-webhook', + type: 'destination', name: 'HTTP Webhook', description: 'Sends a webhook templated by the incoming event data', hog: "let res := fetch(inputs.url, {\n 'headers': inputs.headers,\n 'body': inputs.body,\n 'method': inputs.method\n});\n\nif (inputs.debug) {\n print('Response', res.status, res.body);\n}", @@ -170,6 +171,7 @@ describe('hogFunctionConfigurationLogic', () => { expect(logic.values.template).toEqual(HOG_TEMPLATE) expect(logic.values.configuration).toEqual({ name: HOG_TEMPLATE.name, + type: HOG_TEMPLATE.type, description: HOG_TEMPLATE.description, inputs_schema: HOG_TEMPLATE.inputs_schema, filters: null, diff --git a/frontend/src/scenes/pipeline/hogfunctions/hogFunctionConfigurationLogic.tsx b/frontend/src/scenes/pipeline/hogfunctions/hogFunctionConfigurationLogic.tsx index 45cc43ccdbbdf..08511bfbdc8f9 100644 --- a/frontend/src/scenes/pipeline/hogfunctions/hogFunctionConfigurationLogic.tsx +++ b/frontend/src/scenes/pipeline/hogfunctions/hogFunctionConfigurationLogic.tsx @@ -46,9 +46,9 @@ import { EmailTemplate } from './email-templater/emailTemplaterLogic' import type { hogFunctionConfigurationLogicType } from './hogFunctionConfigurationLogicType' export interface HogFunctionConfigurationLogicProps { - templateId?: string - subTemplateId?: string - id?: string + templateId?: string | null + subTemplateId?: string | null + id?: string | null } export const EVENT_VOLUME_DAILY_WARNING_THRESHOLD = 1000 @@ -56,6 +56,7 @@ const UNSAVED_CONFIGURATION_TTL = 1000 * 60 * 5 const NEW_FUNCTION_TEMPLATE: HogFunctionTemplateType = { id: 'new', + type: 'destination', name: '', description: '', inputs_schema: [], @@ -118,6 +119,7 @@ const templateToConfiguration = ( }) return { + type: template.type ?? 'destination', name: subTemplate?.name ?? template.name, description: subTemplate?.name ?? template.description, inputs_schema: template.inputs_schema, @@ -403,6 +405,10 @@ export const hogFunctionConfigurationLogic = kea ({ logicProps: [() => [(_, props) => props], (props): HogFunctionConfigurationLogicProps => props], + type: [ + (s) => [s.configuration, s.hogFunction], + (configuration, hogFunction) => configuration?.type ?? hogFunction?.type ?? 'loading', + ], hasAddon: [ (s) => [s.hasAvailableFeature], (hasAvailableFeature) => { diff --git a/frontend/src/scenes/pipeline/hogfunctions/list/LinkedHogFunctions.tsx b/frontend/src/scenes/pipeline/hogfunctions/list/LinkedHogFunctions.tsx index 7403e1f63776d..976e1fda00d99 100644 --- a/frontend/src/scenes/pipeline/hogfunctions/list/LinkedHogFunctions.tsx +++ b/frontend/src/scenes/pipeline/hogfunctions/list/LinkedHogFunctions.tsx @@ -1,18 +1,20 @@ import { LemonButton } from '@posthog/lemon-ui' import { useState } from 'react' -import { HogFunctionFiltersType, HogFunctionSubTemplateIdType } from '~/types' +import { HogFunctionFiltersType, HogFunctionSubTemplateIdType, HogFunctionTypeType } from '~/types' import { HogFunctionList } from './HogFunctionsList' import { HogFunctionTemplateList } from './HogFunctionTemplateList' export type LinkedHogFunctionsProps = { + type: HogFunctionTypeType filters: HogFunctionFiltersType subTemplateId?: HogFunctionSubTemplateIdType newDisabledReason?: string } export function LinkedHogFunctions({ + type, filters, subTemplateId, newDisabledReason, @@ -22,6 +24,7 @@ export function LinkedHogFunctions({ return showNewDestination ? ( @@ -34,6 +37,7 @@ export function LinkedHogFunctions({ ) : ( ([ }, ], })), - loaders(({ values, actions }) => ({ + loaders(({ values, actions, props }) => ({ hogFunctions: [ [] as HogFunctionType[], { @@ -76,6 +77,7 @@ export const hogFunctionListLogic = kea([ return ( await api.hogFunctions.list({ filters: values.filters?.filters, + type: props.type, }) ).results }, diff --git a/frontend/src/scenes/pipeline/hogfunctions/list/hogFunctionTemplateListLogic.tsx b/frontend/src/scenes/pipeline/hogfunctions/list/hogFunctionTemplateListLogic.tsx index 079b56d659a7a..6facd97eb61ae 100644 --- a/frontend/src/scenes/pipeline/hogfunctions/list/hogFunctionTemplateListLogic.tsx +++ b/frontend/src/scenes/pipeline/hogfunctions/list/hogFunctionTemplateListLogic.tsx @@ -8,7 +8,7 @@ import { objectsEqual } from 'lib/utils' import { pipelineAccessLogic } from 'scenes/pipeline/pipelineAccessLogic' import { urls } from 'scenes/urls' -import { HogFunctionTemplateType, PipelineStage } from '~/types' +import { HogFunctionTemplateType, HogFunctionTypeType, PipelineStage } from '~/types' import type { hogFunctionTemplateListLogicType } from './hogFunctionTemplateListLogicType' @@ -22,6 +22,7 @@ export type HogFunctionTemplateListFilters = { } export type HogFunctionTemplateListLogicProps = { + type: HogFunctionTypeType defaultFilters?: HogFunctionTemplateListFilters forceFilters?: HogFunctionTemplateListFilters syncFiltersWithUrl?: boolean @@ -29,7 +30,7 @@ export type HogFunctionTemplateListLogicProps = { export const hogFunctionTemplateListLogic = kea([ props({} as HogFunctionTemplateListLogicProps), - key((props) => (props.syncFiltersWithUrl ? 'scene' : 'default')), + key((props) => `${props.syncFiltersWithUrl ? 'scene' : 'default'}/${props.type ?? 'destination'}`), path((id) => ['scenes', 'pipeline', 'destinationsLogic', id]), connect({ values: [pipelineAccessLogic, ['canEnableNewDestinations'], featureFlagLogic, ['featureFlags']], @@ -53,12 +54,12 @@ export const hogFunctionTemplateListLogic = kea ({ + loaders(({ props }) => ({ rawTemplates: [ [] as HogFunctionTemplateType[], { loadHogFunctionTemplates: async () => { - return (await api.hogFunctions.listTemplates()).results + return (await api.hogFunctions.listTemplates(props.type)).results }, }, ], diff --git a/frontend/src/scenes/surveys/SurveyView.tsx b/frontend/src/scenes/surveys/SurveyView.tsx index 5b69762e9d487..2fdfb11802a68 100644 --- a/frontend/src/scenes/surveys/SurveyView.tsx +++ b/frontend/src/scenes/surveys/SurveyView.tsx @@ -403,6 +403,7 @@ export function SurveyView({ id }: { id: string }): JSX.Element {

Get notified whenever a survey result is submitted

Get notified whenever a survey result is submitted

& { status: HogFunctionTemplateStatus sub_templates?: HogFunctionSubTemplateType[] diff --git a/latest_migrations.manifest b/latest_migrations.manifest index 706c12ae7f1ef..4f5e422e17f82 100644 --- a/latest_migrations.manifest +++ b/latest_migrations.manifest @@ -5,7 +5,7 @@ contenttypes: 0002_remove_content_type_name ee: 0016_rolemembership_organization_member otp_static: 0002_throttling otp_totp: 0002_auto_20190420_0723 -posthog: 0498_errortrackingissuefingerprint_and_more +posthog: 0499_hog_function_type sessions: 0001_initial social_django: 0010_uid_db_index two_factor: 0007_auto_20201201_1019 diff --git a/plugin-server/src/cdp/cdp-consumers.ts b/plugin-server/src/cdp/cdp-consumers.ts index 4e9c4904d2c6b..04404d73cf3fc 100644 --- a/plugin-server/src/cdp/cdp-consumers.ts +++ b/plugin-server/src/cdp/cdp-consumers.ts @@ -548,7 +548,7 @@ export class CdpProcessedEventsConsumer extends CdpConsumerBase { try { const clickHouseEvent = JSON.parse(message.value!.toString()) as RawClickHouseEvent - if (!this.hogFunctionManager.teamHasHogFunctions(clickHouseEvent.team_id)) { + if (!this.hogFunctionManager.teamHasHogDestinations(clickHouseEvent.team_id)) { // No need to continue if the team doesn't have any functions return } diff --git a/plugin-server/src/cdp/hog-executor.ts b/plugin-server/src/cdp/hog-executor.ts index 079ecacfd538f..2e62a9a045a13 100644 --- a/plugin-server/src/cdp/hog-executor.ts +++ b/plugin-server/src/cdp/hog-executor.ts @@ -114,7 +114,7 @@ export class HogExecutor { nonMatchingFunctions: HogFunctionType[] erroredFunctions: HogFunctionType[] } { - const allFunctionsForTeam = this.hogFunctionManager.getTeamHogFunctions(event.project.id) + const allFunctionsForTeam = this.hogFunctionManager.getTeamHogDestinations(event.project.id) const filtersGlobals = convertToHogFunctionFilterGlobal(event) const nonMatchingFunctions: HogFunctionType[] = [] diff --git a/plugin-server/src/cdp/hog-function-manager.ts b/plugin-server/src/cdp/hog-function-manager.ts index 2ae5fba957d05..a54be7d96a058 100644 --- a/plugin-server/src/cdp/hog-function-manager.ts +++ b/plugin-server/src/cdp/hog-function-manager.ts @@ -23,8 +23,11 @@ const HOG_FUNCTION_FIELDS = [ 'filters', 'bytecode', 'masking', + 'type', ] +const RELOAD_HOG_FUNCTION_TYPES = ['destination', 'email'] + export class HogFunctionManager { private started: boolean private ready: boolean @@ -93,6 +96,10 @@ export class HogFunctionManager { .filter((x) => !!x) as HogFunctionType[] } + public getTeamHogDestinations(teamId: Team['id']): HogFunctionType[] { + return this.getTeamHogFunctions(teamId).filter((x) => x.type === 'destination' || !x.type) + } + public getHogFunction(id: HogFunctionType['id']): HogFunctionType | undefined { if (!this.ready) { throw new Error('HogFunctionManager is not ready! Run HogFunctionManager.start() before this') @@ -112,8 +119,8 @@ export class HogFunctionManager { } } - public teamHasHogFunctions(teamId: Team['id']): boolean { - return !!Object.keys(this.getTeamHogFunctions(teamId)).length + public teamHasHogDestinations(teamId: Team['id']): boolean { + return !!Object.keys(this.getTeamHogDestinations(teamId)).length } public async reloadAllHogFunctions(): Promise { @@ -123,9 +130,9 @@ export class HogFunctionManager { ` SELECT ${HOG_FUNCTION_FIELDS.join(', ')} FROM posthog_hogfunction - WHERE deleted = FALSE AND enabled = TRUE + WHERE deleted = FALSE AND enabled = TRUE AND (type is NULL or type = ANY($1)) `, - [], + [RELOAD_HOG_FUNCTION_TYPES], 'fetchAllHogFunctions' ) ).rows diff --git a/plugin-server/src/cdp/types.ts b/plugin-server/src/cdp/types.ts index 19664b2f83e25..19cce8157a075 100644 --- a/plugin-server/src/cdp/types.ts +++ b/plugin-server/src/cdp/types.ts @@ -239,7 +239,7 @@ export type HogFunctionInvocationSerializedCompressed = { // Mostly copied from frontend types export type HogFunctionInputSchemaType = { - type: 'string' | 'boolean' | 'dictionary' | 'choice' | 'json' | 'integration' | 'integration_field' + type: 'string' | 'boolean' | 'dictionary' | 'choice' | 'json' | 'integration' | 'integration_field' | 'email' key: string label?: string choices?: { value: string; label: string }[] @@ -252,8 +252,11 @@ export type HogFunctionInputSchemaType = { integration_field?: 'slack_channel' } +export type HogFunctionTypeType = 'destination' | 'email' | 'sms' | 'push' | 'activity' | 'alert' | 'broadcast' + export type HogFunctionType = { id: string + type: HogFunctionTypeType team_id: number name: string enabled: boolean diff --git a/plugin-server/tests/cdp/fixtures.ts b/plugin-server/tests/cdp/fixtures.ts index e0b668559a9f1..efbf3d2a06f83 100644 --- a/plugin-server/tests/cdp/fixtures.ts +++ b/plugin-server/tests/cdp/fixtures.ts @@ -15,6 +15,7 @@ import { insertRow } from '../helpers/sql' export const createHogFunction = (hogFunction: Partial) => { const item: HogFunctionType = { id: randomUUID(), + type: 'destination', name: 'Hog Function', team_id: 1, enabled: true, diff --git a/plugin-server/tests/cdp/hog-executor.test.ts b/plugin-server/tests/cdp/hog-executor.test.ts index 96b722fb9afdb..f13ebd0e9c0a4 100644 --- a/plugin-server/tests/cdp/hog-executor.test.ts +++ b/plugin-server/tests/cdp/hog-executor.test.ts @@ -43,7 +43,7 @@ describe('Hog Executor', () => { const mockFunctionManager = { reloadAllHogFunctions: jest.fn(), - getTeamHogFunctions: jest.fn(), + getTeamHogDestinations: jest.fn(), getTeamHogFunction: jest.fn(), } @@ -64,7 +64,7 @@ describe('Hog Executor', () => { ...HOG_FILTERS_EXAMPLES.no_filters, }) - mockFunctionManager.getTeamHogFunctions.mockReturnValue([hogFunction]) + mockFunctionManager.getTeamHogDestinations.mockReturnValue([hogFunction]) mockFunctionManager.getTeamHogFunction.mockReturnValue(hogFunction) }) @@ -223,7 +223,7 @@ describe('Hog Executor', () => { ...HOG_FILTERS_EXAMPLES.pageview_or_autocapture_filter, }) - mockFunctionManager.getTeamHogFunctions.mockReturnValue([fn]) + mockFunctionManager.getTeamHogDestinations.mockReturnValue([fn]) const resultsShouldntMatch = executor.findMatchingFunctions(createHogExecutionGlobals({ groups: {} })) expect(resultsShouldntMatch.matchingFunctions).toHaveLength(0) @@ -253,7 +253,7 @@ describe('Hog Executor', () => { ...HOG_INPUTS_EXAMPLES.simple_fetch, ...HOG_FILTERS_EXAMPLES.broken_filters, }) - mockFunctionManager.getTeamHogFunctions.mockReturnValue([fn]) + mockFunctionManager.getTeamHogDestinations.mockReturnValue([fn]) const resultsShouldMatch = executor.findMatchingFunctions( createHogExecutionGlobals({ groups: {}, @@ -285,7 +285,7 @@ describe('Hog Executor', () => { ...HOG_FILTERS_EXAMPLES.elements_text_filter, }) - mockFunctionManager.getTeamHogFunctions.mockReturnValue([fn]) + mockFunctionManager.getTeamHogDestinations.mockReturnValue([fn]) const elementsChain = (buttonText: string) => `span.LemonButton__content:attr__class="LemonButton__content"nth-child="2"nth-of-type="2"text="${buttonText}";span.LemonButton__chrome:attr__class="LemonButton__chrome"nth-child="1"nth-of-type="1";button.LemonButton.LemonButton--has-icon.LemonButton--secondary.LemonButton--status-default:attr__class="LemonButton LemonButton--secondary LemonButton--status-default LemonButton--has-icon"attr__type="button"nth-child="1"nth-of-type="1"text="${buttonText}";div.flex.gap-4.items-center:attr__class="flex gap-4 items-center"nth-child="1"nth-of-type="1";div.flex.flex-wrap.gap-4.justify-between:attr__class="flex gap-4 justify-between flex-wrap"nth-child="3"nth-of-type="3";div.flex.flex-1.flex-col.gap-4.h-full.relative.w-full:attr__class="relative w-full flex flex-col gap-4 flex-1 h-full"nth-child="1"nth-of-type="1";div.LemonTabs__content:attr__class="LemonTabs__content"nth-child="2"nth-of-type="1";div.LemonTabs.LemonTabs--medium:attr__class="LemonTabs LemonTabs--medium"attr__style="--lemon-tabs-slider-width: 48px; --lemon-tabs-slider-offset: 0px;"nth-child="1"nth-of-type="1";div.Navigation3000__scene:attr__class="Navigation3000__scene"nth-child="2"nth-of-type="2";main:nth-child="2"nth-of-type="1";div.Navigation3000:attr__class="Navigation3000"nth-child="1"nth-of-type="1";div:attr__id="root"attr_id="root"nth-child="3"nth-of-type="1";body.overflow-hidden:attr__class="overflow-hidden"attr__theme="light"nth-child="2"nth-of-type="1"` @@ -335,7 +335,7 @@ describe('Hog Executor', () => { ...HOG_FILTERS_EXAMPLES.elements_href_filter, }) - mockFunctionManager.getTeamHogFunctions.mockReturnValue([fn]) + mockFunctionManager.getTeamHogDestinations.mockReturnValue([fn]) const elementsChain = (link: string) => `span.LemonButton__content:attr__class="LemonButton__content"attr__href="${link}"href="${link}"nth-child="2"nth-of-type="2"text="Activity";span.LemonButton__chrome:attr__class="LemonButton__chrome"nth-child="1"nth-of-type="1";a.LemonButton.LemonButton--full-width.LemonButton--has-icon.LemonButton--secondary.LemonButton--status-alt.Link.NavbarButton:attr__class="Link LemonButton LemonButton--secondary LemonButton--status-alt LemonButton--full-width LemonButton--has-icon NavbarButton"attr__data-attr="menu-item-activity"attr__href="${link}"href="${link}"nth-child="1"nth-of-type="1"text="Activity";li.w-full:attr__class="w-full"nth-child="6"nth-of-type="6";ul:nth-child="1"nth-of-type="1";div.Navbar3000__top.ScrollableShadows__inner:attr__class="ScrollableShadows__inner Navbar3000__top"nth-child="1"nth-of-type="1";div.ScrollableShadows.ScrollableShadows--vertical:attr__class="ScrollableShadows ScrollableShadows--vertical"nth-child="1"nth-of-type="1";div.Navbar3000__content:attr__class="Navbar3000__content"nth-child="1"nth-of-type="1";nav.Navbar3000:attr__class="Navbar3000"nth-child="1"nth-of-type="1";div.Navigation3000:attr__class="Navigation3000"nth-child="1"nth-of-type="1";div:attr__id="root"attr_id="root"nth-child="3"nth-of-type="1";body.overflow-hidden:attr__class="overflow-hidden"attr__theme="light"nth-child="2"nth-of-type="1"` @@ -385,7 +385,7 @@ describe('Hog Executor', () => { ...HOG_FILTERS_EXAMPLES.elements_tag_and_id_filter, }) - mockFunctionManager.getTeamHogFunctions.mockReturnValue([fn]) + mockFunctionManager.getTeamHogDestinations.mockReturnValue([fn]) const elementsChain = (id: string) => `a.Link.font-semibold.text-text-3000.text-xl:attr__class="Link font-semibold text-xl text-text-3000"attr__href="/project/1/dashboard/1"attr__id="${id}"attr_id="${id}"href="/project/1/dashboard/1"nth-child="1"nth-of-type="1"text="My App Dashboard";div.ProjectHomepage__dashboardheader__title:attr__class="ProjectHomepage__dashboardheader__title"nth-child="1"nth-of-type="1";div.ProjectHomepage__dashboardheader:attr__class="ProjectHomepage__dashboardheader"nth-child="2"nth-of-type="2";div.ProjectHomepage:attr__class="ProjectHomepage"nth-child="1"nth-of-type="1";div.Navigation3000__scene:attr__class="Navigation3000__scene"nth-child="2"nth-of-type="2";main:nth-child="2"nth-of-type="1";div.Navigation3000:attr__class="Navigation3000"nth-child="1"nth-of-type="1";div:attr__id="root"attr_id="root"nth-child="3"nth-of-type="1";body.overflow-hidden:attr__class="overflow-hidden"attr__theme="light"nth-child="2"nth-of-type="1"` @@ -476,7 +476,7 @@ describe('Hog Executor', () => { ...HOG_FILTERS_EXAMPLES.no_filters, }) - mockFunctionManager.getTeamHogFunctions.mockReturnValue([fn]) + mockFunctionManager.getTeamHogDestinations.mockReturnValue([fn]) const result = executor.execute(createInvocation(fn)) expect(result.error).toContain('Execution timed out after 0.1 seconds. Performed ') diff --git a/plugin-server/tests/cdp/hog-function-manager.test.ts b/plugin-server/tests/cdp/hog-function-manager.test.ts index 818def2f5c8e4..9ae3e2c0e6e10 100644 --- a/plugin-server/tests/cdp/hog-function-manager.test.ts +++ b/plugin-server/tests/cdp/hog-function-manager.test.ts @@ -60,6 +60,24 @@ describe('HogFunctionManager', () => { }) ) + hogFunctions.push( + await insertHogFunction(hub.postgres, teamId1, { + name: 'Email Provider team 1', + type: 'email', + inputs_schema: [ + { + type: 'email', + key: 'message', + }, + ], + inputs: { + email: { + value: { from: 'me@a.com', to: 'you@b.com', subject: 'subject', html: 'text' }, + }, + }, + }) + ) + hogFunctions.push( await insertHogFunction(hub.postgres, teamId2, { name: 'Test Hog Function team 2', @@ -89,13 +107,14 @@ describe('HogFunctionManager', () => { }) it('returns the hog functions', async () => { - let items = manager.getTeamHogFunctions(teamId1) + let items = manager.getTeamHogDestinations(teamId1) expect(items).toEqual([ { id: hogFunctions[0].id, team_id: teamId1, name: 'Test Hog Function team 1', + type: 'destination', enabled: true, bytecode: {}, filters: null, @@ -123,6 +142,10 @@ describe('HogFunctionManager', () => { }, ]) + const allFunctions = manager.getTeamHogFunctions(teamId1) + expect(allFunctions.length).toEqual(2) + expect(allFunctions.map((f) => f.type).sort()).toEqual(['destination', 'email']) + await hub.db.postgres.query( PostgresUse.COMMON_WRITE, `UPDATE posthog_hogfunction SET name='Test Hog Function team 1 updated' WHERE id = $1`, @@ -133,7 +156,7 @@ describe('HogFunctionManager', () => { // This is normally dispatched by django await manager.reloadHogFunctions(teamId1, [hogFunctions[0].id]) - items = manager.getTeamHogFunctions(teamId1) + items = manager.getTeamHogDestinations(teamId1) expect(items).toMatchObject([ { @@ -144,7 +167,7 @@ describe('HogFunctionManager', () => { }) it('removes disabled functions', async () => { - let items = manager.getTeamHogFunctions(teamId1) + let items = manager.getTeamHogDestinations(teamId1) expect(items).toMatchObject([ { @@ -162,14 +185,14 @@ describe('HogFunctionManager', () => { // This is normally dispatched by django await manager.reloadHogFunctions(teamId1, [hogFunctions[0].id]) - items = manager.getTeamHogFunctions(teamId1) + items = manager.getTeamHogDestinations(teamId1) expect(items).toEqual([]) }) it('enriches integration inputs if found and belonging to the team', () => { - const function1Inputs = manager.getTeamHogFunctions(teamId1)[0].inputs - const function2Inputs = manager.getTeamHogFunctions(teamId2)[0].inputs + const function1Inputs = manager.getTeamHogDestinations(teamId1)[0].inputs + const function2Inputs = manager.getTeamHogDestinations(teamId2)[0].inputs // Only the right team gets the integration inputs enriched expect(function1Inputs).toEqual({ diff --git a/posthog/api/hog_function.py b/posthog/api/hog_function.py index 881f6a79bd2b1..83f81ca2e63df 100644 --- a/posthog/api/hog_function.py +++ b/posthog/api/hog_function.py @@ -45,6 +45,7 @@ class Meta: model = HogFunction fields = [ "id", + "type", "name", "description", "created_at", @@ -82,6 +83,7 @@ class Meta: model = HogFunction fields = [ "id", + "type", "name", "description", "created_at", @@ -167,6 +169,9 @@ def validate(self, attrs): if "hog" in attrs: attrs["bytecode"] = compile_hog(attrs["hog"]) + if "type" not in attrs: + attrs["type"] = "destination" + return super().validate(attrs) def to_representation(self, data): @@ -226,7 +231,8 @@ def get_serializer_class(self) -> type[BaseSerializer]: def safely_get_queryset(self, queryset: QuerySet) -> QuerySet: if self.action == "list": - queryset = queryset.filter(deleted=False) + type = self.request.GET.get("type", "destination") + queryset = queryset.filter(deleted=False, type=type) if self.request.GET.get("filters"): try: @@ -309,7 +315,7 @@ def perform_create(self, serializer): item_id=serializer.instance.id, scope="HogFunction", activity="created", - detail=Detail(name=serializer.instance.name, type=serializer.instance.type), + detail=Detail(name=serializer.instance.name, type=serializer.instance.type or "destination"), ) def perform_update(self, serializer): @@ -332,5 +338,7 @@ def perform_update(self, serializer): item_id=instance_id, scope="HogFunction", activity="updated", - detail=Detail(changes=changes, name=serializer.instance.name, type=serializer.instance.type), + detail=Detail( + changes=changes, name=serializer.instance.name, type=serializer.instance.type or "destination" + ), ) diff --git a/posthog/api/hog_function_template.py b/posthog/api/hog_function_template.py index c6dbd0a649add..befb986262ee2 100644 --- a/posthog/api/hog_function_template.py +++ b/posthog/api/hog_function_template.py @@ -33,8 +33,8 @@ class PublicHogFunctionTemplateViewSet(viewsets.GenericViewSet): serializer_class = HogFunctionTemplateSerializer def _get_templates(self): - data = HOG_FUNCTION_TEMPLATES - return data + type = self.request.GET.get("type", "destination") + return [item for item in HOG_FUNCTION_TEMPLATES if item.type == type] def list(self, request: Request, *args, **kwargs): page = self.paginate_queryset(self._get_templates()) diff --git a/posthog/api/test/__snapshots__/test_decide.ambr b/posthog/api/test/__snapshots__/test_decide.ambr index 7e2825e51df8f..75b3f38264283 100644 --- a/posthog/api/test/__snapshots__/test_decide.ambr +++ b/posthog/api/test/__snapshots__/test_decide.ambr @@ -433,6 +433,7 @@ "posthog_hogfunction"."deleted", "posthog_hogfunction"."updated_at", "posthog_hogfunction"."enabled", + "posthog_hogfunction"."type", "posthog_hogfunction"."icon_url", "posthog_hogfunction"."hog", "posthog_hogfunction"."bytecode", @@ -536,6 +537,7 @@ "posthog_hogfunction"."deleted", "posthog_hogfunction"."updated_at", "posthog_hogfunction"."enabled", + "posthog_hogfunction"."type", "posthog_hogfunction"."icon_url", "posthog_hogfunction"."hog", "posthog_hogfunction"."bytecode", @@ -794,6 +796,7 @@ "posthog_hogfunction"."deleted", "posthog_hogfunction"."updated_at", "posthog_hogfunction"."enabled", + "posthog_hogfunction"."type", "posthog_hogfunction"."icon_url", "posthog_hogfunction"."hog", "posthog_hogfunction"."bytecode", @@ -1175,6 +1178,7 @@ "posthog_hogfunction"."deleted", "posthog_hogfunction"."updated_at", "posthog_hogfunction"."enabled", + "posthog_hogfunction"."type", "posthog_hogfunction"."icon_url", "posthog_hogfunction"."hog", "posthog_hogfunction"."bytecode", diff --git a/posthog/api/test/test_hog_function.py b/posthog/api/test/test_hog_function.py index 0b845431b6591..7a9954318d896 100644 --- a/posthog/api/test/test_hog_function.py +++ b/posthog/api/test/test_hog_function.py @@ -19,6 +19,7 @@ EXAMPLE_FULL = { "name": "HogHook", "hog": "fetch(inputs.url, {\n 'headers': inputs.headers,\n 'body': inputs.payload,\n 'method': inputs.method\n});", + "type": "destination", "inputs_schema": [ {"key": "url", "type": "string", "label": "Webhook URL", "required": True}, {"key": "payload", "type": "json", "label": "JSON Payload", "required": True}, @@ -164,6 +165,7 @@ def test_create_hog_function(self, *args): assert response.json()["created_by"]["id"] == self.user.id assert response.json() == { "id": ANY, + "type": "destination", "name": "Fetch URL", "description": "Test description", "created_at": ANY, @@ -220,6 +222,7 @@ def test_creates_with_template_id(self, *args): ) assert response.status_code == status.HTTP_201_CREATED, response.json() assert response.json()["template"] == { + "type": "destination", "name": template_webhook.name, "description": template_webhook.description, "id": template_webhook.id, @@ -236,7 +239,12 @@ def test_creates_with_template_id(self, *args): def test_deletes_via_update(self, *args): response = self.client.post( f"/api/projects/{self.team.id}/hog_functions/", - data={"name": "Fetch URL", "description": "Test description", "hog": "fetch(inputs.url);"}, + data={ + "type": "destination", + "name": "Fetch URL", + "description": "Test description", + "hog": "fetch(inputs.url);", + }, ) assert response.status_code == status.HTTP_201_CREATED, response.json() id = response.json()["id"] @@ -925,3 +933,39 @@ def test_list_with_filters_filter(self, *args): filters = {"actions": [{"id": f"{action2.id}"}]} response = self.client.get(f"/api/projects/{self.team.id}/hog_functions/?filters={json.dumps(filters)}") assert len(response.json()["results"]) == 1 + + def test_list_with_type_filter(self, *args): + response = self.client.post( + f"/api/projects/{self.team.id}/hog_functions/", + data={ + **EXAMPLE_FULL, + "filters": { + "events": [{"id": "$pageview", "name": "$pageview", "type": "events", "order": 0}], + }, + }, + ) + assert response.status_code == status.HTTP_201_CREATED, response.json() + + response = self.client.get(f"/api/projects/{self.team.id}/hog_functions/") + assert len(response.json()["results"]) == 1 + + response = self.client.get(f"/api/projects/{self.team.id}/hog_functions/?type=destination") + assert len(response.json()["results"]) == 1 + + response = self.client.get(f"/api/projects/{self.team.id}/hog_functions/?type=email") + assert len(response.json()["results"]) == 0 + + response = self.client.post( + f"/api/projects/{self.team.id}/hog_functions/", + data={**EXAMPLE_FULL, "type": "email"}, + ) + assert response.status_code == status.HTTP_201_CREATED, response.json() + + response = self.client.get(f"/api/projects/{self.team.id}/hog_functions/") + assert len(response.json()["results"]) == 1 + + response = self.client.get(f"/api/projects/{self.team.id}/hog_functions/?type=destination") + assert len(response.json()["results"]) == 1 + + response = self.client.get(f"/api/projects/{self.team.id}/hog_functions/?type=email") + assert len(response.json()["results"]) == 1 diff --git a/posthog/api/test/test_hog_function_templates.py b/posthog/api/test/test_hog_function_templates.py index 1f6c57c36b1b8..cd9479a10b456 100644 --- a/posthog/api/test/test_hog_function_templates.py +++ b/posthog/api/test/test_hog_function_templates.py @@ -7,6 +7,7 @@ # 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, + "type": "destination", "status": template.status, "id": template.id, "name": template.name, @@ -28,6 +29,17 @@ def test_list_function_templates(self): assert len(response.json()["results"]) > 5 assert response.json()["results"][0] == EXPECTED_FIRST_RESULT + def test_filter_function_templates(self): + response1 = self.client.get("/api/projects/@current/hog_function_templates/?type=notfound") + assert response1.status_code == status.HTTP_200_OK, response1.json() + assert len(response1.json()["results"]) == 0 + + response2 = self.client.get("/api/projects/@current/hog_function_templates/?type=destination") + response3 = self.client.get("/api/projects/@current/hog_function_templates/") + + assert response2.json()["results"] == response3.json()["results"] + assert len(response2.json()["results"]) > 5 + def test_public_list_function_templates(self): self.client.logout() response = self.client.get("/api/public_hog_function_templates/") diff --git a/posthog/cdp/templates/activecampaign/template_activecampaign.py b/posthog/cdp/templates/activecampaign/template_activecampaign.py index 1f79d51325574..e25e526017c66 100644 --- a/posthog/cdp/templates/activecampaign/template_activecampaign.py +++ b/posthog/cdp/templates/activecampaign/template_activecampaign.py @@ -3,6 +3,7 @@ template: HogFunctionTemplate = HogFunctionTemplate( status="beta", + type="destination", id="template-activecampaign", name="ActiveCampaign", description="Creates a new contact in ActiveCampaign whenever an event is triggered.", diff --git a/posthog/cdp/templates/airtable/template_airtable.py b/posthog/cdp/templates/airtable/template_airtable.py index f2e0ef2bc6133..436933057e2c7 100644 --- a/posthog/cdp/templates/airtable/template_airtable.py +++ b/posthog/cdp/templates/airtable/template_airtable.py @@ -3,6 +3,7 @@ template: HogFunctionTemplate = HogFunctionTemplate( status="alpha", + type="destination", id="template-airtable", name="Airtable", description="Creates Airtable records", diff --git a/posthog/cdp/templates/attio/template_attio.py b/posthog/cdp/templates/attio/template_attio.py index 8899ee01bff36..0ed851d9ed4d0 100644 --- a/posthog/cdp/templates/attio/template_attio.py +++ b/posthog/cdp/templates/attio/template_attio.py @@ -2,6 +2,7 @@ template: HogFunctionTemplate = HogFunctionTemplate( status="beta", + type="destination", id="template-attio", name="Attio", description="Update contacts in Attio", diff --git a/posthog/cdp/templates/avo/template_avo.py b/posthog/cdp/templates/avo/template_avo.py index 8045e3ce83351..073a819b108ac 100644 --- a/posthog/cdp/templates/avo/template_avo.py +++ b/posthog/cdp/templates/avo/template_avo.py @@ -5,6 +5,7 @@ template: HogFunctionTemplate = HogFunctionTemplate( status="beta", + type="destination", id="template-avo", name="Avo", description="Send events to Avo", diff --git a/posthog/cdp/templates/aws_kinesis/template_aws_kinesis.py b/posthog/cdp/templates/aws_kinesis/template_aws_kinesis.py index f7d7f4d36c603..96fdc76d403f8 100644 --- a/posthog/cdp/templates/aws_kinesis/template_aws_kinesis.py +++ b/posthog/cdp/templates/aws_kinesis/template_aws_kinesis.py @@ -3,6 +3,7 @@ template: HogFunctionTemplate = HogFunctionTemplate( status="beta", + type="destination", id="template-aws-kinesis", name="AWS Kinesis", description="Put data to an AWS Kinesis stream", diff --git a/posthog/cdp/templates/braze/template_braze.py b/posthog/cdp/templates/braze/template_braze.py index 72258f9db5cab..cd7f1ac733793 100644 --- a/posthog/cdp/templates/braze/template_braze.py +++ b/posthog/cdp/templates/braze/template_braze.py @@ -3,6 +3,7 @@ template: HogFunctionTemplate = HogFunctionTemplate( status="beta", + type="destination", id="template-braze", name="Braze", description="Send events to Braze", diff --git a/posthog/cdp/templates/clearbit/template_clearbit.py b/posthog/cdp/templates/clearbit/template_clearbit.py index bc9b3540bb146..415cc14255255 100644 --- a/posthog/cdp/templates/clearbit/template_clearbit.py +++ b/posthog/cdp/templates/clearbit/template_clearbit.py @@ -5,6 +5,7 @@ template: HogFunctionTemplate = HogFunctionTemplate( status="alpha", + type="destination", id="template-clearbit", name="Clearbit", description="Loads data from the Clearbit API and tracks an additional event with the enriched data if found. Once enriched, the person will not be enriched again.", diff --git a/posthog/cdp/templates/customerio/template_customerio.py b/posthog/cdp/templates/customerio/template_customerio.py index 25688ec9d92d8..68239b7de8e89 100644 --- a/posthog/cdp/templates/customerio/template_customerio.py +++ b/posthog/cdp/templates/customerio/template_customerio.py @@ -6,6 +6,7 @@ template: HogFunctionTemplate = HogFunctionTemplate( status="beta", + type="destination", id="template-customerio", name="Customer.io", description="Identify or track events against customers in Customer.io", diff --git a/posthog/cdp/templates/discord/template_discord.py b/posthog/cdp/templates/discord/template_discord.py index bca3f4604e216..fb8cb2bf50c64 100644 --- a/posthog/cdp/templates/discord/template_discord.py +++ b/posthog/cdp/templates/discord/template_discord.py @@ -2,6 +2,7 @@ template: HogFunctionTemplate = HogFunctionTemplate( status="free", + type="destination", id="template-discord", name="Discord", description="Sends a message to a discord channel", diff --git a/posthog/cdp/templates/engage/template_engage.py b/posthog/cdp/templates/engage/template_engage.py index 065cb3c9057bd..78d70d2e99a2f 100644 --- a/posthog/cdp/templates/engage/template_engage.py +++ b/posthog/cdp/templates/engage/template_engage.py @@ -4,6 +4,7 @@ template: HogFunctionTemplate = HogFunctionTemplate( status="beta", + type="destination", id="template-engage-so", name="Engage.so", description="Send events to Engage.so", diff --git a/posthog/cdp/templates/gleap/template_gleap.py b/posthog/cdp/templates/gleap/template_gleap.py index 1616c373594a6..8925c8b19b05b 100644 --- a/posthog/cdp/templates/gleap/template_gleap.py +++ b/posthog/cdp/templates/gleap/template_gleap.py @@ -2,6 +2,7 @@ template: HogFunctionTemplate = HogFunctionTemplate( status="beta", + type="destination", id="template-gleap", name="Gleap", description="Updates a contact in Gleap", diff --git a/posthog/cdp/templates/google_ads/template_google_ads.py b/posthog/cdp/templates/google_ads/template_google_ads.py index 1a658a9436ed2..e6496f50b6b52 100644 --- a/posthog/cdp/templates/google_ads/template_google_ads.py +++ b/posthog/cdp/templates/google_ads/template_google_ads.py @@ -2,6 +2,7 @@ template: HogFunctionTemplate = HogFunctionTemplate( status="alpha", + type="destination", id="template-google-ads", name="Google Ads Conversions", description="Send conversion events to Google Ads", diff --git a/posthog/cdp/templates/google_cloud_storage/template_google_cloud_storage.py b/posthog/cdp/templates/google_cloud_storage/template_google_cloud_storage.py index a24417b603b85..925b43945bc0a 100644 --- a/posthog/cdp/templates/google_cloud_storage/template_google_cloud_storage.py +++ b/posthog/cdp/templates/google_cloud_storage/template_google_cloud_storage.py @@ -8,6 +8,7 @@ template: HogFunctionTemplate = HogFunctionTemplate( status="beta", + type="destination", id="template-google-cloud-storage", name="Google Cloud Storage", description="Send data to GCS. This creates a file per event.", diff --git a/posthog/cdp/templates/google_pubsub/template_google_pubsub.py b/posthog/cdp/templates/google_pubsub/template_google_pubsub.py index 2c17dbb40d2e0..6ee1c56855490 100644 --- a/posthog/cdp/templates/google_pubsub/template_google_pubsub.py +++ b/posthog/cdp/templates/google_pubsub/template_google_pubsub.py @@ -8,6 +8,7 @@ template: HogFunctionTemplate = HogFunctionTemplate( status="beta", + type="destination", id="template-google-pubsub", name="Google Pub/Sub", description="Send data to a Google Pub/Sub topic", diff --git a/posthog/cdp/templates/hog_function_template.py b/posthog/cdp/templates/hog_function_template.py index dc41d27c87dae..f0a29fd7d2cff 100644 --- a/posthog/cdp/templates/hog_function_template.py +++ b/posthog/cdp/templates/hog_function_template.py @@ -26,6 +26,7 @@ class HogFunctionSubTemplate: @dataclasses.dataclass(frozen=True) class HogFunctionTemplate: status: Literal["alpha", "beta", "stable", "free"] + type: Literal["destination", "shared", "email", "sms", "push", "broadcast", "activity", "alert"] id: str name: str description: str diff --git a/posthog/cdp/templates/hubspot/template_hubspot.py b/posthog/cdp/templates/hubspot/template_hubspot.py index 78c0eb4dda73b..4cb08682a0e00 100644 --- a/posthog/cdp/templates/hubspot/template_hubspot.py +++ b/posthog/cdp/templates/hubspot/template_hubspot.py @@ -6,6 +6,7 @@ template: HogFunctionTemplate = HogFunctionTemplate( status="beta", + type="destination", id="template-hubspot", name="Hubspot", description="Creates a new contact in Hubspot whenever an event is triggered.", diff --git a/posthog/cdp/templates/intercom/template_intercom.py b/posthog/cdp/templates/intercom/template_intercom.py index 43bfcc0d80f37..069c231829231 100644 --- a/posthog/cdp/templates/intercom/template_intercom.py +++ b/posthog/cdp/templates/intercom/template_intercom.py @@ -5,6 +5,7 @@ template: HogFunctionTemplate = HogFunctionTemplate( status="beta", + type="destination", id="template-Intercom", name="Intercom", description="Send events and contact information to Intercom", diff --git a/posthog/cdp/templates/klaviyo/template_klaviyo.py b/posthog/cdp/templates/klaviyo/template_klaviyo.py index 805e1ad887226..6032632ae24e3 100644 --- a/posthog/cdp/templates/klaviyo/template_klaviyo.py +++ b/posthog/cdp/templates/klaviyo/template_klaviyo.py @@ -2,6 +2,7 @@ template_user: HogFunctionTemplate = HogFunctionTemplate( status="beta", + type="destination", id="template-klaviyo-user", name="Klaviyo", description="Updates a contact in Klaviyo", @@ -143,6 +144,7 @@ template_event: HogFunctionTemplate = HogFunctionTemplate( status="beta", + type="destination", id="template-klaviyo-event", name="Klaviyo", description="Send events to Klaviyo", diff --git a/posthog/cdp/templates/knock/template_knock.py b/posthog/cdp/templates/knock/template_knock.py index 235f7d38a60ed..68c5bcccff40d 100644 --- a/posthog/cdp/templates/knock/template_knock.py +++ b/posthog/cdp/templates/knock/template_knock.py @@ -2,6 +2,7 @@ template: HogFunctionTemplate = HogFunctionTemplate( status="beta", + type="destination", id="template-knock", name="Knock", description="Send events to Knock", diff --git a/posthog/cdp/templates/loops/template_loops.py b/posthog/cdp/templates/loops/template_loops.py index 114bd3a6e98e5..6052eed186b36 100644 --- a/posthog/cdp/templates/loops/template_loops.py +++ b/posthog/cdp/templates/loops/template_loops.py @@ -4,6 +4,7 @@ template: HogFunctionTemplate = HogFunctionTemplate( status="beta", + type="destination", id="template-loops", name="Loops", description="Send events to Loops", diff --git a/posthog/cdp/templates/mailchimp/template_mailchimp.py b/posthog/cdp/templates/mailchimp/template_mailchimp.py index e12eab6dfc7d9..4ed3465e21e82 100644 --- a/posthog/cdp/templates/mailchimp/template_mailchimp.py +++ b/posthog/cdp/templates/mailchimp/template_mailchimp.py @@ -2,6 +2,7 @@ template: HogFunctionTemplate = HogFunctionTemplate( status="beta", + type="destination", id="template-mailchimp", name="Mailchimp", description="Updates a contact in Mailchimp and subscribes new ones.", diff --git a/posthog/cdp/templates/mailgun/template_mailgun.py b/posthog/cdp/templates/mailgun/template_mailgun.py index e220bf6535dce..9f7a91eea7a17 100644 --- a/posthog/cdp/templates/mailgun/template_mailgun.py +++ b/posthog/cdp/templates/mailgun/template_mailgun.py @@ -6,6 +6,7 @@ template_mailgun_send_email: HogFunctionTemplate = HogFunctionTemplate( status="alpha", + type="destination", id="template-mailgun-send-email", name="Mailgun", description="Send emails using the Mailgun HTTP API", diff --git a/posthog/cdp/templates/mailjet/template_mailjet.py b/posthog/cdp/templates/mailjet/template_mailjet.py index c35a17baf9c95..a089372daefb8 100644 --- a/posthog/cdp/templates/mailjet/template_mailjet.py +++ b/posthog/cdp/templates/mailjet/template_mailjet.py @@ -31,6 +31,7 @@ template_create_contact: HogFunctionTemplate = HogFunctionTemplate( status="beta", + type="destination", id="template-mailjet-create-contact", name="Mailjet", description="Add contacts to Mailjet", @@ -81,6 +82,7 @@ template_update_contact_list: HogFunctionTemplate = HogFunctionTemplate( status="beta", + type="destination", id="template-mailjet-update-contact-list", name="Mailjet", description="Update a Mailjet contact list", diff --git a/posthog/cdp/templates/meta_ads/template_meta_ads.py b/posthog/cdp/templates/meta_ads/template_meta_ads.py index ad5b2fed1ff7c..146166b14946d 100644 --- a/posthog/cdp/templates/meta_ads/template_meta_ads.py +++ b/posthog/cdp/templates/meta_ads/template_meta_ads.py @@ -2,6 +2,7 @@ template: HogFunctionTemplate = HogFunctionTemplate( status="alpha", + type="destination", id="template-meta-ads", name="Meta Ads Conversions", description="Send conversion events to Meta Ads", diff --git a/posthog/cdp/templates/microsoft_teams/template_microsoft_teams.py b/posthog/cdp/templates/microsoft_teams/template_microsoft_teams.py index da8a1506f9d61..c1ce7afc5a909 100644 --- a/posthog/cdp/templates/microsoft_teams/template_microsoft_teams.py +++ b/posthog/cdp/templates/microsoft_teams/template_microsoft_teams.py @@ -2,6 +2,7 @@ template: HogFunctionTemplate = HogFunctionTemplate( status="free", + type="destination", id="template-microsoft-teams", name="Microsoft Teams", description="Sends a message to a Microsoft Teams channel", diff --git a/posthog/cdp/templates/posthog/template_posthog.py b/posthog/cdp/templates/posthog/template_posthog.py index a06876d382cee..fcd76879b8078 100644 --- a/posthog/cdp/templates/posthog/template_posthog.py +++ b/posthog/cdp/templates/posthog/template_posthog.py @@ -6,6 +6,7 @@ template: HogFunctionTemplate = HogFunctionTemplate( status="beta", + type="destination", id="template-posthog-replicator", name="PostHog", description="Send a copy of the incoming data in realtime to another PostHog instance", diff --git a/posthog/cdp/templates/rudderstack/template_rudderstack.py b/posthog/cdp/templates/rudderstack/template_rudderstack.py index b2c19d8fb7e68..fbc3ef2f65371 100644 --- a/posthog/cdp/templates/rudderstack/template_rudderstack.py +++ b/posthog/cdp/templates/rudderstack/template_rudderstack.py @@ -5,6 +5,7 @@ template: HogFunctionTemplate = HogFunctionTemplate( status="alpha", + type="destination", id="template-rudderstack", name="RudderStack", description="Send data to RudderStack", diff --git a/posthog/cdp/templates/salesforce/template_salesforce.py b/posthog/cdp/templates/salesforce/template_salesforce.py index 1b26558b3d48c..eedfd9980efb1 100644 --- a/posthog/cdp/templates/salesforce/template_salesforce.py +++ b/posthog/cdp/templates/salesforce/template_salesforce.py @@ -22,6 +22,7 @@ template_create: HogFunctionTemplate = HogFunctionTemplate( status="alpha", + type="destination", id="template-salesforce-create", name="Salesforce", description="Create objects in Salesforce", @@ -108,6 +109,7 @@ template_update: HogFunctionTemplate = HogFunctionTemplate( status="alpha", + type="destination", id="template-salesforce-update", name="Salesforce", description="Update objects in Salesforce", diff --git a/posthog/cdp/templates/sendgrid/template_sendgrid.py b/posthog/cdp/templates/sendgrid/template_sendgrid.py index 96a0d07ccdf2e..c7a17ebc24516 100644 --- a/posthog/cdp/templates/sendgrid/template_sendgrid.py +++ b/posthog/cdp/templates/sendgrid/template_sendgrid.py @@ -7,6 +7,7 @@ template: HogFunctionTemplate = HogFunctionTemplate( status="beta", + type="destination", id="template-sendgrid", name="Sendgrid", description="Update marketing contacts in Sendgrid", diff --git a/posthog/cdp/templates/slack/template_slack.py b/posthog/cdp/templates/slack/template_slack.py index 970848e8de3e6..b3079485176c6 100644 --- a/posthog/cdp/templates/slack/template_slack.py +++ b/posthog/cdp/templates/slack/template_slack.py @@ -2,6 +2,7 @@ template: HogFunctionTemplate = HogFunctionTemplate( status="free", + type="destination", id="template-slack", name="Slack", description="Sends a message to a slack channel", diff --git a/posthog/cdp/templates/webhook/template_webhook.py b/posthog/cdp/templates/webhook/template_webhook.py index 23ee39cb542cb..f27822c5e0c24 100644 --- a/posthog/cdp/templates/webhook/template_webhook.py +++ b/posthog/cdp/templates/webhook/template_webhook.py @@ -3,6 +3,7 @@ template: HogFunctionTemplate = HogFunctionTemplate( status="beta", + type="destination", id="template-webhook", name="HTTP Webhook", description="Sends a webhook templated by the incoming event data", diff --git a/posthog/cdp/templates/zapier/template_zapier.py b/posthog/cdp/templates/zapier/template_zapier.py index 973fa2f276509..92d5de5916fbf 100644 --- a/posthog/cdp/templates/zapier/template_zapier.py +++ b/posthog/cdp/templates/zapier/template_zapier.py @@ -3,6 +3,7 @@ template: HogFunctionTemplate = HogFunctionTemplate( status="free", + type="destination", id="template-zapier", name="Zapier", description="Sends a webhook templated by the incoming event data", diff --git a/posthog/cdp/templates/zendesk/template_zendesk.py b/posthog/cdp/templates/zendesk/template_zendesk.py index 79184d3f607f0..3604c5d9a3964 100644 --- a/posthog/cdp/templates/zendesk/template_zendesk.py +++ b/posthog/cdp/templates/zendesk/template_zendesk.py @@ -4,6 +4,7 @@ template: HogFunctionTemplate = HogFunctionTemplate( status="beta", + type="destination", id="template-zendesk", name="Zendesk", description="Update contacts in Zendesk", diff --git a/posthog/hogql/transforms/test/__snapshots__/test_in_cohort.ambr b/posthog/hogql/transforms/test/__snapshots__/test_in_cohort.ambr index f018e96ef067a..06e46e0ca30b0 100644 --- a/posthog/hogql/transforms/test/__snapshots__/test_in_cohort.ambr +++ b/posthog/hogql/transforms/test/__snapshots__/test_in_cohort.ambr @@ -31,7 +31,7 @@ FROM events LEFT JOIN ( SELECT person_static_cohort.person_id AS cohort_person_id, 1 AS matched, person_static_cohort.cohort_id AS cohort_id FROM person_static_cohort - WHERE and(equals(person_static_cohort.team_id, 420), in(person_static_cohort.cohort_id, [4]))) AS __in_cohort ON equals(__in_cohort.cohort_person_id, events.person_id) + WHERE and(equals(person_static_cohort.team_id, 420), in(person_static_cohort.cohort_id, [6]))) AS __in_cohort ON equals(__in_cohort.cohort_person_id, events.person_id) WHERE and(equals(events.team_id, 420), 1, ifNull(equals(__in_cohort.matched, 1), 0)) LIMIT 100 SETTINGS readonly=2, max_execution_time=60, allow_experimental_object_type=1, format_csv_allow_double_quotes=0, max_ast_elements=4000000, max_expanded_ast_elements=4000000, max_bytes_before_external_group_by=0 @@ -42,7 +42,7 @@ FROM events LEFT JOIN ( SELECT person_id AS cohort_person_id, 1 AS matched, cohort_id FROM static_cohort_people - WHERE in(cohort_id, [4])) AS __in_cohort ON equals(__in_cohort.cohort_person_id, person_id) + WHERE in(cohort_id, [6])) AS __in_cohort ON equals(__in_cohort.cohort_person_id, person_id) WHERE and(1, equals(__in_cohort.matched, 1)) LIMIT 100 ''' @@ -55,7 +55,7 @@ FROM events LEFT JOIN ( SELECT person_static_cohort.person_id AS cohort_person_id, 1 AS matched, person_static_cohort.cohort_id AS cohort_id FROM person_static_cohort - WHERE and(equals(person_static_cohort.team_id, 420), in(person_static_cohort.cohort_id, [5]))) AS __in_cohort ON equals(__in_cohort.cohort_person_id, events.person_id) + WHERE and(equals(person_static_cohort.team_id, 420), in(person_static_cohort.cohort_id, [7]))) AS __in_cohort ON equals(__in_cohort.cohort_person_id, events.person_id) WHERE and(equals(events.team_id, 420), 1, ifNull(equals(__in_cohort.matched, 1), 0)) LIMIT 100 SETTINGS readonly=2, max_execution_time=60, allow_experimental_object_type=1, format_csv_allow_double_quotes=0, max_ast_elements=4000000, max_expanded_ast_elements=4000000, max_bytes_before_external_group_by=0 @@ -66,7 +66,7 @@ FROM events LEFT JOIN ( SELECT person_id AS cohort_person_id, 1 AS matched, cohort_id FROM static_cohort_people - WHERE in(cohort_id, [5])) AS __in_cohort ON equals(__in_cohort.cohort_person_id, person_id) + WHERE in(cohort_id, [7])) AS __in_cohort ON equals(__in_cohort.cohort_person_id, person_id) WHERE and(1, equals(__in_cohort.matched, 1)) LIMIT 100 ''' diff --git a/posthog/hogql_queries/insights/trends/test/__snapshots__/test_trends.ambr b/posthog/hogql_queries/insights/trends/test/__snapshots__/test_trends.ambr index 6027f7ca7bb42..4ae57feb8cb96 100644 --- a/posthog/hogql_queries/insights/trends/test/__snapshots__/test_trends.ambr +++ b/posthog/hogql_queries/insights/trends/test/__snapshots__/test_trends.ambr @@ -851,14 +851,49 @@ # --- # name: TestTrends.test_dau_with_breakdown_filtering_with_sampling.1 ''' - /* celery:posthog.tasks.tasks.sync_insight_caching_state */ - SELECT team_id, - date_diff('second', max(timestamp), now()) AS age - FROM events - WHERE timestamp > date_sub(DAY, 3, now()) - AND timestamp < now() - GROUP BY team_id - ORDER BY age; + SELECT groupArray(1)(date)[1] AS date, + arrayFold((acc, x) -> arrayMap(i -> plus(acc[i], x[i]), range(1, plus(length(date), 1))), groupArray(ifNull(total, 0)), arrayWithConstant(length(date), reinterpretAsFloat64(0))) AS total, + if(ifNull(ifNull(greaterOrEquals(row_number, 25), 0), 0), '$$_posthog_breakdown_other_$$', breakdown_value) AS breakdown_value + FROM + (SELECT arrayMap(number -> plus(toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-28 00:00:00', 6, 'UTC'))), toIntervalDay(number)), range(0, plus(coalesce(dateDiff('day', toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-28 00:00:00', 6, 'UTC'))), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-04 23:59:59', 6, 'UTC'))))), 1))) AS date, + arrayMap(_match_date -> arraySum(arraySlice(groupArray(ifNull(count, 0)), indexOf(groupArray(day_start) AS _days_for_count, _match_date) AS _index, plus(minus(arrayLastIndex(x -> ifNull(equals(x, _match_date), isNull(x) + and isNull(_match_date)), _days_for_count), _index), 1))), date) AS total, + breakdown_value AS breakdown_value, + rowNumberInAllBlocks() AS row_number + FROM + (SELECT sum(total) AS count, + day_start AS day_start, + breakdown_value AS breakdown_value + FROM + (SELECT count(DISTINCT if(not(empty(e__override.distinct_id)), e__override.person_id, e.person_id)) AS total, + toStartOfDay(toTimeZone(e.timestamp, 'UTC')) AS day_start, + ifNull(nullIf(toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', '')), ''), '$$_posthog_breakdown_null_$$') AS breakdown_value + FROM events AS e SAMPLE 1.0 + LEFT OUTER JOIN + (SELECT argMax(person_distinct_id_overrides.person_id, person_distinct_id_overrides.version) AS person_id, + person_distinct_id_overrides.distinct_id AS distinct_id + FROM person_distinct_id_overrides + WHERE equals(person_distinct_id_overrides.team_id, 2) + GROUP BY person_distinct_id_overrides.distinct_id + HAVING ifNull(equals(argMax(person_distinct_id_overrides.is_deleted, person_distinct_id_overrides.version), 0), 0) SETTINGS optimize_aggregation_in_order=1) AS e__override ON equals(e.distinct_id, e__override.distinct_id) + WHERE and(equals(e.team_id, 2), greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-28 00:00:00', 6, 'UTC')))), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-04 23:59:59', 6, 'UTC'))), equals(e.event, 'sign up')) + GROUP BY day_start, + breakdown_value) + GROUP BY day_start, + breakdown_value + ORDER BY day_start ASC, breakdown_value ASC) + GROUP BY breakdown_value + ORDER BY if(ifNull(equals(breakdown_value, '$$_posthog_breakdown_other_$$'), 0), 2, if(ifNull(equals(breakdown_value, '$$_posthog_breakdown_null_$$'), 0), 1, 0)) ASC, arraySum(total) DESC, breakdown_value ASC) + WHERE isNotNull(breakdown_value) + GROUP BY breakdown_value + ORDER BY if(ifNull(equals(breakdown_value, '$$_posthog_breakdown_other_$$'), 0), 2, if(ifNull(equals(breakdown_value, '$$_posthog_breakdown_null_$$'), 0), 1, 0)) ASC, arraySum(total) DESC, breakdown_value ASC + LIMIT 50000 SETTINGS readonly=2, + max_execution_time=60, + allow_experimental_object_type=1, + format_csv_allow_double_quotes=0, + max_ast_elements=4000000, + max_expanded_ast_elements=4000000, + max_bytes_before_external_group_by=0 ''' # --- # name: TestTrends.test_dau_with_breakdown_filtering_with_sampling.10 @@ -1075,38 +1110,143 @@ # --- # name: TestTrends.test_dau_with_breakdown_filtering_with_sampling.2 ''' - /* celery:posthog.tasks.tasks.sync_insight_caching_state */ - SELECT team_id, - date_diff('second', max(timestamp), now()) AS age - FROM events - WHERE timestamp > date_sub(DAY, 3, now()) - AND timestamp < now() - GROUP BY team_id - ORDER BY age; + SELECT groupArray(1)(date)[1] AS date, + arrayFold((acc, x) -> arrayMap(i -> plus(acc[i], x[i]), range(1, plus(length(date), 1))), groupArray(ifNull(total, 0)), arrayWithConstant(length(date), reinterpretAsFloat64(0))) AS total, + if(ifNull(ifNull(greaterOrEquals(row_number, 25), 0), 0), '$$_posthog_breakdown_other_$$', breakdown_value) AS breakdown_value + FROM + (SELECT arrayMap(number -> plus(toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-28 00:00:00', 6, 'UTC'))), toIntervalDay(number)), range(0, plus(coalesce(dateDiff('day', toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-28 00:00:00', 6, 'UTC'))), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-04 23:59:59', 6, 'UTC'))))), 1))) AS date, + arrayMap(_match_date -> arraySum(arraySlice(groupArray(ifNull(count, 0)), indexOf(groupArray(day_start) AS _days_for_count, _match_date) AS _index, plus(minus(arrayLastIndex(x -> ifNull(equals(x, _match_date), isNull(x) + and isNull(_match_date)), _days_for_count), _index), 1))), date) AS total, + breakdown_value AS breakdown_value, + rowNumberInAllBlocks() AS row_number + FROM + (SELECT sum(total) AS count, + day_start AS day_start, + breakdown_value AS breakdown_value + FROM + (SELECT count(DISTINCT if(not(empty(e__override.distinct_id)), e__override.person_id, e.person_id)) AS total, + toStartOfDay(toTimeZone(e.timestamp, 'UTC')) AS day_start, + ifNull(nullIf(toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', '')), ''), '$$_posthog_breakdown_null_$$') AS breakdown_value + FROM events AS e SAMPLE 1.0 + LEFT OUTER JOIN + (SELECT argMax(person_distinct_id_overrides.person_id, person_distinct_id_overrides.version) AS person_id, + person_distinct_id_overrides.distinct_id AS distinct_id + FROM person_distinct_id_overrides + WHERE equals(person_distinct_id_overrides.team_id, 2) + GROUP BY person_distinct_id_overrides.distinct_id + HAVING ifNull(equals(argMax(person_distinct_id_overrides.is_deleted, person_distinct_id_overrides.version), 0), 0) SETTINGS optimize_aggregation_in_order=1) AS e__override ON equals(e.distinct_id, e__override.distinct_id) + WHERE and(equals(e.team_id, 2), greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-28 00:00:00', 6, 'UTC')))), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-04 23:59:59', 6, 'UTC'))), equals(e.event, 'sign up')) + GROUP BY day_start, + breakdown_value) + GROUP BY day_start, + breakdown_value + ORDER BY day_start ASC, breakdown_value ASC) + GROUP BY breakdown_value + ORDER BY if(ifNull(equals(breakdown_value, '$$_posthog_breakdown_other_$$'), 0), 2, if(ifNull(equals(breakdown_value, '$$_posthog_breakdown_null_$$'), 0), 1, 0)) ASC, arraySum(total) DESC, breakdown_value ASC) + WHERE isNotNull(breakdown_value) + GROUP BY breakdown_value + ORDER BY if(ifNull(equals(breakdown_value, '$$_posthog_breakdown_other_$$'), 0), 2, if(ifNull(equals(breakdown_value, '$$_posthog_breakdown_null_$$'), 0), 1, 0)) ASC, arraySum(total) DESC, breakdown_value ASC + LIMIT 50000 SETTINGS readonly=2, + max_execution_time=60, + allow_experimental_object_type=1, + format_csv_allow_double_quotes=0, + max_ast_elements=4000000, + max_expanded_ast_elements=4000000, + max_bytes_before_external_group_by=0 ''' # --- # name: TestTrends.test_dau_with_breakdown_filtering_with_sampling.3 ''' - /* celery:posthog.tasks.tasks.sync_insight_caching_state */ - SELECT team_id, - date_diff('second', max(timestamp), now()) AS age - FROM events - WHERE timestamp > date_sub(DAY, 3, now()) - AND timestamp < now() - GROUP BY team_id - ORDER BY age; + SELECT groupArray(1)(date)[1] AS date, + arrayFold((acc, x) -> arrayMap(i -> plus(acc[i], x[i]), range(1, plus(length(date), 1))), groupArray(ifNull(total, 0)), arrayWithConstant(length(date), reinterpretAsFloat64(0))) AS total, + arrayMap(i -> if(ifNull(ifNull(greaterOrEquals(row_number, 25), 0), 0), '$$_posthog_breakdown_other_$$', i), breakdown_value) AS breakdown_value + FROM + (SELECT arrayMap(number -> plus(toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-28 00:00:00', 6, 'UTC'))), toIntervalDay(number)), range(0, plus(coalesce(dateDiff('day', toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-28 00:00:00', 6, 'UTC'))), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-04 23:59:59', 6, 'UTC'))))), 1))) AS date, + arrayMap(_match_date -> arraySum(arraySlice(groupArray(ifNull(count, 0)), indexOf(groupArray(day_start) AS _days_for_count, _match_date) AS _index, plus(minus(arrayLastIndex(x -> ifNull(equals(x, _match_date), isNull(x) + and isNull(_match_date)), _days_for_count), _index), 1))), date) AS total, + breakdown_value AS breakdown_value, + rowNumberInAllBlocks() AS row_number + FROM + (SELECT sum(total) AS count, + day_start AS day_start, + [ifNull(toString(breakdown_value_1), '$$_posthog_breakdown_null_$$')] AS breakdown_value + FROM + (SELECT count(DISTINCT if(not(empty(e__override.distinct_id)), e__override.person_id, e.person_id)) AS total, + toStartOfDay(toTimeZone(e.timestamp, 'UTC')) AS day_start, + ifNull(nullIf(toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', '')), ''), '$$_posthog_breakdown_null_$$') AS breakdown_value_1 + FROM events AS e SAMPLE 1.0 + LEFT OUTER JOIN + (SELECT argMax(person_distinct_id_overrides.person_id, person_distinct_id_overrides.version) AS person_id, + person_distinct_id_overrides.distinct_id AS distinct_id + FROM person_distinct_id_overrides + WHERE equals(person_distinct_id_overrides.team_id, 2) + GROUP BY person_distinct_id_overrides.distinct_id + HAVING ifNull(equals(argMax(person_distinct_id_overrides.is_deleted, person_distinct_id_overrides.version), 0), 0) SETTINGS optimize_aggregation_in_order=1) AS e__override ON equals(e.distinct_id, e__override.distinct_id) + WHERE and(equals(e.team_id, 2), greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-28 00:00:00', 6, 'UTC')))), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-04 23:59:59', 6, 'UTC'))), equals(e.event, 'sign up')) + GROUP BY day_start, + breakdown_value_1) + GROUP BY day_start, + breakdown_value_1 + ORDER BY day_start ASC, breakdown_value ASC) + GROUP BY breakdown_value + ORDER BY if(has(breakdown_value, '$$_posthog_breakdown_other_$$'), 2, if(has(breakdown_value, '$$_posthog_breakdown_null_$$'), 1, 0)) ASC, arraySum(total) DESC, breakdown_value ASC) + WHERE arrayExists(x -> isNotNull(x), breakdown_value) + GROUP BY breakdown_value + ORDER BY if(has(breakdown_value, '$$_posthog_breakdown_other_$$'), 2, if(has(breakdown_value, '$$_posthog_breakdown_null_$$'), 1, 0)) ASC, arraySum(total) DESC, breakdown_value ASC + LIMIT 50000 SETTINGS readonly=2, + max_execution_time=60, + allow_experimental_object_type=1, + format_csv_allow_double_quotes=0, + max_ast_elements=4000000, + max_expanded_ast_elements=4000000, + max_bytes_before_external_group_by=0 ''' # --- # name: TestTrends.test_dau_with_breakdown_filtering_with_sampling.4 ''' - /* celery:posthog.tasks.tasks.sync_insight_caching_state */ - SELECT team_id, - date_diff('second', max(timestamp), now()) AS age - FROM events - WHERE timestamp > date_sub(DAY, 3, now()) - AND timestamp < now() - GROUP BY team_id - ORDER BY age; + SELECT groupArray(1)(date)[1] AS date, + arrayFold((acc, x) -> arrayMap(i -> plus(acc[i], x[i]), range(1, plus(length(date), 1))), groupArray(ifNull(total, 0)), arrayWithConstant(length(date), reinterpretAsFloat64(0))) AS total, + arrayMap(i -> if(ifNull(ifNull(greaterOrEquals(row_number, 25), 0), 0), '$$_posthog_breakdown_other_$$', i), breakdown_value) AS breakdown_value + FROM + (SELECT arrayMap(number -> plus(toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-28 00:00:00', 6, 'UTC'))), toIntervalDay(number)), range(0, plus(coalesce(dateDiff('day', toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-28 00:00:00', 6, 'UTC'))), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-04 23:59:59', 6, 'UTC'))))), 1))) AS date, + arrayMap(_match_date -> arraySum(arraySlice(groupArray(ifNull(count, 0)), indexOf(groupArray(day_start) AS _days_for_count, _match_date) AS _index, plus(minus(arrayLastIndex(x -> ifNull(equals(x, _match_date), isNull(x) + and isNull(_match_date)), _days_for_count), _index), 1))), date) AS total, + breakdown_value AS breakdown_value, + rowNumberInAllBlocks() AS row_number + FROM + (SELECT sum(total) AS count, + day_start AS day_start, + [ifNull(toString(breakdown_value_1), '$$_posthog_breakdown_null_$$')] AS breakdown_value + FROM + (SELECT count(DISTINCT if(not(empty(e__override.distinct_id)), e__override.person_id, e.person_id)) AS total, + toStartOfDay(toTimeZone(e.timestamp, 'UTC')) AS day_start, + ifNull(nullIf(toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', '')), ''), '$$_posthog_breakdown_null_$$') AS breakdown_value_1 + FROM events AS e SAMPLE 1.0 + LEFT OUTER JOIN + (SELECT argMax(person_distinct_id_overrides.person_id, person_distinct_id_overrides.version) AS person_id, + person_distinct_id_overrides.distinct_id AS distinct_id + FROM person_distinct_id_overrides + WHERE equals(person_distinct_id_overrides.team_id, 2) + GROUP BY person_distinct_id_overrides.distinct_id + HAVING ifNull(equals(argMax(person_distinct_id_overrides.is_deleted, person_distinct_id_overrides.version), 0), 0) SETTINGS optimize_aggregation_in_order=1) AS e__override ON equals(e.distinct_id, e__override.distinct_id) + WHERE and(equals(e.team_id, 2), greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-28 00:00:00', 6, 'UTC')))), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-04 23:59:59', 6, 'UTC'))), equals(e.event, 'sign up')) + GROUP BY day_start, + breakdown_value_1) + GROUP BY day_start, + breakdown_value_1 + ORDER BY day_start ASC, breakdown_value ASC) + GROUP BY breakdown_value + ORDER BY if(has(breakdown_value, '$$_posthog_breakdown_other_$$'), 2, if(has(breakdown_value, '$$_posthog_breakdown_null_$$'), 1, 0)) ASC, arraySum(total) DESC, breakdown_value ASC) + WHERE arrayExists(x -> isNotNull(x), breakdown_value) + GROUP BY breakdown_value + ORDER BY if(has(breakdown_value, '$$_posthog_breakdown_other_$$'), 2, if(has(breakdown_value, '$$_posthog_breakdown_null_$$'), 1, 0)) ASC, arraySum(total) DESC, breakdown_value ASC + LIMIT 50000 SETTINGS readonly=2, + max_execution_time=60, + allow_experimental_object_type=1, + format_csv_allow_double_quotes=0, + max_ast_elements=4000000, + max_expanded_ast_elements=4000000, + max_bytes_before_external_group_by=0 ''' # --- # name: TestTrends.test_dau_with_breakdown_filtering_with_sampling.5 diff --git a/posthog/migrations/0499_hog_function_type.py b/posthog/migrations/0499_hog_function_type.py new file mode 100644 index 0000000000000..07e05b7185df7 --- /dev/null +++ b/posthog/migrations/0499_hog_function_type.py @@ -0,0 +1,31 @@ +# Generated by Django 4.2.15 on 2024-10-24 11:05 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("posthog", "0498_errortrackingissuefingerprint_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="hogfunction", + name="type", + field=models.CharField( + blank=True, + choices=[ + ("destination", "Destination"), + ("email", "Email"), + ("sms", "Sms"), + ("push", "Push"), + ("activity", "Activity"), + ("alert", "Alert"), + ("broadcast", "Broadcast"), + ], + max_length=24, + null=True, + ), + ), + migrations.RunSQL("UPDATE posthog_hogfunction SET type = 'destination' WHERE type IS NULL", "SELECT 1"), + ] diff --git a/posthog/models/hog_functions/hog_function.py b/posthog/models/hog_functions/hog_function.py index d9922a83d47f8..c2a1dfa63b042 100644 --- a/posthog/models/hog_functions/hog_function.py +++ b/posthog/models/hog_functions/hog_function.py @@ -31,6 +31,19 @@ class HogFunctionState(enum.Enum): DISABLED_PERMANENTLY = 4 +class HogFunctionType(models.TextChoices): + DESTINATION = "destination" + EMAIL = "email" + SMS = "sms" + PUSH = "push" + ACTIVITY = "activity" + ALERT = "alert" + BROADCAST = "broadcast" + + +TYPES_THAT_RELOAD_PLUGIN_SERVER = (HogFunctionType.DESTINATION, HogFunctionType.EMAIL) + + class HogFunction(UUIDModel): team = models.ForeignKey("Team", on_delete=models.CASCADE) name = models.CharField(max_length=400, null=True, blank=True) @@ -40,6 +53,7 @@ class HogFunction(UUIDModel): deleted = models.BooleanField(default=False) updated_at = models.DateTimeField(auto_now=True) enabled = models.BooleanField(default=False) + type = models.CharField(max_length=24, choices=HogFunctionType.choices, null=True, blank=True) icon_url = models.TextField(null=True, blank=True) hog = models.TextField() @@ -69,11 +83,6 @@ def filter_action_ids(self) -> list[int]: _status: Optional[dict] = None - @property - def type(self) -> str: - # Used in activity logs - return "destination" - @property def status(self) -> dict: if not self.enabled: @@ -145,7 +154,8 @@ def __str__(self): @receiver(post_save, sender=HogFunction) def hog_function_saved(sender, instance: HogFunction, created, **kwargs): - reload_hog_functions_on_workers(team_id=instance.team_id, hog_function_ids=[str(instance.id)]) + if instance.type is None or instance.type in TYPES_THAT_RELOAD_PLUGIN_SERVER: + reload_hog_functions_on_workers(team_id=instance.team_id, hog_function_ids=[str(instance.id)]) @receiver(post_save, sender=Action)