-
Notifications
You must be signed in to change notification settings - Fork 1.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
refactor(plugin-server): refactor the event pipeline #9829
Changes from all commits
20dc8b8
7e26d78
6988d0c
9d28061
8bafa40
f5c88cd
9f5f764
05901a1
14c6a7a
d897eaf
40a0aa7
2cac16c
6766a5d
bfbbc84
c8a1537
f3b81e9
1bc00f5
a29ab5f
1752b6f
ccdb921
464f17b
e679d72
5b29842
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import { Person, PreIngestionEvent } from '../../../types' | ||
import { EventPipelineRunner, StepResult } from './runner' | ||
|
||
export async function createEventStep( | ||
runner: EventPipelineRunner, | ||
event: PreIngestionEvent, | ||
person: Person | undefined | ||
): Promise<StepResult> { | ||
const [, , elements] = await runner.hub.eventsProcessor.createEvent(event) | ||
return runner.nextStep('runAsyncHandlersStep', event, person, elements) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
import { DateTime } from 'luxon' | ||
|
||
import { Hub, Person, PreIngestionEvent, TeamId } from '../../../types' | ||
import { EventPipelineRunner, StepResult } from './runner' | ||
|
||
export async function emitToBufferStep( | ||
runner: EventPipelineRunner, | ||
event: PreIngestionEvent, | ||
shouldBuffer: ( | ||
hub: Hub, | ||
event: PreIngestionEvent, | ||
person: Person | undefined, | ||
teamId: TeamId | ||
) => boolean = shouldSendEventToBuffer | ||
): Promise<StepResult> { | ||
const person = await runner.hub.db.fetchPerson(event.teamId, event.distinctId) | ||
|
||
if (shouldBuffer(runner.hub, event, person, event.teamId)) { | ||
await runner.hub.eventsProcessor.produceEventToBuffer(event) | ||
return null | ||
} else { | ||
return runner.nextStep('createEventStep', event, person) | ||
} | ||
} | ||
|
||
// context: https://github.com/PostHog/posthog/issues/9182 | ||
// TL;DR: events from a recently created non-anonymous person are sent to a buffer | ||
// because their person_id might change. We merge based on the person_id of the anonymous user | ||
// so ingestion is delayed for those events to increase our chances of getting person_id correctly | ||
export function shouldSendEventToBuffer( | ||
hub: Hub, | ||
event: PreIngestionEvent, | ||
person: Person | undefined, | ||
teamId: TeamId | ||
): boolean { | ||
const isAnonymousEvent = | ||
event.properties && event.properties['$device_id'] && event.distinctId === event.properties['$device_id'] | ||
const isRecentPerson = | ||
!person || DateTime.now().diff(person.created_at).as('seconds') < hub.BUFFER_CONVERSION_SECONDS | ||
const ingestEventDirectly = isAnonymousEvent || event.event === '$identify' || !isRecentPerson | ||
const sendToBuffer = !ingestEventDirectly | ||
|
||
if (sendToBuffer) { | ||
hub.statsd?.increment('conversion_events_buffer_size', { teamId: event.teamId.toString() }) | ||
} | ||
|
||
if (!hub.CONVERSION_BUFFER_ENABLED && !hub.conversionBufferEnabledTeams.has(teamId)) { | ||
return false | ||
} | ||
|
||
return sendToBuffer | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
import { PluginEvent } from '@posthog/plugin-scaffold' | ||
|
||
import { runInstrumentedFunction } from '../../../main/utils' | ||
import { runProcessEvent } from '../../plugins/run' | ||
import { EventPipelineRunner, StepResult } from './runner' | ||
|
||
export async function pluginsProcessEventStep(runner: EventPipelineRunner, event: PluginEvent): Promise<StepResult> { | ||
let processedEvent: PluginEvent | null = event | ||
|
||
// run processEvent on all events that are not $snapshot | ||
if (event.event !== '$snapshot') { | ||
processedEvent = await runInstrumentedFunction({ | ||
server: runner.hub, | ||
event, | ||
func: (event) => runProcessEvent(runner.hub, event), | ||
statsKey: 'kafka_queue.single_event', | ||
timeoutMessage: 'Still running plugins on event. Timeout warning after 30 sec!', | ||
}) | ||
} | ||
|
||
if (processedEvent) { | ||
return runner.nextStep('prepareEventStep', processedEvent) | ||
} else { | ||
// processEvent might not return an event. This is expected and plugins, e.g. downsample plugin uses it. | ||
runner.hub.statsd?.increment('kafka_queue.dropped_event', { | ||
teamID: String(event.team_id), | ||
}) | ||
return null | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import { PluginEvent } from '@posthog/plugin-scaffold' | ||
import { DateTime } from 'luxon' | ||
|
||
import { EventPipelineRunner, StepResult } from './runner' | ||
|
||
export async function prepareEventStep(runner: EventPipelineRunner, event: PluginEvent): Promise<StepResult> { | ||
const { ip, site_url, team_id, now, sent_at, uuid } = event | ||
const distinctId = String(event.distinct_id) | ||
const preIngestionEvent = await runner.hub.eventsProcessor.processEvent( | ||
distinctId, | ||
ip, | ||
event, | ||
team_id, | ||
DateTime.fromISO(now), | ||
sent_at ? DateTime.fromISO(sent_at) : null, | ||
uuid!, // it will throw if it's undefined, | ||
site_url | ||
) | ||
|
||
if (preIngestionEvent && preIngestionEvent.event !== '$snapshot') { | ||
return runner.nextStep('emitToBufferStep', preIngestionEvent) | ||
} else if (preIngestionEvent && preIngestionEvent.event === '$snapshot') { | ||
return runner.nextStep('runAsyncHandlersStep', preIngestionEvent, undefined, undefined) | ||
} else { | ||
return null | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
import { runInstrumentedFunction } from '../../../main/utils' | ||
import { Action, Element, Person, PreIngestionEvent } from '../../../types' | ||
import { convertToProcessedPluginEvent } from '../../../utils/event' | ||
import { runOnAction, runOnEvent, runOnSnapshot } from '../../plugins/run' | ||
import { EventPipelineRunner, StepResult } from './runner' | ||
|
||
export async function runAsyncHandlersStep( | ||
runner: EventPipelineRunner, | ||
event: PreIngestionEvent, | ||
person: Person | undefined, | ||
elements: Element[] | undefined | ||
): Promise<StepResult> { | ||
const promises = [] | ||
let actionMatches: Action[] = [] | ||
if (event.event !== '$snapshot') { | ||
actionMatches = await runner.hub.actionMatcher.match(event, person, elements) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can we actually run the will anyway group code together that's used together There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure what you mean. Do you want to ignore errors from action matching when deciding whether to run onAction? That's a new requirement if so. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No - let me submit a suggestion as to what I want There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I mistyped my question - onEvent calling should not be affected by action-related errors? If so, I think re-ordering is too implicit about that and we should make that obvious in the code. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's resolve this in a follow-up PR - I'm in merge conflict hell until this is in. |
||
promises.push(runner.hub.hookCannon.findAndFireHooks(event, person, event.siteUrl, actionMatches)) | ||
} | ||
|
||
const processedPluginEvent = convertToProcessedPluginEvent(event) | ||
const isSnapshot = event.event === '$snapshot' | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we could move this up given we have a |
||
const method = isSnapshot ? runOnSnapshot : runOnEvent | ||
promises.push( | ||
runInstrumentedFunction({ | ||
server: runner.hub, | ||
event: processedPluginEvent, | ||
func: (event) => method(runner.hub, event), | ||
statsKey: `kafka_queue.single_${isSnapshot ? 'on_snapshot' : 'on_event'}`, | ||
timeoutMessage: `After 30 seconds still running ${isSnapshot ? 'onSnapshot' : 'onEvent'}`, | ||
}) | ||
) | ||
for (const actionMatch of actionMatches) { | ||
promises.push( | ||
runInstrumentedFunction({ | ||
server: runner.hub, | ||
event: processedPluginEvent, | ||
func: (event) => runOnAction(runner.hub, actionMatch, event), | ||
statsKey: `kafka_queue.on_action`, | ||
timeoutMessage: 'After 30 seconds still running onAction', | ||
}) | ||
) | ||
} | ||
|
||
await Promise.all(promises) | ||
|
||
return null | ||
macobo marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Intentional - I was trying to make it clearer to the reader it's dealing with a snapshot here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fair