mirror of
https://github.com/PostHog/posthog.git
synced 2024-11-24 18:07:17 +01:00
chore: plugin-server updates for exception handling (#26276)
This commit is contained in:
parent
2353cacb28
commit
53b600d37e
@ -1,83 +0,0 @@
|
|||||||
import { captureException } from '@sentry/node'
|
|
||||||
import { Counter } from 'prom-client'
|
|
||||||
|
|
||||||
import { PreIngestionEvent } from '../../../types'
|
|
||||||
import { EventPipelineRunner } from './runner'
|
|
||||||
|
|
||||||
const EXTERNAL_FINGERPRINT_COUNTER = new Counter({
|
|
||||||
name: 'enrich_exception_events_external_fingerprint',
|
|
||||||
help: 'Counter for exceptions that already have a fingerprint',
|
|
||||||
})
|
|
||||||
|
|
||||||
const COULD_NOT_PARSE_STACK_TRACE_COUNTER = new Counter({
|
|
||||||
name: 'enrich_exception_events_could_not_parse_stack_trace',
|
|
||||||
help: 'Counter for exceptions where the stack trace could not be parsed',
|
|
||||||
})
|
|
||||||
|
|
||||||
const COULD_NOT_PREPARE_FOR_FINGERPRINTING_COUNTER = new Counter({
|
|
||||||
name: 'enrich_exception_events_could_not_prepare_for_fingerprinting',
|
|
||||||
help: 'Counter for exceptions where the event could not be prepared for fingerprinting',
|
|
||||||
})
|
|
||||||
|
|
||||||
const EXCEPTIONS_ENRICHED_COUNTER = new Counter({
|
|
||||||
name: 'enrich_exception_events_enriched',
|
|
||||||
help: 'Counter for exceptions that have been enriched',
|
|
||||||
})
|
|
||||||
|
|
||||||
export function enrichExceptionEventStep(
|
|
||||||
_runner: EventPipelineRunner,
|
|
||||||
event: PreIngestionEvent
|
|
||||||
): Promise<PreIngestionEvent> {
|
|
||||||
if (event.event !== '$exception') {
|
|
||||||
return Promise.resolve(event)
|
|
||||||
}
|
|
||||||
|
|
||||||
let type: string | null = null
|
|
||||||
let message: string | null = null
|
|
||||||
let firstFunction: string | null = null
|
|
||||||
let exceptionList: any[] | null = null
|
|
||||||
|
|
||||||
try {
|
|
||||||
exceptionList = event.properties['$exception_list']
|
|
||||||
const fingerPrint = event.properties['$exception_fingerprint']
|
|
||||||
type = event.properties['$exception_type']
|
|
||||||
message = event.properties['$exception_message']
|
|
||||||
|
|
||||||
if (!type && exceptionList && exceptionList.length > 0) {
|
|
||||||
type = exceptionList[0].type
|
|
||||||
}
|
|
||||||
if (!message && exceptionList && exceptionList.length > 0) {
|
|
||||||
message = exceptionList[0].value
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fingerPrint) {
|
|
||||||
EXTERNAL_FINGERPRINT_COUNTER.inc()
|
|
||||||
return Promise.resolve(event)
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
captureException(e)
|
|
||||||
COULD_NOT_PREPARE_FOR_FINGERPRINTING_COUNTER.inc()
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (exceptionList && exceptionList.length > 0) {
|
|
||||||
const firstException = exceptionList[0]
|
|
||||||
if (firstException.stacktrace) {
|
|
||||||
// TODO: Should this be the last function instead?, or first in app function?
|
|
||||||
firstFunction = firstException.stacktrace.frames[0].function
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
captureException(e)
|
|
||||||
COULD_NOT_PARSE_STACK_TRACE_COUNTER.inc()
|
|
||||||
}
|
|
||||||
|
|
||||||
const fingerprint = [type, message, firstFunction].filter(Boolean)
|
|
||||||
event.properties['$exception_fingerprint'] = fingerprint.length ? fingerprint : undefined
|
|
||||||
|
|
||||||
if (event.properties['$exception_fingerprint']) {
|
|
||||||
EXCEPTIONS_ENRICHED_COUNTER.inc()
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.resolve(event)
|
|
||||||
}
|
|
@ -9,7 +9,7 @@ export function produceExceptionSymbolificationEventStep(
|
|||||||
const ack = runner.hub.kafkaProducer
|
const ack = runner.hub.kafkaProducer
|
||||||
.produce({
|
.produce({
|
||||||
topic: runner.hub.EXCEPTIONS_SYMBOLIFICATION_KAFKA_TOPIC,
|
topic: runner.hub.EXCEPTIONS_SYMBOLIFICATION_KAFKA_TOPIC,
|
||||||
key: event.uuid,
|
key: String(event.team_id),
|
||||||
value: Buffer.from(JSON.stringify(event)),
|
value: Buffer.from(JSON.stringify(event)),
|
||||||
waitForAck: true,
|
waitForAck: true,
|
||||||
})
|
})
|
||||||
|
@ -12,7 +12,6 @@ import { EventsProcessor } from '../process-event'
|
|||||||
import { captureIngestionWarning, generateEventDeadLetterQueueMessage } from '../utils'
|
import { captureIngestionWarning, generateEventDeadLetterQueueMessage } from '../utils'
|
||||||
import { createEventStep } from './createEventStep'
|
import { createEventStep } from './createEventStep'
|
||||||
import { emitEventStep } from './emitEventStep'
|
import { emitEventStep } from './emitEventStep'
|
||||||
import { enrichExceptionEventStep } from './enrichExceptionEventStep'
|
|
||||||
import { extractHeatmapDataStep } from './extractHeatmapDataStep'
|
import { extractHeatmapDataStep } from './extractHeatmapDataStep'
|
||||||
import {
|
import {
|
||||||
eventProcessedAndIngestedCounter,
|
eventProcessedAndIngestedCounter,
|
||||||
@ -254,15 +253,9 @@ export class EventPipelineRunner {
|
|||||||
heatmapKafkaAcks.forEach((ack) => kafkaAcks.push(ack))
|
heatmapKafkaAcks.forEach((ack) => kafkaAcks.push(ack))
|
||||||
}
|
}
|
||||||
|
|
||||||
const enrichedIfErrorEvent = await this.runStep(
|
|
||||||
enrichExceptionEventStep,
|
|
||||||
[this, preparedEventWithoutHeatmaps],
|
|
||||||
event.team_id
|
|
||||||
)
|
|
||||||
|
|
||||||
const rawEvent = await this.runStep(
|
const rawEvent = await this.runStep(
|
||||||
createEventStep,
|
createEventStep,
|
||||||
[this, enrichedIfErrorEvent, person, processPerson],
|
[this, preparedEventWithoutHeatmaps, person, processPerson],
|
||||||
event.team_id
|
event.team_id
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -96,22 +96,6 @@ Array [
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
Array [
|
|
||||||
"enrichExceptionEventStep",
|
|
||||||
Array [
|
|
||||||
Object {
|
|
||||||
"distinctId": "my_id",
|
|
||||||
"elementsList": Array [],
|
|
||||||
"event": "$pageview",
|
|
||||||
"eventUuid": "uuid1",
|
|
||||||
"ip": "127.0.0.1",
|
|
||||||
"projectId": 1,
|
|
||||||
"properties": Object {},
|
|
||||||
"teamId": 2,
|
|
||||||
"timestamp": "2020-02-23T02:15:00.000Z",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
],
|
|
||||||
Array [
|
Array [
|
||||||
"createEventStep",
|
"createEventStep",
|
||||||
Array [
|
Array [
|
||||||
|
@ -1,161 +0,0 @@
|
|||||||
import { ISOTimestamp, PreIngestionEvent } from '../../../../src/types'
|
|
||||||
import { cloneObject } from '../../../../src/utils/utils'
|
|
||||||
import { enrichExceptionEventStep } from '../../../../src/worker/ingestion/event-pipeline/enrichExceptionEventStep'
|
|
||||||
|
|
||||||
jest.mock('../../../../src/worker/plugins/run')
|
|
||||||
|
|
||||||
const DEFAULT_EXCEPTION_LIST = [
|
|
||||||
{
|
|
||||||
mechanism: {
|
|
||||||
handled: true,
|
|
||||||
type: 'generic',
|
|
||||||
synthetic: false,
|
|
||||||
},
|
|
||||||
stacktrace: {
|
|
||||||
frames: [
|
|
||||||
{
|
|
||||||
colno: 220,
|
|
||||||
filename: 'https://app-static-prod.posthog.com/static/chunk-UFQKIDIH.js',
|
|
||||||
function: 'submitZendeskTicket',
|
|
||||||
in_app: true,
|
|
||||||
lineno: 25,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
type: 'Error',
|
|
||||||
value: 'There was an error creating the support ticket with zendesk.',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
const preIngestionEvent: PreIngestionEvent = {
|
|
||||||
eventUuid: '018eebf3-cb48-750b-bfad-36409ea6f2b2',
|
|
||||||
event: '$exception',
|
|
||||||
distinctId: '018eebf3-79b1-7082-a7c6-eeb56a36002f',
|
|
||||||
properties: {
|
|
||||||
$current_url: 'http://localhost:3000/',
|
|
||||||
$host: 'localhost:3000',
|
|
||||||
$pathname: '/',
|
|
||||||
$viewport_height: 1328,
|
|
||||||
$viewport_width: 1071,
|
|
||||||
$device_id: '018eebf3-79b1-7082-a7c6-eeb56a36002f',
|
|
||||||
$session_id: '018eebf3-79cd-70da-895f-b6cf352bd688',
|
|
||||||
$window_id: '018eebf3-79cd-70da-895f-b6d09add936a',
|
|
||||||
},
|
|
||||||
timestamp: '2024-04-17T12:06:46.861Z' as ISOTimestamp,
|
|
||||||
teamId: 1,
|
|
||||||
projectId: 1,
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('enrichExceptionEvent()', () => {
|
|
||||||
let runner: any
|
|
||||||
let event: PreIngestionEvent
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
event = cloneObject(preIngestionEvent)
|
|
||||||
runner = {
|
|
||||||
hub: {
|
|
||||||
kafkaProducer: {
|
|
||||||
produce: jest.fn((e) => Promise.resolve(e)),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
it('ignores non-exception events - even if they have a stack trace', async () => {
|
|
||||||
event.event = 'not_exception'
|
|
||||||
event.properties['$exception_list'] = DEFAULT_EXCEPTION_LIST
|
|
||||||
expect(event.properties['$exception_fingerprint']).toBeUndefined()
|
|
||||||
|
|
||||||
const response = await enrichExceptionEventStep(runner, event)
|
|
||||||
expect(response).toBe(event)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('use a fingerprint if it is present', async () => {
|
|
||||||
event.event = '$exception'
|
|
||||||
event.properties['$exception_list'] = DEFAULT_EXCEPTION_LIST
|
|
||||||
|
|
||||||
event.properties['$exception_fingerprint'] = 'some-fingerprint'
|
|
||||||
|
|
||||||
const response = await enrichExceptionEventStep(runner, event)
|
|
||||||
|
|
||||||
expect(response.properties['$exception_fingerprint']).toBe('some-fingerprint')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('uses the message and stack trace as the simplest grouping', async () => {
|
|
||||||
event.event = '$exception'
|
|
||||||
event.properties['$exception_message'] = 'some-message'
|
|
||||||
event.properties['$exception_list'] = DEFAULT_EXCEPTION_LIST
|
|
||||||
|
|
||||||
const response = await enrichExceptionEventStep(runner, event)
|
|
||||||
|
|
||||||
expect(response.properties['$exception_fingerprint']).toStrictEqual([
|
|
||||||
'Error',
|
|
||||||
'some-message',
|
|
||||||
'submitZendeskTicket',
|
|
||||||
])
|
|
||||||
})
|
|
||||||
|
|
||||||
it('includes type in stack grouping when present', async () => {
|
|
||||||
event.event = '$exception'
|
|
||||||
event.properties['$exception_message'] = 'some-message'
|
|
||||||
event.properties['$exception_list'] = DEFAULT_EXCEPTION_LIST
|
|
||||||
event.properties['$exception_type'] = 'UnhandledRejection'
|
|
||||||
|
|
||||||
const response = await enrichExceptionEventStep(runner, event)
|
|
||||||
|
|
||||||
expect(response.properties['$exception_fingerprint']).toStrictEqual([
|
|
||||||
'UnhandledRejection',
|
|
||||||
'some-message',
|
|
||||||
'submitZendeskTicket',
|
|
||||||
])
|
|
||||||
})
|
|
||||||
|
|
||||||
it('falls back to message and type when no stack trace', async () => {
|
|
||||||
event.event = '$exception'
|
|
||||||
event.properties['$exception_message'] = 'some-message'
|
|
||||||
event.properties['$exception_list'] = null
|
|
||||||
event.properties['$exception_type'] = 'UnhandledRejection'
|
|
||||||
|
|
||||||
const response = await enrichExceptionEventStep(runner, event)
|
|
||||||
|
|
||||||
expect(response.properties['$exception_fingerprint']).toStrictEqual(['UnhandledRejection', 'some-message'])
|
|
||||||
})
|
|
||||||
|
|
||||||
it('adds no fingerprint if no qualifying properties', async () => {
|
|
||||||
event.event = '$exception'
|
|
||||||
event.properties['$exception_message'] = null
|
|
||||||
event.properties['$exception_list'] = null
|
|
||||||
event.properties['$exception_type'] = null
|
|
||||||
|
|
||||||
const response = await enrichExceptionEventStep(runner, event)
|
|
||||||
|
|
||||||
expect(response.properties['$exception_fingerprint']).toBeUndefined()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('uses exception_list to generate message, type, and fingerprint when not present', async () => {
|
|
||||||
event.event = '$exception'
|
|
||||||
event.properties['$exception_list'] = DEFAULT_EXCEPTION_LIST
|
|
||||||
|
|
||||||
const response = await enrichExceptionEventStep(runner, event)
|
|
||||||
|
|
||||||
expect(response.properties['$exception_fingerprint']).toStrictEqual([
|
|
||||||
'Error',
|
|
||||||
'There was an error creating the support ticket with zendesk.',
|
|
||||||
'submitZendeskTicket',
|
|
||||||
])
|
|
||||||
})
|
|
||||||
|
|
||||||
it('exception_type overrides exception_list to generate fingerprint when present', async () => {
|
|
||||||
event.event = '$exception'
|
|
||||||
event.properties['$exception_list'] = DEFAULT_EXCEPTION_LIST
|
|
||||||
event.properties['$exception_type'] = 'UnhandledRejection'
|
|
||||||
|
|
||||||
const response = await enrichExceptionEventStep(runner, event)
|
|
||||||
|
|
||||||
expect(response.properties['$exception_fingerprint']).toStrictEqual([
|
|
||||||
'UnhandledRejection',
|
|
||||||
'There was an error creating the support ticket with zendesk.',
|
|
||||||
'submitZendeskTicket',
|
|
||||||
])
|
|
||||||
})
|
|
||||||
})
|
|
@ -145,7 +145,6 @@ describe('EventPipelineRunner', () => {
|
|||||||
'processPersonsStep',
|
'processPersonsStep',
|
||||||
'prepareEventStep',
|
'prepareEventStep',
|
||||||
'extractHeatmapDataStep',
|
'extractHeatmapDataStep',
|
||||||
'enrichExceptionEventStep',
|
|
||||||
'createEventStep',
|
'createEventStep',
|
||||||
'emitEventStep',
|
'emitEventStep',
|
||||||
])
|
])
|
||||||
@ -175,7 +174,6 @@ describe('EventPipelineRunner', () => {
|
|||||||
'processPersonsStep',
|
'processPersonsStep',
|
||||||
'prepareEventStep',
|
'prepareEventStep',
|
||||||
'extractHeatmapDataStep',
|
'extractHeatmapDataStep',
|
||||||
'enrichExceptionEventStep',
|
|
||||||
'createEventStep',
|
'createEventStep',
|
||||||
'emitEventStep',
|
'emitEventStep',
|
||||||
])
|
])
|
||||||
@ -199,7 +197,7 @@ describe('EventPipelineRunner', () => {
|
|||||||
const result = await runner.runEventPipeline(pipelineEvent)
|
const result = await runner.runEventPipeline(pipelineEvent)
|
||||||
expect(result.error).toBeUndefined()
|
expect(result.error).toBeUndefined()
|
||||||
|
|
||||||
expect(pipelineStepMsSummarySpy).toHaveBeenCalledTimes(9)
|
expect(pipelineStepMsSummarySpy).toHaveBeenCalledTimes(8)
|
||||||
expect(pipelineLastStepCounterSpy).toHaveBeenCalledTimes(1)
|
expect(pipelineLastStepCounterSpy).toHaveBeenCalledTimes(1)
|
||||||
expect(eventProcessedAndIngestedCounterSpy).toHaveBeenCalledTimes(1)
|
expect(eventProcessedAndIngestedCounterSpy).toHaveBeenCalledTimes(1)
|
||||||
expect(pipelineStepMsSummarySpy).toHaveBeenCalledWith('emitEventStep')
|
expect(pipelineStepMsSummarySpy).toHaveBeenCalledWith('emitEventStep')
|
||||||
@ -398,7 +396,6 @@ describe('EventPipelineRunner', () => {
|
|||||||
'processPersonsStep',
|
'processPersonsStep',
|
||||||
'prepareEventStep',
|
'prepareEventStep',
|
||||||
'extractHeatmapDataStep',
|
'extractHeatmapDataStep',
|
||||||
'enrichExceptionEventStep',
|
|
||||||
'createEventStep',
|
'createEventStep',
|
||||||
'emitEventStep',
|
'emitEventStep',
|
||||||
])
|
])
|
||||||
|
Loading…
Reference in New Issue
Block a user