0
0
mirror of https://github.com/PostHog/posthog.git synced 2024-12-01 12:21:02 +01:00
posthog/plugin-server/tests/e2e.test.ts
Harry Waye e7a9b7de79
fix(autocapture): ensure $elements passed to onEvent (#10880)
* fix(autocapture): ensure `$elements` passed to `onEvent`

Before calling `onEvent` the plugin does, amoung other things, a delete
on `event.properties` of the `$elements` associated with `$autocapture`.
This means that for instance the S3 plugin doesn't include this data in
it's dump.

We could also include other data like `elements_chain` that we also
store in `ClickHouse` but I've gone for just including `elements` for
now as `elements_chain` is derived from `elements` anyhow.

* revert .env changes, I'll do that separately

* run prettier

* update to scaffold 1.3.0

* fix lint

* chore: update scaffold to 1.3.1

* update scaffold
2022-07-20 14:33:32 +01:00

335 lines
13 KiB
TypeScript

import Piscina from '@posthog/piscina'
import IORedis from 'ioredis'
import { ONE_HOUR } from '../src/config/constants'
import { startPluginsServer } from '../src/main/pluginsServer'
import { LogLevel, PluginsServerConfig } from '../src/types'
import { Hub } from '../src/types'
import { delay, UUIDT } from '../src/utils/utils'
import { makePiscina } from '../src/worker/piscina'
import { createPosthog, DummyPostHog } from '../src/worker/vm/extensions/posthog'
import { writeToFile } from '../src/worker/vm/extensions/test-utils'
import { delayUntilEventIngested, resetTestDatabaseClickhouse } from './helpers/clickhouse'
import { resetKafka } from './helpers/kafka'
import { pluginConfig39 } from './helpers/plugins'
import { resetTestDatabase } from './helpers/sql'
const { console: testConsole } = writeToFile
jest.mock('../src/utils/status')
jest.setTimeout(60000) // 60 sec timeout
const extraServerConfig: Partial<PluginsServerConfig> = {
WORKER_CONCURRENCY: 2,
LOG_LEVEL: LogLevel.Log,
CONVERSION_BUFFER_ENABLED: false,
}
const indexJs = `
import { console as testConsole } from 'test-utils/write-to-file'
export async function processEvent (event) {
testConsole.log('processEvent')
console.info('amogus')
event.properties.processed = 'hell yes'
event.properties.upperUuid = event.properties.uuid?.toUpperCase()
event.properties['$snapshot_data'] = 'no way'
return event
}
export function onEvent (event, { global }) {
// we use this to mock setupPlugin being
// run after some events were already ingested
global.timestampBoundariesForTeam = {
max: new Date(),
min: new Date(Date.now()-${ONE_HOUR})
}
testConsole.log('onEvent', JSON.stringify(event))
}
export function onSnapshot (event) {
testConsole.log('onSnapshot', event.event)
}
export async function exportEvents(events) {
for (const event of events) {
if (event.properties && event.properties['$$is_historical_export_event']) {
testConsole.log('exported historical event', event)
}
}
}
export async function runEveryMinute() {}
`
describe('E2E', () => {
let hub: Hub
let stopServer: () => Promise<void>
let posthog: DummyPostHog
let piscina: Piscina
let redis: IORedis.Redis
beforeAll(async () => {
await resetKafka(extraServerConfig)
})
beforeEach(async () => {
testConsole.reset()
await resetTestDatabase(indexJs)
await resetTestDatabaseClickhouse(extraServerConfig)
const startResponse = await startPluginsServer(extraServerConfig, makePiscina)
hub = startResponse.hub
piscina = startResponse.piscina
stopServer = startResponse.stop
redis = await hub.redisPool.acquire()
posthog = createPosthog(hub, pluginConfig39)
})
afterEach(async () => {
await hub.redisPool.release(redis)
await stopServer()
})
describe('ClickHouse ingestion', () => {
test('event captured, processed, ingested', async () => {
expect((await hub.db.fetchEvents()).length).toBe(0)
const uuid = new UUIDT().toString()
const event = {
event: 'custom event',
properties: { name: 'haha', uuid },
}
await posthog.capture(event.event, event.properties)
await delayUntilEventIngested(() => hub.db.fetchEvents())
await hub.kafkaProducer.flush()
const events = await hub.db.fetchEvents()
expect(events.length).toBe(1)
// processEvent ran and modified
expect(events[0].properties.processed).toEqual('hell yes')
expect(events[0].properties.upperUuid).toEqual(uuid.toUpperCase())
// onEvent ran
const consoleOutput = testConsole.read()
expect(consoleOutput).toEqual([['processEvent'], ['onEvent', expect.any(String)]])
const onEventEvent = JSON.parse(consoleOutput[1][1])
expect(onEventEvent.event).toEqual('custom event')
expect(onEventEvent.properties).toEqual(expect.objectContaining(event.properties))
})
test('correct $autocapture properties included in onEvent calls', async () => {
// The plugin server does modifications to the `event.properties`
// and as a results we remove the initial `$elements` from the
// object. Thus we want to ensure that this information is passed
// through to any plugins with `onEvent` handlers
const properties = {
$elements: [{ tag_name: 'div', nth_child: 1, nth_of_type: 2, $el_text: '💻' }],
}
const event = {
event: '$autocapture',
properties: properties,
}
await posthog.capture(event.event, event.properties)
await delayUntilEventIngested(() => hub.db.fetchEvents(), 1)
// onEvent ran
const consoleOutput = testConsole.read()
expect(consoleOutput).toEqual([['processEvent'], ['onEvent', expect.any(String)]])
const onEventEvent = JSON.parse(consoleOutput[1][1])
expect(onEventEvent.elements).toEqual([
{ attributes: {}, nth_child: 1, nth_of_type: 2, tag_name: 'div', text: '💻' },
])
})
test('snapshot captured, processed, ingested', async () => {
expect((await hub.db.fetchSessionRecordingEvents()).length).toBe(0)
await posthog.capture('$snapshot', { $session_id: '1234abc', $snapshot_data: 'yes way' })
await delayUntilEventIngested(() => hub.db.fetchSessionRecordingEvents())
await hub.kafkaProducer.flush()
const events = await hub.db.fetchSessionRecordingEvents()
expect(events.length).toBe(1)
// processEvent did not modify
expect(events[0].snapshot_data).toEqual('yes way')
// onSnapshot ran
expect(testConsole.read()).toEqual([['onSnapshot', '$snapshot']])
})
test('console logging is persistent', async () => {
const fetchLogs = async () => {
const logs = await hub.db.fetchPluginLogEntries()
return logs.filter(({ type, source }) => type === 'INFO' && source !== 'SYSTEM')
}
await posthog.capture('custom event', { name: 'hehe', uuid: new UUIDT().toString() })
await hub.kafkaProducer.flush()
await delayUntilEventIngested(() => hub.db.fetchEvents())
// :KLUDGE: Force workers to emit their logs, otherwise they might never get cpu time.
await piscina.broadcastTask({ task: 'flushKafkaMessages' })
const pluginLogEntries = await delayUntilEventIngested(fetchLogs)
expect(pluginLogEntries).toContainEqual(
expect.objectContaining({
type: 'INFO',
message: 'amogus',
})
)
})
})
// TODO: we should enable this test again - they are enabled on self-hosted
// historical exports are currently disabled
describe.skip('export historical events', () => {
const awaitHistoricalEventLogs = async () =>
await new Promise((resolve) => {
resolve(testConsole.read().filter((log) => log[0] === 'exported historical event'))
})
test('export historical events', async () => {
await posthog.capture('historicalEvent1')
await posthog.capture('historicalEvent2')
await posthog.capture('historicalEvent3')
await posthog.capture('historicalEvent4')
await delayUntilEventIngested(() => hub.db.fetchEvents(), 4)
// the db needs to have events _before_ running setupPlugin
// to test payloads with missing timestamps
// hence we reload here
await piscina.broadcastTask({ task: 'teardownPlugins' })
await delay(2000)
await piscina.broadcastTask({ task: 'reloadPlugins' })
await delay(2000)
const historicalEvents = await hub.db.fetchEvents()
expect(historicalEvents.length).toBe(4)
const exportedEventsCountBeforeJob = testConsole
.read()
.filter((log) => log[0] === 'exported historical event').length
expect(exportedEventsCountBeforeJob).toEqual(0)
// TODO: trigger job via graphile here
await delayUntilEventIngested(awaitHistoricalEventLogs as any, 4, 1000, 50)
const exportLogs = testConsole.read().filter((log) => log[0] === 'exported historical event')
const exportedEventsCountAfterJob = exportLogs.length
const exportedEvents = exportLogs.map((log) => log[1])
expect(exportedEventsCountAfterJob).toEqual(4)
expect(exportedEvents.map((e) => e.event)).toEqual(
expect.arrayContaining(['historicalEvent1', 'historicalEvent2', 'historicalEvent3', 'historicalEvent4'])
)
expect(Object.keys(exportedEvents[0].properties)).toEqual(
expect.arrayContaining([
'$$historical_export_source_db',
'$$is_historical_export_event',
'$$historical_export_timestamp',
])
)
expect(exportedEvents[0].properties['$$historical_export_source_db']).toEqual('clickhouse')
})
test('export historical events with specified timestamp boundaries', async () => {
await posthog.capture('historicalEvent1')
await posthog.capture('historicalEvent2')
await posthog.capture('historicalEvent3')
await posthog.capture('historicalEvent4')
await delayUntilEventIngested(() => hub.db.fetchEvents(), 4)
const historicalEvents = await hub.db.fetchEvents()
expect(historicalEvents.length).toBe(4)
const exportedEventsCountBeforeJob = testConsole
.read()
.filter((log) => log[0] === 'exported historical event').length
expect(exportedEventsCountBeforeJob).toEqual(0)
// TODO: trigger job via graphile here
await delayUntilEventIngested(awaitHistoricalEventLogs as any, 4, 1000)
const exportLogs = testConsole.read().filter((log) => log[0] === 'exported historical event')
const exportedEventsCountAfterJob = exportLogs.length
const exportedEvents = exportLogs.map((log) => log[1])
expect(exportedEventsCountAfterJob).toEqual(4)
expect(exportedEvents.map((e) => e.event)).toEqual(
expect.arrayContaining(['historicalEvent1', 'historicalEvent2', 'historicalEvent3', 'historicalEvent4'])
)
expect(Object.keys(exportedEvents[0].properties)).toEqual(
expect.arrayContaining([
'$$historical_export_source_db',
'$$is_historical_export_event',
'$$historical_export_timestamp',
])
)
expect(exportedEvents[0].properties['$$historical_export_source_db']).toEqual('clickhouse')
})
test('correct $elements included in historical event', async () => {
const properties = {
$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: '💻' },
],
}
await posthog.capture('$autocapture', properties)
await delayUntilEventIngested(() => hub.db.fetchEvents(), 1)
const historicalEvents = await hub.db.fetchEvents()
expect(historicalEvents.length).toBe(1)
// TODO: trigger job via graphile here
const exportLogs = testConsole.read().filter((log) => log[0] === 'exported historical event')
const exportedEventsCountAfterJob = exportLogs.length
const exportedEvents = exportLogs.map((log) => log[1])
expect(exportedEventsCountAfterJob).toEqual(1)
expect(exportedEvents.map((e) => e.event)).toEqual(['$autocapture'])
expect(Object.keys(exportedEvents[0].properties)).toEqual(
expect.arrayContaining([
'$$historical_export_source_db',
'$$is_historical_export_event',
'$$historical_export_timestamp',
])
)
expect(exportedEvents[0].properties['$elements']).toEqual([
{
attr_class: 'btn btn-sm',
attributes: { attr__class: 'btn btn-sm' },
nth_child: 1,
nth_of_type: 2,
order: 0,
tag_name: 'a',
},
{ $el_text: '💻', attributes: {}, nth_child: 1, nth_of_type: 2, order: 1, tag_name: 'div', text: '💻' },
])
})
})
})