diff --git a/src/ingestion/process-event.ts b/src/ingestion/process-event.ts index e9554a8d..32db65af 100644 --- a/src/ingestion/process-event.ts +++ b/src/ingestion/process-event.ts @@ -24,7 +24,7 @@ import { } from '../types' import { castTimestampOrNow, UUID, UUIDT } from '../utils' import { KAFKA_EVENTS, KAFKA_SESSION_RECORDING_EVENTS } from './topics' -import { elementsToString, sanitizeEventName, timeoutGuard } from './utils' +import { elementsToString, personInitialAndUTMProperties, sanitizeEventName, timeoutGuard } from './utils' export class EventsProcessor { pluginsServer: PluginsServer @@ -326,7 +326,6 @@ export class EventsProcessor { sentAt: DateTime | null ): Promise { event = sanitizeEventName(event) - const elements: Record[] | undefined = properties['$elements'] let elementsList: Element[] = [] if (elements && elements.length) { @@ -371,6 +370,8 @@ export class EventsProcessor { } catch {} } + properties = personInitialAndUTMProperties(properties) + if (properties['$set'] || properties['$set_once']) { await this.updatePersonProperties( teamId, diff --git a/src/ingestion/utils.ts b/src/ingestion/utils.ts index a8bfcb64..ec184d18 100644 --- a/src/ingestion/utils.ts +++ b/src/ingestion/utils.ts @@ -1,3 +1,4 @@ +import { Properties } from '@posthog/plugin-scaffold' import * as Sentry from '@sentry/node' import crypto from 'crypto' @@ -163,3 +164,21 @@ export function timeoutGuard(message: string, timeout = defaultConfig.TASK_TIMEO Sentry.captureMessage(message) }, timeout) } + +const campaignParams = new Set(['utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term']) +const initialParams = new Set(['$browser', '$browser_version', '$current_url', '$os', '$referring_domain', '$referrer']) +const combinedParams = new Set([...campaignParams, ...initialParams]) + +/** If we get new UTM params, make sure we set those **/ +export function personInitialAndUTMProperties(properties: Properties): Properties { + const propertiesCopy = { ...properties } + const maybeSet = Object.entries(properties).filter(([key, value]) => campaignParams.has(key)) + const maybeSetInitial = Object.entries(properties) + .filter(([key, value]) => combinedParams.has(key)) + .map(([key, value]) => [`$initial_${key.replace('$', '')}`, value]) + if (Object.keys(maybeSet).length > 0) { + propertiesCopy.$set = { ...(properties.$set || {}), ...Object.fromEntries(maybeSet) } + propertiesCopy.$set_once = { ...(properties.$set_once || {}), ...Object.fromEntries(maybeSetInitial) } + } + return propertiesCopy +} diff --git a/tests/shared/process-event.ts b/tests/shared/process-event.ts index 8c9763c0..02a74f58 100644 --- a/tests/shared/process-event.ts +++ b/tests/shared/process-event.ts @@ -217,6 +217,13 @@ export const createProcessEventTests = ( properties: { distinct_id: 2, token: team.api_token, + $browser: 'Chrome', + $current_url: 'https://test.com', + $os: 'Mac OS X', + $browser_version: 80, + $initial_referring_domain: 'https://google.com', + $initial_referrer_url: 'https://google.com/?q=posthog', + utm_medium: 'twitter', $elements: [ { tag_name: 'a', nth_child: 1, nth_of_type: 2, attr__class: 'btn btn-sm' }, { tag_name: 'div', nth_child: 1, nth_of_type: 2, $el_text: '💻' }, @@ -230,12 +237,23 @@ export const createProcessEventTests = ( ) if (database === 'clickhouse') { - expect(queryCounter).toBe(9) + expect(queryCounter).toBe(11) } else if (database === 'postgresql') { - expect(queryCounter).toBe(12) + expect(queryCounter).toBe(14) } + let persons = await server.db.fetchPersons() + expect(persons[0].properties).toEqual({ + $initial_browser: 'Chrome', + $initial_browser_version: 80, + $initial_utm_medium: 'twitter', + $initial_current_url: 'https://test.com', + $initial_os: 'Mac OS X', + utm_medium: 'twitter', + }) + // capture a second time to verify e.g. event_names is not ['$autocapture', '$autocapture'] + // Also pass new utm params in to override await processEvent( '2', '127.0.0.1', @@ -245,6 +263,7 @@ export const createProcessEventTests = ( properties: { distinct_id: 2, token: team.api_token, + utm_medium: 'instagram', $elements: [ { tag_name: 'a', nth_child: 1, nth_of_type: 2, attr__class: 'btn btn-sm' }, { tag_name: 'div', nth_child: 1, nth_of_type: 2, $el_text: '💻' }, @@ -258,9 +277,17 @@ export const createProcessEventTests = ( ) const events = await server.db.fetchEvents() - const persons = await server.db.fetchPersons() + persons = await server.db.fetchPersons() expect(events.length).toEqual(2) expect(persons.length).toEqual(1) + expect(persons[0].properties).toEqual({ + $initial_browser: 'Chrome', + $initial_browser_version: 80, + $initial_utm_medium: 'twitter', + $initial_current_url: 'https://test.com', + $initial_os: 'Mac OS X', + utm_medium: 'instagram', + }) const [person] = persons const distinctIds = await server.db.fetchDistinctIdValues(person) @@ -285,10 +312,52 @@ export const createProcessEventTests = ( team = await getFirstTeam(server) expect(team.event_names).toEqual(['$autocapture']) expect(team.event_names_with_usage).toEqual([{ event: '$autocapture', volume: null, usage_count: null }]) - expect(team.event_properties).toEqual(['distinct_id', 'token', '$ip']) + expect(team.event_properties).toEqual([ + 'distinct_id', + 'token', + '$browser', + '$current_url', + '$os', + '$browser_version', + '$initial_referring_domain', + '$initial_referrer_url', + 'utm_medium', + '$ip', + ]) expect(team.event_properties_with_usage).toEqual([ { key: 'distinct_id', usage_count: null, volume: null }, { key: 'token', usage_count: null, volume: null }, + { key: '$browser', usage_count: null, volume: null }, + { + key: '$current_url', + usage_count: null, + volume: null, + }, + { + key: '$os', + usage_count: null, + volume: null, + }, + { + key: '$browser_version', + usage_count: null, + volume: null, + }, + { + key: '$initial_referring_domain', + usage_count: null, + volume: null, + }, + { + key: '$initial_referrer_url', + usage_count: null, + volume: null, + }, + { + key: 'utm_medium', + usage_count: null, + volume: null, + }, { key: '$ip', usage_count: null, volume: null }, ]) })