0
0
mirror of https://github.com/PostHog/posthog.git synced 2024-11-25 11:17:50 +01:00
posthog/plugin-server/tests/main/process-event.test.ts

2598 lines
81 KiB
TypeScript

/*
This file contains a bunch of legacy E2E tests mixed with unit tests.
Rather than add tests here, consider improving event-pipeline-integration test suite or adding
unit tests to appropriate classes/functions.
*/
import { Properties } from '@posthog/plugin-scaffold'
import { PluginEvent } from '@posthog/plugin-scaffold/src/types'
import * as IORedis from 'ioredis'
import { DateTime } from 'luxon'
import { KAFKA_EVENTS_PLUGIN_INGESTION } from '../../src/config/kafka-topics'
import {
ClickHouseEvent,
Database,
Hub,
LogLevel,
Person,
PluginsServerConfig,
PropertyDefinitionTypeEnum,
Team,
} from '../../src/types'
import { createHub } from '../../src/utils/db/hub'
import { PostgresUse } from '../../src/utils/db/postgres'
import { personInitialAndUTMProperties } from '../../src/utils/db/utils'
import { posthog } from '../../src/utils/posthog'
import { UUIDT } from '../../src/utils/utils'
import { EventPipelineRunner } from '../../src/worker/ingestion/event-pipeline/runner'
import { EventsProcessor } from '../../src/worker/ingestion/process-event'
import { delayUntilEventIngested, resetTestDatabaseClickhouse } from '../helpers/clickhouse'
import { resetKafka } from '../helpers/kafka'
import { createUserTeamAndOrganization, getFirstTeam, getTeams, resetTestDatabase } from '../helpers/sql'
jest.mock('../../src/utils/status')
jest.setTimeout(600000) // 600 sec timeout.
export async function createPerson(
server: Hub,
team: Team,
distinctIds: string[],
properties: Record<string, any> = {}
): Promise<Person> {
return server.db.createPerson(
DateTime.utc(),
properties,
{},
{},
team.id,
null,
false,
new UUIDT().toString(),
distinctIds
)
}
export type ReturnWithHub = { hub?: Hub; closeHub?: () => Promise<void> }
type EventsByPerson = [string[], string[]]
export const getEventsByPerson = async (hub: Hub): Promise<EventsByPerson[]> => {
// Helper function to retrieve events paired with their associated distinct
// ids
const persons = await hub.db.fetchPersons()
const events = await hub.db.fetchEvents()
return await Promise.all(
persons
.sort((p1, p2) => p1.created_at.diff(p2.created_at).toMillis())
.map(async (person) => {
const distinctIds = await hub.db.fetchDistinctIdValues(person)
return [
distinctIds,
(events as ClickHouseEvent[])
.filter((event) => distinctIds.includes(event.distinct_id))
.sort((e1, e2) => e1.timestamp.diff(e2.timestamp).toMillis())
.map((event) => event.event),
] as EventsByPerson
})
)
}
const TEST_CONFIG: Partial<PluginsServerConfig> = {
LOG_LEVEL: LogLevel.Log,
KAFKA_CONSUMPTION_TOPIC: KAFKA_EVENTS_PLUGIN_INGESTION,
}
let processEventCounter = 0
let mockClientEventCounter = 0
let team: Team
let hub: Hub
let closeHub: () => Promise<void>
let redis: IORedis.Redis
let eventsProcessor: EventsProcessor
let now = DateTime.utc()
async function createTestHub(additionalProps?: Record<string, any>): Promise<[Hub, () => Promise<void>]> {
const [hub, closeHub] = await createHub({
...TEST_CONFIG,
...(additionalProps ?? {}),
})
redis = await hub.redisPool.acquire()
return [hub, closeHub]
}
async function processEvent(
distinctId: string,
ip: string | null,
_siteUrl: string,
data: Partial<PluginEvent>,
teamId: number,
timestamp: DateTime,
eventUuid: string
): Promise<void> {
const pluginEvent: PluginEvent = {
distinct_id: distinctId,
site_url: _siteUrl,
team_id: teamId,
timestamp: timestamp.toUTC().toISO(),
now: timestamp.toUTC().toISO(),
ip: ip,
uuid: eventUuid,
...data,
} as any as PluginEvent
const runner = new EventPipelineRunner(hub, pluginEvent)
await runner.runEventPipeline(pluginEvent)
await delayUntilEventIngested(() => hub.db.fetchEvents(), ++processEventCounter)
}
// Simple client used to simulate sending events
// Use state object to simulate stateful clients that keep track of old
// distinct id, starting with an anonymous one. I've taken posthog-js as
// the reference implementation.
let state = { currentDistinctId: 'anonymous_id' }
beforeAll(async () => {
await resetKafka(TEST_CONFIG)
})
beforeEach(async () => {
const testCode = `
function processEvent (event, meta) {
event.properties["somewhere"] = "over the rainbow";
return event
}
`
await resetTestDatabase(testCode, TEST_CONFIG)
await resetTestDatabaseClickhouse(TEST_CONFIG)
;[hub, closeHub] = await createTestHub()
eventsProcessor = new EventsProcessor(hub)
processEventCounter = 0
mockClientEventCounter = 0
team = await getFirstTeam(hub)
now = DateTime.utc()
// clear the webhook redis cache
const hooksCacheKey = `@posthog/plugin-server/hooks/${team.id}`
await redis.del(hooksCacheKey)
// Always start with an anonymous state
state = { currentDistinctId: 'anonymous_id' }
})
afterEach(async () => {
await hub.redisPool.release(redis)
await closeHub?.()
})
const capture = async (hub: Hub, eventName: string, properties: any = {}) => {
const event = {
event: eventName,
distinct_id: properties.distinct_id ?? state.currentDistinctId,
properties: properties,
now: new Date().toISOString(),
sent_at: new Date().toISOString(),
ip: '127.0.0.1',
site_url: 'https://posthog.com',
team_id: team.id,
uuid: new UUIDT().toString(),
}
const runner = new EventPipelineRunner(hub, event)
await runner.runEventPipeline(event)
await delayUntilEventIngested(() => hub.db.fetchEvents(), ++mockClientEventCounter)
}
const identify = async (hub: Hub, distinctId: string) => {
// Update currentDistinctId state immediately, as the event will be
// dispatch asynchronously
const currentDistinctId = state.currentDistinctId
state.currentDistinctId = distinctId
await capture(hub, '$identify', {
// posthog-js will send the previous distinct id as
// $anon_distinct_id
$anon_distinct_id: currentDistinctId,
distinct_id: distinctId,
})
}
const alias = async (hub: Hub, alias: string, distinctId: string) => {
await capture(hub, '$create_alias', { alias, disinct_id: distinctId })
}
test('merge people', async () => {
const p0 = await createPerson(hub, team, ['person_0'], { $os: 'Microsoft' })
await delayUntilEventIngested(() => hub.db.fetchPersons(Database.ClickHouse), 1)
await hub.db.updatePersonDeprecated(p0, { created_at: DateTime.fromISO('2020-01-01T00:00:00Z') })
const p1 = await createPerson(hub, team, ['person_1'], { $os: 'Chrome', $browser: 'Chrome' })
await delayUntilEventIngested(() => hub.db.fetchPersons(Database.ClickHouse), 2)
await hub.db.updatePersonDeprecated(p1, { created_at: DateTime.fromISO('2019-07-01T00:00:00Z') })
await processEvent(
'person_1',
'',
'',
{
event: 'user signed up',
properties: {},
} as any as PluginEvent,
team.id,
now,
new UUIDT().toString()
)
expect((await hub.db.fetchPersons()).length).toEqual(2)
await delayUntilEventIngested(() => hub.db.fetchPersons(Database.ClickHouse), 2)
const chPeople = await hub.db.fetchPersons(Database.ClickHouse)
expect(chPeople.length).toEqual(2)
await processEvent(
'person_0',
'',
'',
{
event: '$identify',
properties: { $anon_distinct_id: 'person_1' },
} as any as PluginEvent,
team.id,
now,
new UUIDT().toString()
)
await delayUntilEventIngested(async () =>
(await hub.db.fetchPersons(Database.ClickHouse)).length === 1 ? [1] : []
)
expect((await hub.db.fetchPersons(Database.ClickHouse)).length).toEqual(1)
const [person] = await hub.db.fetchPersons()
expect(person.properties).toEqual({ $os: 'Microsoft', $browser: 'Chrome' })
expect(await hub.db.fetchDistinctIdValues(person)).toEqual(['person_0', 'person_1'])
expect(person.created_at.toISO()).toEqual(DateTime.fromISO('2019-07-01T00:00:00Z').setZone('UTC').toISO())
})
test('capture new person', async () => {
await hub.db.postgres.query(
PostgresUse.COMMON_WRITE,
`UPDATE posthog_team
SET ingested_event = $1
WHERE id = $2`,
[true, team.id],
'testTag'
)
team = await getFirstTeam(hub)
expect(await hub.db.fetchEventDefinitions()).toEqual([])
expect(await hub.db.fetchPropertyDefinitions()).toEqual([])
const properties = personInitialAndUTMProperties({
distinct_id: 2,
token: team.api_token,
$browser: 'Chrome',
$current_url: 'https://test.com',
$os: 'Mac OS X',
$browser_version: '95',
$referring_domain: 'https://google.com',
$referrer: 'https://google.com/?q=posthog',
utm_medium: 'twitter',
gclid: 'GOOGLE ADS ID',
msclkid: 'BING ADS ID',
$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: '💻' },
],
})
const uuid = new UUIDT().toString()
await processEvent(
'2',
'127.0.0.1',
'',
{
event: '$autocapture',
properties,
} as any as PluginEvent,
team.id,
now,
uuid
)
let persons = await hub.db.fetchPersons()
expect(persons[0].version).toEqual(0)
expect(persons[0].created_at).toEqual(now)
let expectedProps: Record<string, any> = {
$creator_event_uuid: uuid,
$initial_browser: 'Chrome',
$initial_browser_version: '95',
$initial_utm_medium: 'twitter',
$initial_current_url: 'https://test.com',
$initial_os: 'Mac OS X',
utm_medium: 'twitter',
$initial_gclid: 'GOOGLE ADS ID',
$initial_msclkid: 'BING ADS ID',
gclid: 'GOOGLE ADS ID',
msclkid: 'BING ADS ID',
$initial_referrer: 'https://google.com/?q=posthog',
$initial_referring_domain: 'https://google.com',
$browser: 'Chrome',
$browser_version: '95',
$current_url: 'https://test.com',
$os: 'Mac OS X',
$referrer: 'https://google.com/?q=posthog',
$referring_domain: 'https://google.com',
}
expect(persons[0].properties).toEqual(expectedProps)
await delayUntilEventIngested(() => hub.db.fetchEvents(), 1)
await delayUntilEventIngested(() => hub.db.fetchPersons(Database.ClickHouse), 1)
const chPeople = await hub.db.fetchPersons(Database.ClickHouse)
expect(chPeople.length).toEqual(1)
expect(JSON.parse(chPeople[0].properties)).toEqual(expectedProps)
expect(chPeople[0].created_at).toEqual(now.toFormat('yyyy-MM-dd HH:mm:ss.000'))
let events = await hub.db.fetchEvents()
expect(events[0].properties).toEqual({
$ip: '127.0.0.1',
$os: 'Mac OS X',
$set: {
utm_medium: 'twitter',
gclid: 'GOOGLE ADS ID',
msclkid: 'BING ADS ID',
$browser: 'Chrome',
$browser_version: '95',
$current_url: 'https://test.com',
$os: 'Mac OS X',
$referrer: 'https://google.com/?q=posthog',
$referring_domain: 'https://google.com',
},
token: 'THIS IS NOT A TOKEN FOR TEAM 2',
$browser: 'Chrome',
$set_once: {
$initial_os: 'Mac OS X',
$initial_browser: 'Chrome',
$initial_utm_medium: 'twitter',
$initial_current_url: 'https://test.com',
$initial_browser_version: '95',
$initial_gclid: 'GOOGLE ADS ID',
$initial_msclkid: 'BING ADS ID',
$initial_referrer: 'https://google.com/?q=posthog',
$initial_referring_domain: 'https://google.com',
},
utm_medium: 'twitter',
distinct_id: 2,
$current_url: 'https://test.com',
$browser_version: '95',
gclid: 'GOOGLE ADS ID',
msclkid: 'BING ADS ID',
$referrer: 'https://google.com/?q=posthog',
$referring_domain: 'https://google.com',
})
// 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',
'',
{
event: '$autocapture',
properties: personInitialAndUTMProperties({
distinct_id: 2,
token: team.api_token,
utm_medium: 'instagram',
$current_url: 'https://test.com/pricing',
$browser_version: 80,
$browser: 'Firefox',
$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: '💻' },
],
}),
} as any as PluginEvent,
team.id,
DateTime.now(),
new UUIDT().toString()
)
events = await hub.db.fetchEvents()
persons = await hub.db.fetchPersons()
expect(events.length).toEqual(2)
expect(persons.length).toEqual(1)
expect(persons[0].version).toEqual(1)
expectedProps = {
$creator_event_uuid: uuid,
$initial_browser: 'Chrome',
$initial_browser_version: '95',
$initial_utm_medium: 'twitter',
$initial_current_url: 'https://test.com',
$initial_os: 'Mac OS X',
utm_medium: 'instagram',
$initial_gclid: 'GOOGLE ADS ID',
$initial_msclkid: 'BING ADS ID',
gclid: 'GOOGLE ADS ID',
msclkid: 'BING ADS ID',
$initial_referrer: 'https://google.com/?q=posthog',
$initial_referring_domain: 'https://google.com',
$browser: 'Firefox',
$browser_version: 80,
$current_url: 'https://test.com/pricing',
$os: 'Mac OS X',
$referrer: 'https://google.com/?q=posthog',
$referring_domain: 'https://google.com',
}
expect(persons[0].properties).toEqual(expectedProps)
const chPeople2 = await delayUntilEventIngested(async () =>
(
await hub.db.fetchPersons(Database.ClickHouse)
).filter((p) => p && JSON.parse(p.properties).utm_medium == 'instagram')
)
expect(chPeople2.length).toEqual(1)
expect(JSON.parse(chPeople2[0].properties)).toEqual(expectedProps)
expect(events[1].properties.$set).toEqual({
utm_medium: 'instagram',
$browser: 'Firefox',
$browser_version: 80,
$current_url: 'https://test.com/pricing',
})
expect(events[1].properties.$set_once).toEqual({
$initial_browser: 'Firefox',
$initial_browser_version: 80,
$initial_utm_medium: 'instagram',
$initial_current_url: 'https://test.com/pricing',
})
const [person] = persons
const distinctIds = await hub.db.fetchDistinctIdValues(person)
const [event] = events as ClickHouseEvent[]
expect(event.distinct_id).toEqual('2')
expect(distinctIds).toEqual(['2'])
expect(event.event).toEqual('$autocapture')
const elements = event.elements_chain!
expect(elements[0].tag_name).toEqual('a')
expect(elements[0].attr_class).toEqual(['btn', 'btn-sm'])
expect(elements[1].order).toEqual(1)
expect(elements[1].text).toEqual('💻')
// Don't update any props, set and set_once should be what was sent
await processEvent(
'2',
'127.0.0.1',
'',
{
event: '$autocapture',
properties: personInitialAndUTMProperties({
distinct_id: 2,
token: team.api_token,
utm_medium: 'instagram',
$current_url: 'https://test.com/pricing',
$browser: 'Firefox',
$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: '💻' },
],
}),
} as any as PluginEvent,
team.id,
DateTime.now(),
new UUIDT().toString()
)
events = await hub.db.fetchEvents()
persons = await hub.db.fetchPersons()
expect(events.length).toEqual(3)
expect(persons.length).toEqual(1)
// no new props, person wasn't updated with old fn, was because of timestamps update with new fn
expect(persons[0].version).toEqual(1)
expect(events[2].properties.$set).toEqual({
$browser: 'Firefox',
$current_url: 'https://test.com/pricing',
utm_medium: 'instagram',
})
expect(events[2].properties.$set_once).toEqual({
$initial_browser: 'Firefox',
$initial_utm_medium: 'instagram',
$initial_current_url: 'https://test.com/pricing',
})
// check that person properties didn't change
expect(persons[0].properties).toEqual(expectedProps)
const chPeople3 = await hub.db.fetchPersons(Database.ClickHouse)
expect(chPeople3.length).toEqual(1)
expect(JSON.parse(chPeople3[0].properties)).toEqual(expectedProps)
team = await getFirstTeam(hub)
expect(await hub.db.fetchEventDefinitions()).toEqual([
{
id: expect.any(String),
name: '$autocapture',
query_usage_30_day: null,
team_id: 2,
volume_30_day: null,
created_at: expect.any(String),
last_seen_at: expect.any(String),
},
])
const received = await hub.db.fetchPropertyDefinitions()
const expected = [
{
id: expect.any(String),
is_numerical: true,
name: 'distinct_id',
property_type: 'Numeric',
property_type_format: null,
query_usage_30_day: null,
team_id: 2,
type: 1,
group_type_index: null,
volume_30_day: null,
},
{
id: expect.any(String),
is_numerical: false,
name: 'token',
property_type: 'String',
property_type_format: null,
query_usage_30_day: null,
team_id: 2,
type: 1,
group_type_index: null,
volume_30_day: null,
},
{
id: expect.any(String),
is_numerical: false,
name: '$browser',
property_type: 'String',
property_type_format: null,
query_usage_30_day: null,
team_id: 2,
type: 1,
group_type_index: null,
volume_30_day: null,
},
{
id: expect.any(String),
is_numerical: false,
name: '$current_url',
property_type: 'String',
property_type_format: null,
query_usage_30_day: null,
team_id: 2,
type: 1,
group_type_index: null,
volume_30_day: null,
},
{
id: expect.any(String),
is_numerical: false,
name: '$os',
property_type: 'String',
property_type_format: null,
query_usage_30_day: null,
team_id: 2,
type: 1,
group_type_index: null,
volume_30_day: null,
},
{
id: expect.any(String),
is_numerical: false,
name: '$browser_version',
property_type: 'String',
property_type_format: null,
query_usage_30_day: null,
team_id: 2,
type: 1,
group_type_index: null,
volume_30_day: null,
},
{
id: expect.any(String),
is_numerical: false,
name: '$referring_domain',
property_type: 'String',
property_type_format: null,
query_usage_30_day: null,
team_id: 2,
type: 1,
group_type_index: null,
volume_30_day: null,
},
{
id: expect.any(String),
is_numerical: false,
name: '$referrer',
property_type: 'String',
property_type_format: null,
query_usage_30_day: null,
team_id: 2,
type: 1,
group_type_index: null,
volume_30_day: null,
},
{
id: expect.any(String),
is_numerical: false,
name: 'utm_medium',
property_type: 'String',
property_type_format: null,
query_usage_30_day: null,
team_id: 2,
type: 1,
group_type_index: null,
volume_30_day: null,
},
{
id: expect.any(String),
is_numerical: false,
name: 'gclid',
property_type: 'String',
property_type_format: null,
query_usage_30_day: null,
team_id: 2,
type: 1,
group_type_index: null,
volume_30_day: null,
},
{
id: expect.any(String),
is_numerical: false,
name: 'msclkid',
property_type: 'String',
property_type_format: null,
query_usage_30_day: null,
team_id: 2,
type: 1,
group_type_index: null,
volume_30_day: null,
},
{
id: expect.any(String),
is_numerical: false,
name: '$ip',
property_type: 'String',
property_type_format: null,
query_usage_30_day: null,
team_id: 2,
type: 1,
group_type_index: null,
volume_30_day: null,
},
{
id: expect.any(String),
is_numerical: false,
name: 'utm_medium',
property_type: 'String',
property_type_format: null,
query_usage_30_day: null,
team_id: 2,
type: 2,
group_type_index: null,
volume_30_day: null,
},
{
id: expect.any(String),
is_numerical: false,
name: 'gclid',
property_type: 'String',
property_type_format: null,
query_usage_30_day: null,
team_id: 2,
type: 2,
group_type_index: null,
volume_30_day: null,
},
{
id: expect.any(String),
is_numerical: false,
name: 'msclkid',
property_type: 'String',
property_type_format: null,
query_usage_30_day: null,
team_id: 2,
type: 2,
group_type_index: null,
volume_30_day: null,
},
{
id: expect.any(String),
is_numerical: false,
name: '$initial_browser',
property_type: 'String',
property_type_format: null,
query_usage_30_day: null,
team_id: 2,
type: 2,
group_type_index: null,
volume_30_day: null,
},
{
id: expect.any(String),
is_numerical: false,
name: '$initial_current_url',
property_type: 'String',
property_type_format: null,
query_usage_30_day: null,
team_id: 2,
type: 2,
group_type_index: null,
volume_30_day: null,
},
{
id: expect.any(String),
is_numerical: false,
name: '$initial_os',
property_type: 'String',
property_type_format: null,
query_usage_30_day: null,
team_id: 2,
type: 2,
group_type_index: null,
volume_30_day: null,
},
{
id: expect.any(String),
is_numerical: false,
name: '$initial_browser_version',
property_type: 'String',
property_type_format: null,
query_usage_30_day: null,
team_id: 2,
type: 2,
group_type_index: null,
volume_30_day: null,
},
{
id: expect.any(String),
is_numerical: false,
name: '$initial_referring_domain',
property_type: 'String',
property_type_format: null,
query_usage_30_day: null,
team_id: 2,
type: 2,
group_type_index: null,
volume_30_day: null,
},
{
id: expect.any(String),
is_numerical: false,
name: '$initial_referrer',
property_type: 'String',
property_type_format: null,
query_usage_30_day: null,
team_id: 2,
type: 2,
group_type_index: null,
volume_30_day: null,
},
{
id: expect.any(String),
is_numerical: false,
name: '$initial_utm_medium',
property_type: 'String',
property_type_format: null,
query_usage_30_day: null,
team_id: 2,
type: 2,
group_type_index: null,
volume_30_day: null,
},
{
id: expect.any(String),
is_numerical: false,
name: '$initial_gclid',
property_type: 'String',
property_type_format: null,
query_usage_30_day: null,
team_id: 2,
type: 2,
group_type_index: null,
volume_30_day: null,
},
{
id: expect.any(String),
is_numerical: false,
name: '$initial_msclkid',
property_type: 'String',
property_type_format: null,
query_usage_30_day: null,
team_id: 2,
type: 2,
group_type_index: null,
volume_30_day: null,
},
]
for (const element of expected) {
// Looping in an array to make it easier to debug
expect(received).toEqual(expect.arrayContaining([element]))
}
})
test('capture bad team', async () => {
await expect(
eventsProcessor.processEvent(
'asdfasdfasdf',
{
event: '$pageview',
properties: { distinct_id: 'asdfasdfasdf', token: team.api_token },
} as any as PluginEvent,
1337,
now,
new UUIDT().toString()
)
).rejects.toThrowError("No team found with ID 1337. Can't ingest event.")
})
test('capture no element', async () => {
await createPerson(hub, team, ['asdfasdfasdf'])
await processEvent(
'asdfasdfasdf',
'',
'',
{
event: '$pageview',
properties: { distinct_id: 'asdfasdfasdf', token: team.api_token },
} as any as PluginEvent,
team.id,
now,
new UUIDT().toString()
)
expect(await hub.db.fetchDistinctIdValues((await hub.db.fetchPersons())[0])).toEqual(['asdfasdfasdf'])
const [event] = await hub.db.fetchEvents()
expect(event.event).toBe('$pageview')
})
test('ip none', async () => {
await createPerson(hub, team, ['asdfasdfasdf'])
await processEvent(
'asdfasdfasdf',
null,
'',
{
event: '$pageview',
properties: { distinct_id: 'asdfasdfasdf', token: team.api_token },
} as any as PluginEvent,
team.id,
now,
new UUIDT().toString()
)
const [event] = await hub.db.fetchEvents()
expect(Object.keys(event.properties)).not.toContain('$ip')
})
test('ip capture', async () => {
await createPerson(hub, team, ['asdfasdfasdf'])
await processEvent(
'asdfasdfasdf',
'11.12.13.14',
'',
{
event: '$pageview',
properties: { distinct_id: 'asdfasdfasdf', token: team.api_token },
} as any as PluginEvent,
team.id,
now,
new UUIDT().toString()
)
const [event] = await hub.db.fetchEvents()
expect(event.properties['$ip']).toBe('11.12.13.14')
})
test('ip override', async () => {
await createPerson(hub, team, ['asdfasdfasdf'])
await processEvent(
'asdfasdfasdf',
'11.12.13.14',
'',
{
event: '$pageview',
properties: { $ip: '1.0.0.1', distinct_id: 'asdfasdfasdf', token: team.api_token },
} as any as PluginEvent,
team.id,
now,
new UUIDT().toString()
)
const [event] = await hub.db.fetchEvents()
expect(event.properties['$ip']).toBe('1.0.0.1')
})
test('anonymized ip capture', async () => {
await hub.db.postgres.query(
PostgresUse.COMMON_WRITE,
'update posthog_team set anonymize_ips = $1',
[true],
'testTag'
)
await createPerson(hub, team, ['asdfasdfasdf'])
await processEvent(
'asdfasdfasdf',
'11.12.13.14',
'',
{
event: '$pageview',
properties: { distinct_id: 'asdfasdfasdf', token: team.api_token },
} as any as PluginEvent,
team.id,
now,
new UUIDT().toString()
)
const [event] = await hub.db.fetchEvents()
expect(event.properties['$ip']).not.toBeDefined()
})
test('merge_dangerously', async () => {
await createPerson(hub, team, ['old_distinct_id'])
await processEvent(
'new_distinct_id',
'',
'',
{
event: '$merge_dangerously',
properties: { distinct_id: 'new_distinct_id', token: team.api_token, alias: 'old_distinct_id' },
} as any as PluginEvent,
team.id,
now,
new UUIDT().toString()
)
expect((await hub.db.fetchEvents()).length).toBe(1)
expect(await hub.db.fetchDistinctIdValues((await hub.db.fetchPersons())[0])).toEqual([
'old_distinct_id',
'new_distinct_id',
])
})
test('alias', async () => {
await createPerson(hub, team, ['old_distinct_id'])
await processEvent(
'new_distinct_id',
'',
'',
{
event: '$create_alias',
properties: { distinct_id: 'new_distinct_id', token: team.api_token, alias: 'old_distinct_id' },
} as any as PluginEvent,
team.id,
now,
new UUIDT().toString()
)
expect((await hub.db.fetchEvents()).length).toBe(1)
expect(await hub.db.fetchDistinctIdValues((await hub.db.fetchPersons())[0])).toEqual([
'old_distinct_id',
'new_distinct_id',
])
})
test('alias reverse', async () => {
await createPerson(hub, team, ['old_distinct_id'])
await processEvent(
'old_distinct_id',
'',
'',
{
event: '$create_alias',
properties: { distinct_id: 'old_distinct_id', token: team.api_token, alias: 'new_distinct_id' },
} as any as PluginEvent,
team.id,
now,
new UUIDT().toString()
)
expect((await hub.db.fetchEvents()).length).toBe(1)
expect(await hub.db.fetchDistinctIdValues((await hub.db.fetchPersons())[0])).toEqual([
'old_distinct_id',
'new_distinct_id',
])
})
test('alias twice', async () => {
await createPerson(hub, team, ['old_distinct_id'])
await processEvent(
'new_distinct_id',
'',
'',
{
event: '$create_alias',
properties: { distinct_id: 'new_distinct_id', token: team.api_token, alias: 'old_distinct_id' },
} as any as PluginEvent,
team.id,
now,
new UUIDT().toString()
)
expect((await hub.db.fetchPersons()).length).toBe(1)
expect((await hub.db.fetchEvents()).length).toBe(1)
expect(await hub.db.fetchDistinctIdValues((await hub.db.fetchPersons())[0])).toEqual([
'old_distinct_id',
'new_distinct_id',
])
await createPerson(hub, team, ['old_distinct_id_2'])
expect((await hub.db.fetchPersons()).length).toBe(2)
await processEvent(
'new_distinct_id',
'',
'',
{
event: '$create_alias',
properties: { distinct_id: 'new_distinct_id', token: team.api_token, alias: 'old_distinct_id_2' },
} as any as PluginEvent,
team.id,
now,
new UUIDT().toString()
)
expect((await hub.db.fetchEvents()).length).toBe(2)
expect((await hub.db.fetchPersons()).length).toBe(1)
expect(await hub.db.fetchDistinctIdValues((await hub.db.fetchPersons())[0])).toEqual([
'old_distinct_id',
'new_distinct_id',
'old_distinct_id_2',
])
})
test('alias before person', async () => {
await processEvent(
'new_distinct_id',
'',
'',
{
event: '$create_alias',
properties: { distinct_id: 'new_distinct_id', token: team.api_token, alias: 'old_distinct_id' },
} as any as PluginEvent,
team.id,
now,
new UUIDT().toString()
)
expect((await hub.db.fetchEvents()).length).toBe(1)
expect((await hub.db.fetchPersons()).length).toBe(1)
expect(await hub.db.fetchDistinctIdValues((await hub.db.fetchPersons())[0])).toEqual([
'new_distinct_id',
'old_distinct_id',
])
})
test('alias both existing', async () => {
await createPerson(hub, team, ['old_distinct_id'])
await createPerson(hub, team, ['new_distinct_id'])
await processEvent(
'new_distinct_id',
'',
'',
{
event: '$create_alias',
properties: { distinct_id: 'new_distinct_id', token: team.api_token, alias: 'old_distinct_id' },
} as any as PluginEvent,
team.id,
now,
new UUIDT().toString()
)
expect((await hub.db.fetchEvents()).length).toBe(1)
expect(await hub.db.fetchDistinctIdValues((await hub.db.fetchPersons())[0])).toEqual([
'old_distinct_id',
'new_distinct_id',
])
})
test('alias merge properties', async () => {
await createPerson(hub, team, ['new_distinct_id'], {
key_on_both: 'new value both',
key_on_new: 'new value',
})
await createPerson(hub, team, ['old_distinct_id'], {
key_on_both: 'old value both',
key_on_old: 'old value',
})
await processEvent(
'new_distinct_id',
'',
'',
{
event: '$create_alias',
properties: { distinct_id: 'new_distinct_id', token: team.api_token, alias: 'old_distinct_id' },
} as any as PluginEvent,
team.id,
now,
new UUIDT().toString()
)
expect((await hub.db.fetchEvents()).length).toBe(1)
expect((await hub.db.fetchPersons()).length).toBe(1)
const [person] = await hub.db.fetchPersons()
expect((await hub.db.fetchDistinctIdValues(person)).sort()).toEqual(['new_distinct_id', 'old_distinct_id'])
expect(person.properties).toEqual({
key_on_both: 'new value both',
key_on_new: 'new value',
key_on_old: 'old value',
})
})
test('long htext', async () => {
await processEvent(
'new_distinct_id',
'',
'',
{
event: '$autocapture',
properties: {
distinct_id: 'new_distinct_id',
token: team.api_token,
$elements: [
{
tag_name: 'a',
$el_text: 'a'.repeat(2050),
attr__href: 'a'.repeat(2050),
nth_child: 1,
nth_of_type: 2,
attr__class: 'btn btn-sm',
},
],
},
} as any as PluginEvent,
team.id,
now,
new UUIDT().toString()
)
const [event] = await hub.db.fetchEvents()
const [element] = event.elements_chain!
expect(element.href?.length).toEqual(2048)
expect(element.text?.length).toEqual(400)
})
test('capture first team event', async () => {
await hub.db.postgres.query(
PostgresUse.COMMON_WRITE,
`UPDATE posthog_team
SET ingested_event = $1
WHERE id = $2`,
[false, team.id],
'testTag'
)
posthog.capture = jest.fn() as any
posthog.identify = jest.fn() as any
await processEvent(
'2',
'',
'',
{
event: '$autocapture',
properties: {
distinct_id: 1,
token: team.api_token,
$elements: [{ tag_name: 'a', nth_child: 1, nth_of_type: 2, attr__class: 'btn btn-sm' }],
},
} as any as PluginEvent,
team.id,
now,
new UUIDT().toString()
)
expect(posthog.capture).toHaveBeenCalledWith({
distinctId: 'plugin_test_user_distinct_id_1001',
event: 'first team event ingested',
properties: {
team: team.uuid,
},
groups: {
project: team.uuid,
organization: team.organization_id,
instance: 'unknown',
},
})
team = await getFirstTeam(hub)
expect(team.ingested_event).toEqual(true)
const [event] = await hub.db.fetchEvents()
const elements = event.elements_chain!
expect(elements.length).toEqual(1)
})
test('identify set', async () => {
await createPerson(hub, team, ['distinct_id1'])
const ts_before = now
const ts_after = now.plus({ hours: 1 })
await processEvent(
'distinct_id1',
'',
'',
{
event: '$identify',
properties: {
token: team.api_token,
distinct_id: 'distinct_id1',
$set: { a_prop: 'test-1', c_prop: 'test-1' },
},
} as any as PluginEvent,
team.id,
ts_before,
new UUIDT().toString()
)
expect((await hub.db.fetchEvents()).length).toBe(1)
const [event] = await hub.db.fetchEvents()
expect(event.properties['$set']).toEqual({ a_prop: 'test-1', c_prop: 'test-1' })
const [person] = await hub.db.fetchPersons()
expect(await hub.db.fetchDistinctIdValues(person)).toEqual(['distinct_id1'])
expect(person.properties).toEqual({ a_prop: 'test-1', c_prop: 'test-1' })
expect(person.is_identified).toEqual(false)
await processEvent(
'distinct_id1',
'',
'',
{
event: '$identify',
properties: {
token: team.api_token,
distinct_id: 'distinct_id1',
$set: { a_prop: 'test-2', b_prop: 'test-2b' },
},
} as any as PluginEvent,
team.id,
ts_after,
new UUIDT().toString()
)
expect((await hub.db.fetchEvents()).length).toBe(2)
const [person2] = await hub.db.fetchPersons()
expect(person2.properties).toEqual({ a_prop: 'test-2', b_prop: 'test-2b', c_prop: 'test-1' })
})
test('identify set_once', async () => {
await createPerson(hub, team, ['distinct_id1'])
await processEvent(
'distinct_id1',
'',
'',
{
event: '$identify',
properties: {
token: team.api_token,
distinct_id: 'distinct_id1',
$set_once: { a_prop: 'test-1', c_prop: 'test-1' },
},
} as any as PluginEvent,
team.id,
now,
new UUIDT().toString()
)
expect((await hub.db.fetchEvents()).length).toBe(1)
const [event] = await hub.db.fetchEvents()
expect(event.properties['$set_once']).toEqual({ a_prop: 'test-1', c_prop: 'test-1' })
const [person] = await hub.db.fetchPersons()
expect(await hub.db.fetchDistinctIdValues(person)).toEqual(['distinct_id1'])
expect(person.properties).toEqual({ a_prop: 'test-1', c_prop: 'test-1' })
expect(person.is_identified).toEqual(false)
await processEvent(
'distinct_id1',
'',
'',
{
event: '$identify',
properties: {
token: team.api_token,
distinct_id: 'distinct_id1',
$set_once: { a_prop: 'test-2', b_prop: 'test-2b' },
},
} as any as PluginEvent,
team.id,
now,
new UUIDT().toString()
)
expect((await hub.db.fetchEvents()).length).toBe(2)
const [person2] = await hub.db.fetchPersons()
expect(person2.properties).toEqual({ a_prop: 'test-1', b_prop: 'test-2b', c_prop: 'test-1' })
expect(person2.is_identified).toEqual(false)
})
test('identify with illegal (generic) id', async () => {
await createPerson(hub, team, ['im an anonymous id'])
expect((await hub.db.fetchPersons()).length).toBe(1)
const createPersonAndSendIdentify = async (distinctId: string): Promise<void> => {
await createPerson(hub, team, [distinctId])
await processEvent(
distinctId,
'',
'',
{
event: '$identify',
properties: {
token: team.api_token,
distinct_id: distinctId,
$anon_distinct_id: 'im an anonymous id',
},
} as any as PluginEvent,
team.id,
now,
new UUIDT().toString()
)
}
// try to merge, the merge should fail
await createPersonAndSendIdentify('distinctId')
expect((await hub.db.fetchPersons()).length).toBe(2)
await createPersonAndSendIdentify(' ')
expect((await hub.db.fetchPersons()).length).toBe(3)
await createPersonAndSendIdentify('NaN')
expect((await hub.db.fetchPersons()).length).toBe(4)
await createPersonAndSendIdentify('undefined')
expect((await hub.db.fetchPersons()).length).toBe(5)
await createPersonAndSendIdentify('None')
expect((await hub.db.fetchPersons()).length).toBe(6)
await createPersonAndSendIdentify('0')
expect((await hub.db.fetchPersons()).length).toBe(7)
// 'Nan' is an allowed id, so the merge should work
// as such, no extra person is created
await createPersonAndSendIdentify('Nan')
expect((await hub.db.fetchPersons()).length).toBe(7)
})
test('Alias with illegal (generic) id', async () => {
const legal_id = 'user123'
const illegal_id = 'null'
await createPerson(hub, team, [legal_id])
expect((await hub.db.fetchPersons()).length).toBe(1)
await processEvent(
illegal_id,
'',
'',
{
event: '$create_alias',
properties: {
token: team.api_token,
distinct_id: legal_id,
alias: illegal_id,
},
} as any as PluginEvent,
team.id,
now,
new UUIDT().toString()
)
// person with illegal id got created but not merged
expect((await hub.db.fetchPersons()).length).toBe(2)
})
test('distinct with anonymous_id', async () => {
await createPerson(hub, team, ['anonymous_id'])
await processEvent(
'new_distinct_id',
'',
'',
{
event: '$identify',
properties: {
$anon_distinct_id: 'anonymous_id',
token: team.api_token,
distinct_id: 'new_distinct_id',
$set: { a_prop: 'test' },
},
} as any as PluginEvent,
team.id,
now,
new UUIDT().toString()
)
expect((await hub.db.fetchEvents()).length).toBe(1)
const [event] = await hub.db.fetchEvents()
expect(event.properties['$set']).toEqual({ a_prop: 'test' })
const [person] = await hub.db.fetchPersons()
expect(await hub.db.fetchDistinctIdValues(person)).toEqual(['anonymous_id', 'new_distinct_id'])
expect(person.properties).toEqual({ a_prop: 'test' })
expect(person.is_identified).toEqual(true)
// check no errors as this call can happen multiple times
await processEvent(
'new_distinct_id',
'',
'',
{
event: '$identify',
properties: {
$anon_distinct_id: 'anonymous_id',
token: team.api_token,
distinct_id: 'new_distinct_id',
$set: { a_prop: 'test' },
},
} as any as PluginEvent,
team.id,
now,
new UUIDT().toString()
)
})
// This case is likely to happen after signup, for example:
// 1. User browses website with anonymous_id
// 2. User signs up, triggers event with their new_distinct_id (creating a new Person)
// 3. In the frontend, try to alias anonymous_id with new_distinct_id
// Result should be that we end up with one Person with both ID's
test('distinct with anonymous_id which was already created', async () => {
await createPerson(hub, team, ['anonymous_id'])
await createPerson(hub, team, ['new_distinct_id'], { email: 'someone@gmail.com' })
await processEvent(
'new_distinct_id',
'',
'',
{
event: '$identify',
properties: {
$anon_distinct_id: 'anonymous_id',
token: team.api_token,
distinct_id: 'new_distinct_id',
},
} as any as PluginEvent,
team.id,
now,
new UUIDT().toString()
)
const [person] = await hub.db.fetchPersons()
expect(await hub.db.fetchDistinctIdValues(person)).toEqual(['anonymous_id', 'new_distinct_id'])
expect(person.properties['email']).toEqual('someone@gmail.com')
expect(person.is_identified).toEqual(true)
})
test('identify with the same distinct_id as anon_distinct_id', async () => {
await createPerson(hub, team, ['anonymous_id'])
await processEvent(
'anonymous_id',
'',
'',
{
event: '$identify',
properties: {
$anon_distinct_id: 'anonymous_id',
token: team.api_token,
distinct_id: 'anonymous_id',
},
} as any as PluginEvent,
team.id,
now,
new UUIDT().toString()
)
const [person] = await hub.db.fetchPersons()
expect(await hub.db.fetchDistinctIdValues(person)).toEqual(['anonymous_id'])
expect(person.is_identified).toEqual(false)
})
test('distinct with multiple anonymous_ids which were already created', async () => {
await createPerson(hub, team, ['anonymous_id'])
await createPerson(hub, team, ['new_distinct_id'], { email: 'someone@gmail.com' })
await processEvent(
'new_distinct_id',
'',
'',
{
event: '$identify',
properties: {
$anon_distinct_id: 'anonymous_id',
token: team.api_token,
distinct_id: 'new_distinct_id',
},
} as any as PluginEvent,
team.id,
now,
new UUIDT().toString()
)
const persons1 = await hub.db.fetchPersons()
expect(persons1.length).toBe(1)
expect(await hub.db.fetchDistinctIdValues(persons1[0])).toEqual(['anonymous_id', 'new_distinct_id'])
expect(persons1[0].properties['email']).toEqual('someone@gmail.com')
expect(persons1[0].is_identified).toEqual(true)
await createPerson(hub, team, ['anonymous_id_2'])
await processEvent(
'new_distinct_id',
'',
'',
{
event: '$identify',
properties: {
$anon_distinct_id: 'anonymous_id_2',
token: team.api_token,
distinct_id: 'new_distinct_id',
},
} as any as PluginEvent,
team.id,
now,
new UUIDT().toString()
)
const persons2 = await hub.db.fetchPersons()
expect(persons2.length).toBe(1)
expect(await hub.db.fetchDistinctIdValues(persons2[0])).toEqual([
'anonymous_id',
'new_distinct_id',
'anonymous_id_2',
])
expect(persons2[0].properties['email']).toEqual('someone@gmail.com')
expect(persons2[0].is_identified).toEqual(true)
})
test('distinct team leakage', async () => {
await createUserTeamAndOrganization(
hub.postgres,
3,
1002,
'a73fc995-a63f-4e4e-bf65-2a5e9f93b2b1',
'01774e2f-0d01-0000-ee94-9a238640c6ee',
'0174f81e-36f5-0000-7ef8-cc26c1fbab1c'
)
const team2 = (await getTeams(hub))[1]
await createPerson(hub, team2, ['2'], { email: 'team2@gmail.com' })
await createPerson(hub, team, ['1', '2'])
await processEvent(
'2',
'',
'',
{
event: '$identify',
properties: {
$anon_distinct_id: '1',
token: team.api_token,
distinct_id: '2',
},
} as any as PluginEvent,
team.id,
now,
new UUIDT().toString()
)
const people = (await hub.db.fetchPersons()).sort((p1, p2) => p2.team_id - p1.team_id)
expect(people.length).toEqual(2)
expect(people[1].team_id).toEqual(team.id)
expect(people[1].properties).toEqual({})
expect(await hub.db.fetchDistinctIdValues(people[1])).toEqual(['1', '2'])
expect(people[0].team_id).toEqual(team2.id)
expect(await hub.db.fetchDistinctIdValues(people[0])).toEqual(['2'])
})
describe('when handling $identify', () => {
test('we do not alias users if distinct id changes but we are already identified', async () => {
// This test is in reference to
// https://github.com/PostHog/posthog/issues/5527 , where we were
// correctly identifying that an anonymous user before login should be
// aliased to the user they subsequently login as, but incorrectly
// aliasing on subsequent $identify events. The anonymous case is
// special as we want to alias to a known user, but otherwise we
// shouldn't be doing so.
const anonymousId = 'anonymous_id'
const initialDistinctId = 'initial_distinct_id'
const p2DistinctId = 'p2_distinct_id'
const p2NewDistinctId = 'new_distinct_id'
// Play out a sequence of events that should result in two users being
// identified, with the first to events associated with one user, and
// the third with another.
await capture(hub, 'event 1')
await identify(hub, initialDistinctId)
await capture(hub, 'event 2')
state.currentDistinctId = p2DistinctId
await capture(hub, 'event 3')
await identify(hub, p2NewDistinctId)
await capture(hub, 'event 4')
// Let's also make sure that we do not alias when switching back to
// initialDistictId
await identify(hub, initialDistinctId)
// Get pairins of person distinctIds and the events associated with them
const eventsByPerson = await getEventsByPerson(hub)
expect(eventsByPerson).toEqual([
[
[anonymousId, initialDistinctId],
['event 1', '$identify', 'event 2', '$identify'],
],
[
[p2DistinctId, p2NewDistinctId],
['event 3', '$identify', 'event 4'],
],
])
// Make sure the persons are identified
const persons = await hub.db.fetchPersons()
expect(persons.map((person) => person.is_identified)).toEqual([true, true])
})
test('we do not alias users if distinct id changes but we are already identified, with no anonymous event', async () => {
// This test is in reference to
// https://github.com/PostHog/posthog/issues/5527 , where we were
// correctly identifying that an anonymous user before login should be
// aliased to the user they subsequently login as, but incorrectly
// aliasing on subsequent $identify events. The anonymous case is
// special as we want to alias to a known user, but otherwise we
// shouldn't be doing so. This test is similar to the previous one,
// except it does not include an initial anonymous event.
const anonymousId = 'anonymous_id'
const initialDistinctId = 'initial_distinct_id'
const p2DistinctId = 'p2_distinct_id'
const p2NewDistinctId = 'new_distinct_id'
// Play out a sequence of events that should result in two users being
// identified, with the first to events associated with one user, and
// the third with another.
await identify(hub, initialDistinctId)
await capture(hub, 'event 2')
state.currentDistinctId = p2DistinctId
await capture(hub, 'event 3')
await identify(hub, p2NewDistinctId)
await capture(hub, 'event 4')
// Let's also make sure that we do not alias when switching back to
// initialDistictId
await identify(hub, initialDistinctId)
// Get pairins of person distinctIds and the events associated with them
const eventsByPerson = await getEventsByPerson(hub)
expect(eventsByPerson).toEqual([
[
[initialDistinctId, anonymousId],
['$identify', 'event 2', '$identify'],
],
[
[p2DistinctId, p2NewDistinctId],
['event 3', '$identify', 'event 4'],
],
])
// Make sure the persons are identified
const persons = await hub.db.fetchPersons()
expect(persons.map((person) => person.is_identified)).toEqual([true, true])
})
test('we do not leave things in inconsistent state if $identify is run concurrently', async () => {
// There are a few places where we have the pattern of:
//
// 1. fetch from postgres
// 2. check rows match condition
// 3. perform update
//
// This test is designed to check the specific case where, in
// handling we are creating an unidentified user, then updating this
// user to have is_identified = true. Since we are using the
// is_identified to decide on if we will merge persons, we want to
// make sure we guard against this race condition. The scenario is:
//
// 1. initiate identify for 'distinct-id'
// 2. once person for distinct-id has been created, initiate
// identify for 'new-distinct-id'
// 3. check that the persons remain distinct
// Check the db is empty to start with
expect(await hub.db.fetchPersons()).toEqual([])
const anonymousId = 'anonymous_id'
const initialDistinctId = 'initial-distinct-id'
const newDistinctId = 'new-distinct-id'
state.currentDistinctId = newDistinctId
await capture(hub, 'some event')
state.currentDistinctId = anonymousId
// Hook into createPerson, which is as of writing called from
// alias. Here we simply call identify again and wait on it
// completing before continuing with the first identify.
const originalCreatePerson = hub.db.createPerson.bind(hub.db)
const createPersonMock = jest.fn(async (...args) => {
const result = await originalCreatePerson(...args)
if (createPersonMock.mock.calls.length === 1) {
// On second invocation, make another identify call
await identify(hub, newDistinctId)
}
return result
})
hub.db.createPerson = createPersonMock
// set the first identify going
await identify(hub, initialDistinctId)
// Let's first just make sure `updatePerson` was called, as a way of
// checking that our mocking was actually invoked
expect(hub.db.createPerson).toHaveBeenCalled()
// Now make sure that we have one person in the db that has been
// identified
const persons = await hub.db.fetchPersons()
expect(persons.length).toEqual(2)
expect(persons.map((person) => person.is_identified)).toEqual([true, true])
})
})
describe('when handling $create_alias', () => {
test('we can alias an identified person to an identified person', async () => {
const anonymousId = 'anonymous_id'
const identifiedId1 = 'identified_id1'
const identifiedId2 = 'identified_id2'
// anonymous_id -> identified_id1
await identify(hub, identifiedId1)
state.currentDistinctId = identifiedId1
await capture(hub, 'some event')
await identify(hub, identifiedId2)
await alias(hub, identifiedId1, identifiedId2)
// Get pairings of person distinctIds and the events associated with them
const eventsByPerson = await getEventsByPerson(hub)
// There should just be one person, to which all events are associated
expect(eventsByPerson).toEqual([
[
expect.arrayContaining([anonymousId, identifiedId1, identifiedId2]),
['$identify', 'some event', '$identify', '$create_alias'],
],
])
// Make sure there is one identified person
const persons = await hub.db.fetchPersons()
expect(persons.map((person) => person.is_identified)).toEqual([true])
})
test('we can alias an anonymous person to an identified person', async () => {
const anonymousId = 'anonymous_id'
const initialDistinctId = 'initial_distinct_id'
// Identify one person, then become anonymous
await identify(hub, initialDistinctId)
state.currentDistinctId = anonymousId
await capture(hub, 'anonymous event')
// Then try to alias them
await alias(hub, anonymousId, initialDistinctId)
// Get pairings of person distinctIds and the events associated with them
const eventsByPerson = await getEventsByPerson(hub)
// There should just be one person, to which all events are associated
expect(eventsByPerson).toEqual([
[
[initialDistinctId, anonymousId],
['$identify', 'anonymous event', '$create_alias'],
],
])
// Make sure there is one identified person
const persons = await hub.db.fetchPersons()
expect(persons.map((person) => person.is_identified)).toEqual([true])
})
test('we can alias an identified person to an anonymous person', async () => {
const anonymousId = 'anonymous_id'
const initialDistinctId = 'initial_distinct_id'
// Identify one person, then become anonymous
await identify(hub, initialDistinctId)
state.currentDistinctId = anonymousId
await capture(hub, 'anonymous event')
// Then try to alias them
await alias(hub, initialDistinctId, anonymousId)
// Get pairings of person distinctIds and the events associated with them
const eventsByPerson = await getEventsByPerson(hub)
// There should just be one person, to which all events are associated
expect(eventsByPerson).toEqual([
[
[initialDistinctId, anonymousId],
['$identify', 'anonymous event', '$create_alias'],
],
])
// Make sure there is one identified person
const persons = await hub.db.fetchPersons()
expect(persons.map((person) => person.is_identified)).toEqual([true])
})
test('we can alias an anonymous person to an anonymous person', async () => {
const anonymous1 = 'anonymous-1'
const anonymous2 = 'anonymous-2'
// Identify one person, then become anonymous
state.currentDistinctId = anonymous1
await capture(hub, 'anonymous event 1')
state.currentDistinctId = anonymous2
await capture(hub, 'anonymous event 2')
// Then try to alias them
await alias(hub, anonymous1, anonymous2)
// Get pairings of person distinctIds and the events associated with them
const eventsByPerson = await getEventsByPerson(hub)
// There should just be one person, to which all events are associated
expect(eventsByPerson).toEqual([
[
[anonymous1, anonymous2],
['anonymous event 1', 'anonymous event 2', '$create_alias'],
],
])
// Make sure there is one identified person
const persons = await hub.db.fetchPersons()
expect(persons.map((person) => person.is_identified)).toEqual([true])
})
test('we can alias two non-existent persons', async () => {
const anonymous1 = 'anonymous-1'
const anonymous2 = 'anonymous-2'
// Then try to alias them
state.currentDistinctId = anonymous1
await alias(hub, anonymous2, anonymous1)
// Get pairings of person distinctIds and the events associated with them
const eventsByPerson = await getEventsByPerson(hub)
// There should just be one person, to which all events are associated
expect(eventsByPerson).toEqual([[[anonymous1, anonymous2], ['$create_alias']]])
const persons = await hub.db.fetchPersons()
expect(persons.map((person) => person.is_identified)).toEqual([true])
})
})
test('team event_properties', async () => {
expect(await hub.db.fetchEventDefinitions()).toEqual([])
expect(await hub.db.fetchEventProperties()).toEqual([])
expect(await hub.db.fetchPropertyDefinitions()).toEqual([])
await processEvent(
'xxx',
'127.0.0.1',
'',
{ event: 'purchase', properties: { price: 299.99, name: 'AirPods Pro' } } as any as PluginEvent,
team.id,
now,
new UUIDT().toString()
)
team = await getFirstTeam(hub)
expect(await hub.db.fetchEventDefinitions()).toEqual([
{
id: expect.any(String),
name: 'purchase',
query_usage_30_day: null,
team_id: 2,
volume_30_day: null,
created_at: expect.any(String),
last_seen_at: expect.any(String),
},
])
expect(await hub.db.fetchPropertyDefinitions()).toEqual([
{
id: expect.any(String),
is_numerical: false,
name: '$ip',
property_type: 'String',
property_type_format: null,
query_usage_30_day: null,
team_id: 2,
volume_30_day: null,
type: PropertyDefinitionTypeEnum.Event,
group_type_index: null,
},
{
id: expect.any(String),
is_numerical: false,
name: 'name',
property_type: 'String',
property_type_format: null,
query_usage_30_day: null,
team_id: 2,
volume_30_day: null,
type: PropertyDefinitionTypeEnum.Event,
group_type_index: null,
},
{
id: expect.any(String),
is_numerical: true,
name: 'price',
property_type: 'Numeric',
property_type_format: null,
query_usage_30_day: null,
team_id: 2,
volume_30_day: null,
type: PropertyDefinitionTypeEnum.Event,
group_type_index: null,
},
])
// flushed every minute normally, triggering flush now, it's tested elsewhere
expect(await hub.db.fetchEventProperties()).toEqual([
{
id: expect.any(Number),
event: 'purchase',
property: '$ip',
team_id: 2,
},
{
id: expect.any(Number),
event: 'purchase',
property: 'name',
team_id: 2,
},
{
id: expect.any(Number),
event: 'purchase',
property: 'price',
team_id: 2,
},
])
})
test('event name object json', async () => {
await processEvent(
'xxx',
'',
'',
{ event: { 'event name': 'as object' }, properties: {} } as any as PluginEvent,
team.id,
now,
new UUIDT().toString()
)
const [event] = await hub.db.fetchEvents()
expect(event.event).toEqual('{"event name":"as object"}')
})
test('event name array json', async () => {
await processEvent(
'xxx',
'',
'',
{ event: ['event name', 'a list'], properties: {} } as any as PluginEvent,
team.id,
now,
new UUIDT().toString()
)
const [event] = await hub.db.fetchEvents()
expect(event.event).toEqual('["event name","a list"]')
})
test('long event name substr', async () => {
await processEvent(
'xxx',
'',
'',
{ event: 'E'.repeat(300), properties: { price: 299.99, name: 'AirPods Pro' } } as any as PluginEvent,
team.id,
DateTime.utc(),
new UUIDT().toString()
)
const [event] = await hub.db.fetchEvents()
expect(event.event?.length).toBe(200)
})
describe('validates eventUuid', () => {
test('invalid uuid string returns an error', async () => {
const pluginEvent: PluginEvent = {
distinct_id: 'i_am_a_distinct_id',
site_url: '',
team_id: team.id,
timestamp: DateTime.utc().toISO(),
now: now.toUTC().toISO(),
ip: '',
uuid: 'i_am_not_a_uuid',
event: 'eVeNt',
properties: { price: 299.99, name: 'AirPods Pro' },
}
const runner = new EventPipelineRunner(hub, pluginEvent)
const result = await runner.runEventPipeline(pluginEvent)
expect(result.error).toBeDefined()
expect(result.error).toEqual('Not a valid UUID: "i_am_not_a_uuid"')
})
test('null value in eventUUID returns an error', async () => {
const pluginEvent: PluginEvent = {
distinct_id: 'i_am_a_distinct_id',
site_url: '',
team_id: team.id,
timestamp: DateTime.utc().toISO(),
now: now.toUTC().toISO(),
ip: '',
uuid: null as any,
event: 'eVeNt',
properties: { price: 299.99, name: 'AirPods Pro' },
}
const runner = new EventPipelineRunner(hub, pluginEvent)
const result = await runner.runEventPipeline(pluginEvent)
expect(result.error).toBeDefined()
expect(result.error).toEqual('Not a valid UUID: "null"')
})
})
test('any event can do $set on props (user exists)', async () => {
await createPerson(hub, team, ['distinct_id1'])
await processEvent(
'distinct_id1',
'',
'',
{
event: 'some_event',
properties: {
token: team.api_token,
distinct_id: 'distinct_id1',
$set: { a_prop: 'test-1', c_prop: 'test-1' },
},
} as any as PluginEvent,
team.id,
now,
new UUIDT().toString()
)
expect((await hub.db.fetchEvents()).length).toBe(1)
const [event] = await hub.db.fetchEvents()
expect(event.properties['$set']).toEqual({ a_prop: 'test-1', c_prop: 'test-1' })
const [person] = await hub.db.fetchPersons()
expect(await hub.db.fetchDistinctIdValues(person)).toEqual(['distinct_id1'])
expect(person.properties).toEqual({ a_prop: 'test-1', c_prop: 'test-1' })
})
test('any event can do $set on props (new user)', async () => {
const uuid = new UUIDT().toString()
await processEvent(
'distinct_id1',
'',
'',
{
event: 'some_event',
properties: {
token: team.api_token,
distinct_id: 'distinct_id1',
$set: { a_prop: 'test-1', c_prop: 'test-1' },
},
} as any as PluginEvent,
team.id,
now,
uuid
)
expect((await hub.db.fetchEvents()).length).toBe(1)
const [event] = await hub.db.fetchEvents()
expect(event.properties['$set']).toEqual({ a_prop: 'test-1', c_prop: 'test-1' })
const [person] = await hub.db.fetchPersons()
expect(await hub.db.fetchDistinctIdValues(person)).toEqual(['distinct_id1'])
expect(person.properties).toEqual({ $creator_event_uuid: uuid, a_prop: 'test-1', c_prop: 'test-1' })
})
test('any event can do $set_once on props', async () => {
await createPerson(hub, team, ['distinct_id1'])
await processEvent(
'distinct_id1',
'',
'',
{
event: 'some_event',
properties: {
token: team.api_token,
distinct_id: 'distinct_id1',
$set_once: { a_prop: 'test-1', c_prop: 'test-1' },
},
} as any as PluginEvent,
team.id,
now,
new UUIDT().toString()
)
expect((await hub.db.fetchEvents()).length).toBe(1)
const [event] = await hub.db.fetchEvents()
expect(event.properties['$set_once']).toEqual({ a_prop: 'test-1', c_prop: 'test-1' })
const [person] = await hub.db.fetchPersons()
expect(await hub.db.fetchDistinctIdValues(person)).toEqual(['distinct_id1'])
expect(person.properties).toEqual({ a_prop: 'test-1', c_prop: 'test-1' })
await processEvent(
'distinct_id1',
'',
'',
{
event: 'some_other_event',
properties: {
token: team.api_token,
distinct_id: 'distinct_id1',
$set_once: { a_prop: 'test-2', b_prop: 'test-2b' },
},
} as any as PluginEvent,
team.id,
now,
new UUIDT().toString()
)
expect((await hub.db.fetchEvents()).length).toBe(2)
const [person2] = await hub.db.fetchPersons()
expect(person2.properties).toEqual({ a_prop: 'test-1', b_prop: 'test-2b', c_prop: 'test-1' })
})
test('$set and $set_once', async () => {
const uuid = new UUIDT().toString()
await processEvent(
'distinct_id1',
'',
'',
{
event: 'some_event',
properties: {
token: team.api_token,
distinct_id: 'distinct_id1',
$set: { key1: 'value1', key2: 'value2', key3: 'value4' },
$set_once: { key1_once: 'value1', key2_once: 'value2', key3_once: 'value4' },
},
} as any as PluginEvent,
team.id,
now,
uuid
)
expect((await hub.db.fetchEvents()).length).toBe(1)
const [person] = await hub.db.fetchPersons()
expect(await hub.db.fetchDistinctIdValues(person)).toEqual(['distinct_id1'])
expect(person.properties).toEqual({
$creator_event_uuid: uuid,
key1: 'value1',
key2: 'value2',
key3: 'value4',
key1_once: 'value1',
key2_once: 'value2',
key3_once: 'value4',
})
})
test('groupidentify', async () => {
await createPerson(hub, team, ['distinct_id1'])
await processEvent(
'distinct_id1',
'',
'',
{
event: '$groupidentify',
properties: {
token: team.api_token,
distinct_id: 'distinct_id1',
$group_type: 'organization',
$group_key: 'org::5',
$group_set: {
foo: 'bar',
},
},
} as any as PluginEvent,
team.id,
now,
new UUIDT().toString()
)
expect((await hub.db.fetchEvents()).length).toBe(1)
await delayUntilEventIngested(() => hub.db.fetchClickhouseGroups(), 1)
const [clickhouseGroup] = await hub.db.fetchClickhouseGroups()
expect(clickhouseGroup).toEqual({
group_key: 'org::5',
group_properties: JSON.stringify({ foo: 'bar' }),
group_type_index: 0,
team_id: team.id,
created_at: expect.any(String),
})
const group = await hub.db.fetchGroup(team.id, 0, 'org::5')
expect(group).toEqual({
id: expect.any(Number),
team_id: team.id,
group_type_index: 0,
group_key: 'org::5',
group_properties: { foo: 'bar' },
created_at: now,
properties_last_updated_at: {},
properties_last_operation: {},
version: 1,
})
})
test('groupidentify without group_type ingests event', async () => {
await createPerson(hub, team, ['distinct_id1'])
await processEvent(
'distinct_id1',
'',
'',
{
event: '$groupidentify',
properties: {
token: team.api_token,
distinct_id: 'distinct_id1',
$group_key: 'org::5',
$group_set: {
foo: 'bar',
},
},
} as any as PluginEvent,
team.id,
now,
new UUIDT().toString()
)
expect((await hub.db.fetchEvents()).length).toBe(1)
})
test('$groupidentify updating properties', async () => {
const next: DateTime = now.plus({ minutes: 1 })
await createPerson(hub, team, ['distinct_id1'])
await hub.db.insertGroup(team.id, 0, 'org::5', { a: 1, b: 2 }, now, {}, {}, 1)
await processEvent(
'distinct_id1',
'',
'',
{
event: '$groupidentify',
properties: {
token: team.api_token,
distinct_id: 'distinct_id1',
$group_type: 'organization',
$group_key: 'org::5',
$group_set: {
foo: 'bar',
a: 3,
},
},
} as any as PluginEvent,
team.id,
next,
new UUIDT().toString()
)
expect((await hub.db.fetchEvents()).length).toBe(1)
await delayUntilEventIngested(() => hub.db.fetchClickhouseGroups(), 1)
const [clickhouseGroup] = await hub.db.fetchClickhouseGroups()
expect(clickhouseGroup).toEqual({
group_key: 'org::5',
group_properties: JSON.stringify({ a: 3, b: 2, foo: 'bar' }),
group_type_index: 0,
team_id: team.id,
created_at: expect.any(String),
})
const group = await hub.db.fetchGroup(team.id, 0, 'org::5')
expect(group).toEqual({
id: expect.any(Number),
team_id: team.id,
group_type_index: 0,
group_key: 'org::5',
group_properties: { a: 3, b: 2, foo: 'bar' },
created_at: now,
properties_last_updated_at: {},
properties_last_operation: {},
version: 2,
})
})
test('person and group properties on events', async () => {
await createPerson(hub, team, ['distinct_id1'], { pineapple: 'on', pizza: 1 })
await processEvent(
'distinct_id1',
'',
'',
{
event: '$groupidentify',
properties: {
token: team.api_token,
distinct_id: 'distinct_id1',
$group_type: 'organization',
$group_key: 'org:5',
$group_set: {
foo: 'bar',
},
},
} as any as PluginEvent,
team.id,
now,
new UUIDT().toString()
)
await processEvent(
'distinct_id1',
'',
'',
{
event: '$groupidentify',
properties: {
token: team.api_token,
distinct_id: 'distinct_id1',
$group_type: 'second',
$group_key: 'second_key',
$group_set: {
pineapple: 'yummy',
},
},
} as any as PluginEvent,
team.id,
now,
new UUIDT().toString()
)
await processEvent(
'distinct_id1',
'',
'',
{
event: 'test event',
properties: {
token: team.api_token,
distinct_id: 'distinct_id1',
$set: { new: 5 },
$group_0: 'org:5',
$group_1: 'second_key',
},
} as any as PluginEvent,
team.id,
now,
new UUIDT().toString()
)
const events = await hub.db.fetchEvents()
const event = [...events].find((e: any) => e['event'] === 'test event')
expect(event?.person_properties).toEqual({ pineapple: 'on', pizza: 1, new: 5 })
expect(event?.group0_properties).toEqual({ foo: 'bar' })
expect(event?.group1_properties).toEqual({ pineapple: 'yummy' })
})
test('set and set_once on the same key', async () => {
await createPerson(hub, team, ['distinct_id1'])
await processEvent(
'distinct_id1',
'',
'',
{
event: 'some_event',
properties: {
token: team.api_token,
distinct_id: 'distinct_id1',
$set: { a_prop: 'test-set' },
$set_once: { a_prop: 'test-set_once' },
},
} as any as PluginEvent,
team.id,
now,
new UUIDT().toString()
)
expect((await hub.db.fetchEvents()).length).toBe(1)
const [event] = await hub.db.fetchEvents()
expect(event.properties['$set']).toEqual({ a_prop: 'test-set' })
expect(event.properties['$set_once']).toEqual({ a_prop: 'test-set_once' })
const [person] = await hub.db.fetchPersons()
expect(await hub.db.fetchDistinctIdValues(person)).toEqual(['distinct_id1'])
expect(person.properties).toEqual({ a_prop: 'test-set' })
})
test('$unset person property', async () => {
await createPerson(hub, team, ['distinct_id1'], { a: 1, b: 2, c: 3 })
await processEvent(
'distinct_id1',
'',
'',
{
event: 'some_event',
properties: {
token: team.api_token,
distinct_id: 'distinct_id1',
$unset: ['a', 'c'],
},
} as any as PluginEvent,
team.id,
now,
new UUIDT().toString()
)
expect((await hub.db.fetchEvents()).length).toBe(1)
const [event] = await hub.db.fetchEvents()
expect(event.properties['$unset']).toEqual(['a', 'c'])
const [person] = await hub.db.fetchPersons()
expect(await hub.db.fetchDistinctIdValues(person)).toEqual(['distinct_id1'])
expect(person.properties).toEqual({ b: 2 })
})
test('$unset person empty set ignored', async () => {
await createPerson(hub, team, ['distinct_id1'], { a: 1, b: 2, c: 3 })
await processEvent(
'distinct_id1',
'',
'',
{
event: 'some_event',
properties: {
token: team.api_token,
distinct_id: 'distinct_id1',
$unset: {},
},
} as any as PluginEvent,
team.id,
now,
new UUIDT().toString()
)
expect((await hub.db.fetchEvents()).length).toBe(1)
const [event] = await hub.db.fetchEvents()
expect(event.properties['$unset']).toEqual({})
const [person] = await hub.db.fetchPersons()
expect(await hub.db.fetchDistinctIdValues(person)).toEqual(['distinct_id1'])
expect(person.properties).toEqual({ a: 1, b: 2, c: 3 })
})
describe('ingestion in any order', () => {
const ts0: DateTime = now
const ts1: DateTime = now.plus({ minutes: 1 })
const ts2: DateTime = now.plus({ minutes: 2 })
const ts3: DateTime = now.plus({ minutes: 3 })
// key encodes when the value is updated, e.g. s0 means only set call for the 0th event
// s03o23 means via a set in events number 0 and 3 plus via set_once on 2nd and 3rd event
// the value corresponds to which call updated it + random letter (same letter for the same key)
// the letter is for verifying we update the right key only
const set0: Properties = { s0123o0123: 's0a', s02o13: 's0b', s013: 's0e' }
const setOnce0: Properties = { s0123o0123: 'o0a', s13o02: 'o0g', o023: 'o0f' }
const set1: Properties = { s0123o0123: 's1a', s13o02: 's1g', s1: 's1c', s013: 's1e' }
const setOnce1: Properties = { s0123o0123: 'o1a', s02o13: 'o1b', o1: 'o1d' }
const set2: Properties = { s0123o0123: 's2a', s02o13: 's2b' }
const setOnce2: Properties = { s0123o0123: 'o2a', s13o02: 'o2g', o023: 'o2f' }
const set3: Properties = { s0123o0123: 's3a', s13o02: 's3g', s013: 's3e' }
const setOnce3: Properties = { s0123o0123: 'o3a', s02o13: 'o3b', o023: 'o3f' }
beforeEach(async () => {
await createPerson(hub, team, ['distinct_id1'])
})
async function verifyPersonPropertiesSetCorrectly() {
expect((await hub.db.fetchEvents()).length).toBe(4)
const [person] = await hub.db.fetchPersons()
expect(await hub.db.fetchDistinctIdValues(person)).toEqual(['distinct_id1'])
expect(person.properties).toEqual({
s0123o0123: 's3a',
s02o13: 's2b',
s1: 's1c',
o1: 'o1d',
s013: 's3e',
o023: 'o0f',
s13o02: 's3g',
})
expect(person.version).toEqual(4)
}
async function runProcessEvent(set: Properties, setOnce: Properties, ts: DateTime) {
await processEvent(
'distinct_id1',
'',
'',
{
event: 'some_event',
properties: {
$set: set,
$set_once: setOnce,
},
} as any as PluginEvent,
team.id,
ts,
new UUIDT().toString()
)
}
async function ingest0() {
await runProcessEvent(set0, setOnce0, ts0)
}
async function ingest1() {
await runProcessEvent(set1, setOnce1, ts1)
}
async function ingest2() {
await runProcessEvent(set2, setOnce2, ts2)
}
async function ingest3() {
await runProcessEvent(set3, setOnce3, ts3)
}
test('ingestion in order', async () => {
await ingest0()
await ingest1()
await ingest2()
await ingest3()
await verifyPersonPropertiesSetCorrectly()
})
})