diff --git a/cypress/e2e/error-tracking.cy.ts b/cypress/e2e/error-tracking.cy.ts index 2b58ede97..94c685b11 100644 --- a/cypress/e2e/error-tracking.cy.ts +++ b/cypress/e2e/error-tracking.cy.ts @@ -17,12 +17,11 @@ describe('Exception capture', () => { cy.phCaptures({ full: true }).then((captures) => { expect(captures.map((c) => c.event)).to.deep.equal(['$pageview', '$autocapture', '$exception']) expect(captures[2].event).to.be.eql('$exception') - expect(captures[2].properties.$exception_message).to.be.eql('wat even am I') - expect(captures[2].properties.$exception_type).to.be.eql('Error') expect(captures[2].properties.extra_prop).to.be.eql(2) expect(captures[2].properties.$exception_source).to.eql(undefined) expect(captures[2].properties.$exception_personURL).to.eql(undefined) - expect(captures[2].properties.$exception_stack_trace_raw).not.to.exist + expect(captures[2].properties.$exception_list[0].value).to.be.eql('wat even am I') + expect(captures[2].properties.$exception_list[0].type).to.be.eql('Error') }) }) @@ -51,11 +50,9 @@ describe('Exception capture', () => { cy.phCaptures({ full: true }).then((captures) => { expect(captures.map((c) => c.event)).to.deep.equal(['$pageview', '$autocapture', '$exception']) expect(captures[2].event).to.be.eql('$exception') - expect(captures[2].properties.$exception_message).to.be.eql('This is an error') - expect(captures[2].properties.$exception_type).to.be.eql('Error') - expect(captures[2].properties.$exception_source).to.match( - /http:\/\/localhost:\d+\/playground\/cypress\// - ) + expect(captures[2].properties.$exception_list[0].value).to.be.eql('This is an error') + expect(captures[2].properties.$exception_list[0].type).to.be.eql('Error') + expect(captures[2].properties.$exception_personURL).to.match( /http:\/\/localhost:\d+\/project\/test_token\/person\/[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}/ ) @@ -69,8 +66,8 @@ describe('Exception capture', () => { cy.wait(1500) cy.phCaptures({ full: true }).then((captures) => { - expect(captures[2].properties.$exception_message).to.be.eql('wat even am I') - expect(captures[2].properties.$exception_stack_trace_raw).to.exist + expect(captures[2].properties.$exception_list).to.exist + expect(captures[2].properties.$exception_list[0].value).to.be.eql('wat even am I') }) }) }) diff --git a/src/__tests__/extensions/exception-autocapture/error-conversion.test.ts b/src/__tests__/extensions/exception-autocapture/error-conversion.test.ts index 181ff01c4..16c25c83c 100644 --- a/src/__tests__/extensions/exception-autocapture/error-conversion.test.ts +++ b/src/__tests__/extensions/exception-autocapture/error-conversion.test.ts @@ -1,13 +1,13 @@ /* eslint-disable compat/compat */ import { + ErrorProperties, errorToProperties, unhandledRejectionToProperties, } from '../../../extensions/exception-autocapture/error-conversion' import { isNull } from '../../../utils/type-utils' import { expect } from '@jest/globals' -import { ErrorProperties } from '../../../types' // ugh, jest // can't reference PromiseRejectionEvent to construct it 🤷 @@ -34,30 +34,42 @@ export class PromiseRejectionEvent extends Event { describe('Error conversion', () => { it('should convert a string to an error', () => { const expected: ErrorProperties = { - $exception_type: 'InternalError', - $exception_message: 'but somehow still a string', - $exception_is_synthetic: true, $exception_level: 'error', + $exception_list: [ + { + type: 'InternalError', + value: 'but somehow still a string', + mechanism: { synthetic: true, handled: true }, + }, + ], } expect(errorToProperties(['Uncaught exception: InternalError: but somehow still a string'])).toEqual(expected) }) it('should convert a plain object to an error', () => { const expected: ErrorProperties = { - $exception_type: 'Error', - $exception_message: 'Non-Error exception captured with keys: foo, string', - $exception_is_synthetic: true, $exception_level: 'error', + $exception_list: [ + { + type: 'Error', + value: 'Non-Error exception captured with keys: foo, string', + mechanism: { synthetic: true, handled: true }, + }, + ], } expect(errorToProperties([{ string: 'candidate', foo: 'bar' } as unknown as Event])).toEqual(expected) }) it('should convert a plain Event to an error', () => { const expected: ErrorProperties = { - $exception_type: 'MouseEvent', - $exception_message: 'Non-Error exception captured with keys: isTrusted', - $exception_is_synthetic: true, $exception_level: 'error', + $exception_list: [ + { + type: 'MouseEvent', + value: 'Non-Error exception captured with keys: isTrusted', + mechanism: { synthetic: true, handled: true }, + }, + ], } const event = new MouseEvent('click', { bubbles: true, cancelable: true, composed: true }) expect(errorToProperties([event])).toEqual(expected) @@ -71,13 +83,16 @@ describe('Error conversion', () => { throw new Error("this mustn't be null") } - expect(Object.keys(errorProperties)).toHaveLength(4) - expect(errorProperties.$exception_type).toEqual('Error') - expect(errorProperties.$exception_message).toEqual('oh no an error has happened') + expect(Object.keys(errorProperties)).toHaveLength(2) expect(errorProperties.$exception_level).toEqual('error') // the stack trace changes between runs, so we just check that it's there - expect(errorProperties.$exception_stack_trace_raw).toBeDefined() - expect(errorProperties.$exception_stack_trace_raw).toContain('{"filename') + expect(errorProperties.$exception_list).toBeDefined() + expect(errorProperties.$exception_list[0].type).toEqual('Error') + expect(errorProperties.$exception_list[0].value).toEqual('oh no an error has happened') + expect(errorProperties.$exception_list[0].stacktrace.frames[0].in_app).toEqual(true) + expect(errorProperties.$exception_list[0].stacktrace.frames[0].filename).toBeDefined() + expect(errorProperties.$exception_list[0].mechanism.synthetic).toEqual(false) + expect(errorProperties.$exception_list[0].mechanism.handled).toEqual(true) }) class FakeDomError { @@ -87,9 +102,14 @@ describe('Error conversion', () => { it('should convert a DOM Error to an error', () => { const expected: ErrorProperties = { - $exception_type: 'DOMError', - $exception_message: 'click: foo', $exception_level: 'error', + $exception_list: [ + { + type: 'DOMError', + value: 'click: foo', + mechanism: { synthetic: true, handled: true }, + }, + ], } const event = new FakeDomError('click', 'foo') expect(errorToProperties([event as unknown as Event])).toEqual(expected) @@ -103,13 +123,15 @@ describe('Error conversion', () => { throw new Error("this mustn't be null") } - expect(Object.keys(errorProperties)).toHaveLength(5) - expect(errorProperties.$exception_type).toEqual('dom-exception') - expect(errorProperties.$exception_message).toEqual('oh no disaster') + expect(Object.keys(errorProperties)).toHaveLength(3) + expect(errorProperties.$exception_list[0].type).toEqual('dom-exception') + expect(errorProperties.$exception_list[0].value).toEqual('oh no disaster') + expect(errorProperties.$exception_DOMException_code).toEqual('0') expect(errorProperties.$exception_level).toEqual('error') // the stack trace changes between runs, so we just check that it's there - expect(errorProperties.$exception_stack_trace_raw).toBeDefined() - expect(errorProperties.$exception_stack_trace_raw).toContain('{"filename') + expect(errorProperties.$exception_list).toBeDefined() + expect(errorProperties.$exception_list[0].stacktrace.frames[0].in_app).toEqual(true) + expect(errorProperties.$exception_list[0].stacktrace.frames[0].filename).toBeDefined() }) it('should convert an error event to an error', () => { @@ -120,24 +142,28 @@ describe('Error conversion', () => { throw new Error("this mustn't be null") } - expect(Object.keys(errorProperties)).toHaveLength(4) - expect(errorProperties.$exception_type).toEqual('Error') - expect(errorProperties.$exception_message).toEqual('the real error is hidden inside') + expect(Object.keys(errorProperties)).toHaveLength(2) + expect(errorProperties.$exception_list[0].type).toEqual('Error') + expect(errorProperties.$exception_list[0].value).toEqual('the real error is hidden inside') expect(errorProperties.$exception_level).toEqual('error') // the stack trace changes between runs, so we just check that it's there - expect(errorProperties.$exception_stack_trace_raw).toBeDefined() - expect(errorProperties.$exception_stack_trace_raw).toContain('{"filename') + expect(errorProperties.$exception_list).toBeDefined() + expect(errorProperties.$exception_list[0].stacktrace.frames[0].in_app).toEqual(true) + expect(errorProperties.$exception_list[0].stacktrace.frames[0].filename).toBeDefined() + expect(errorProperties.$exception_list[0].mechanism.synthetic).toEqual(false) + expect(errorProperties.$exception_list[0].mechanism.handled).toEqual(true) }) it('can convert source, lineno, colno', () => { const expected: ErrorProperties = { - $exception_colno: 200, - $exception_is_synthetic: true, - $exception_lineno: 12, - $exception_message: 'string candidate', - $exception_source: 'a source', - $exception_type: 'Error', $exception_level: 'error', + $exception_list: [ + { + type: 'Error', + value: 'string candidate', + mechanism: { synthetic: true, handled: true }, + }, + ], } expect(errorToProperties(['string candidate', 'a source', 12, 200])).toEqual(expected) }) @@ -152,14 +178,16 @@ describe('Error conversion', () => { const errorProperties: ErrorProperties = unhandledRejectionToProperties([ ce as unknown as PromiseRejectionEvent, ]) - expect(Object.keys(errorProperties)).toHaveLength(5) - expect(errorProperties.$exception_type).toEqual('UnhandledRejection') - expect(errorProperties.$exception_message).toEqual('a wrapped rejection event') - expect(errorProperties.$exception_handled).toEqual(false) + expect(Object.keys(errorProperties)).toHaveLength(2) + expect(errorProperties.$exception_list[0].type).toEqual('UnhandledRejection') + expect(errorProperties.$exception_list[0].value).toEqual('a wrapped rejection event') expect(errorProperties.$exception_level).toEqual('error') // the stack trace changes between runs, so we just check that it's there - expect(errorProperties.$exception_stack_trace_raw).toBeDefined() - expect(errorProperties.$exception_stack_trace_raw).toContain('{"filename') + expect(errorProperties.$exception_list).toBeDefined() + expect(errorProperties.$exception_list[0].stacktrace.frames[0].in_app).toEqual(true) + expect(errorProperties.$exception_list[0].stacktrace.frames[0].filename).toBeDefined() + expect(errorProperties.$exception_list[0].mechanism.synthetic).toEqual(false) + expect(errorProperties.$exception_list[0].mechanism.handled).toEqual(false) }) it('should convert unhandled promise rejection', () => { @@ -170,12 +198,13 @@ describe('Error conversion', () => { const errorProperties: ErrorProperties = unhandledRejectionToProperties([ pre as unknown as PromiseRejectionEvent, ]) - expect(Object.keys(errorProperties)).toHaveLength(4) - expect(errorProperties.$exception_type).toEqual('UnhandledRejection') - expect(errorProperties.$exception_message).toEqual( + expect(Object.keys(errorProperties)).toHaveLength(2) + expect(errorProperties.$exception_list[0].type).toEqual('UnhandledRejection') + expect(errorProperties.$exception_list[0].value).toEqual( 'Non-Error promise rejection captured with value: My house is on fire' ) - expect(errorProperties.$exception_handled).toEqual(false) expect(errorProperties.$exception_level).toEqual('error') + expect(errorProperties.$exception_list[0].mechanism.synthetic).toEqual(false) + expect(errorProperties.$exception_list[0].mechanism.handled).toEqual(false) }) }) diff --git a/src/__tests__/extensions/exception-autocapture/exception-observer.test.ts b/src/__tests__/extensions/exception-autocapture/exception-observer.test.ts index 42acf7292..492bae78b 100644 --- a/src/__tests__/extensions/exception-autocapture/exception-observer.test.ts +++ b/src/__tests__/extensions/exception-autocapture/exception-observer.test.ts @@ -97,9 +97,15 @@ describe('Exception Observer', () => { expect(singleCall[0]).toBe('$exception') expect(singleCall[1]).toMatchObject({ properties: { - $exception_message: 'test error', - $exception_type: 'Error', $exception_personURL: expect.any(String), + $exception_list: [ + { + type: 'Error', + value: 'test error', + stacktrace: { frames: expect.any(Array) }, + mechanism: { synthetic: false, handled: true }, + }, + ], }, }) }) @@ -120,9 +126,15 @@ describe('Exception Observer', () => { expect(singleCall[0]).toBe('$exception') expect(singleCall[1]).toMatchObject({ properties: { - $exception_message: 'test error', - $exception_type: 'UnhandledRejection', $exception_personURL: expect.any(String), + $exception_list: [ + { + type: 'UnhandledRejection', + value: 'test error', + stacktrace: { frames: expect.any(Array) }, + mechanism: { synthetic: false, handled: false }, + }, + ], }, }) }) @@ -137,10 +149,15 @@ describe('Exception Observer', () => { expect(request.data).toMatchObject({ event: '$exception', properties: { - $exception_message: 'test error', - $exception_type: 'Error', $exception_personURL: expect.any(String), - $exception_stack_trace_raw: expect.any(String), + $exception_list: [ + { + type: 'Error', + value: 'test error', + stacktrace: { frames: expect.any(Array) }, + mechanism: { synthetic: false, handled: true }, + }, + ], }, }) expect(request.batchKey).toBe('exceptionEvent') diff --git a/src/extensions/exception-autocapture/error-conversion.ts b/src/extensions/exception-autocapture/error-conversion.ts index 26398a9b5..9d2c1e19c 100644 --- a/src/extensions/exception-autocapture/error-conversion.ts +++ b/src/extensions/exception-autocapture/error-conversion.ts @@ -11,7 +11,44 @@ import { import { defaultStackParser, StackFrame } from './stack-trace' import { isEmptyString, isNumber, isString, isUndefined } from '../../utils/type-utils' -import { ErrorEventArgs, ErrorProperties, SeverityLevel, severityLevels } from '../../types' +import { ErrorEventArgs, ErrorMetadata, SeverityLevel, severityLevels } from '../../types' + +export interface ErrorProperties { + $exception_list: Exception[] + $exception_level?: SeverityLevel + $exception_DOMException_code?: string + $exception_personURL?: string +} + +export interface Exception { + type?: string + value?: string + mechanism?: { + /** + * In theory, whether or not the exception has been handled by the user. In practice, whether or not we see it before + * it hits the global error/rejection handlers, whether through explicit handling by the user or auto instrumentation. + */ + handled?: boolean + type?: string + source?: string + /** + * True when `captureException` is called with anything other than an instance of `Error` (or, in the case of browser, + * an instance of `ErrorEvent`, `DOMError`, or `DOMException`). causing us to create a synthetic error in an attempt + * to recreate the stacktrace. + */ + synthetic?: boolean + } + module?: string + thread_id?: number + stacktrace?: { + frames?: StackFrame[] + } +} + +export interface ErrorConversions { + errorToProperties: (args: ErrorEventArgs, metadata?: ErrorMetadata) => ErrorProperties + unhandledRejectionToProperties: (args: [ev: PromiseRejectionEvent]) => ErrorProperties +} /** * based on the very wonderful MIT licensed Sentry SDK @@ -53,21 +90,58 @@ export function parseStackFrames(ex: Error & { framesToPop?: number; stacktrace? return [] } -function errorPropertiesFromError(error: Error): ErrorProperties { +function errorPropertiesFromError(error: Error, metadata?: ErrorMetadata): ErrorProperties { const frames = parseStackFrames(error) + const handled = metadata?.handled ?? true + const synthetic = metadata?.synthetic ?? false + + const exceptionType = metadata?.overrideExceptionType ? metadata.overrideExceptionType : error.name + const exceptionMessage = metadata?.overrideExceptionMessage ? metadata.overrideExceptionMessage : error.message + return { - $exception_type: error.name, - $exception_message: error.message, - $exception_stack_trace_raw: JSON.stringify(frames), + $exception_list: [ + { + type: exceptionType, + value: exceptionMessage, + stacktrace: { + frames, + }, + mechanism: { + handled, + synthetic, + }, + }, + ], $exception_level: 'error', } } -function errorPropertiesFromString(candidate: string): ErrorProperties { +function errorPropertiesFromString(candidate: string, metadata?: ErrorMetadata): ErrorProperties { + // Defaults for metadata are based on what the error candidate is. + const handled = metadata?.handled ?? true + const synthetic = metadata?.synthetic ?? true + + const exceptionType = metadata?.overrideExceptionType + ? metadata.overrideExceptionType + : metadata?.defaultExceptionType ?? 'Error' + const exceptionMessage = metadata?.overrideExceptionMessage + ? metadata.overrideExceptionMessage + : candidate + ? candidate + : metadata?.defaultExceptionMessage + return { - $exception_type: 'Error', - $exception_message: candidate, + $exception_list: [ + { + type: exceptionType, + value: exceptionMessage, + mechanism: { + handled, + synthetic, + }, + }, + ], $exception_level: 'error', } } @@ -103,35 +177,41 @@ function isSeverityLevel(x: unknown): x is SeverityLevel { return isString(x) && !isEmptyString(x) && severityLevels.indexOf(x as SeverityLevel) >= 0 } -function errorPropertiesFromObject(candidate: Record): ErrorProperties { +function errorPropertiesFromObject(candidate: Record, metadata?: ErrorMetadata): ErrorProperties { + // Defaults for metadata are based on what the error candidate is. + const handled = metadata?.handled ?? true + const synthetic = metadata?.synthetic ?? true + + const exceptionType = metadata?.overrideExceptionType + ? metadata.overrideExceptionType + : isEvent(candidate) + ? candidate.constructor.name + : 'Error' + const exceptionMessage = metadata?.overrideExceptionMessage + ? metadata.overrideExceptionMessage + : `Non-Error ${'exception'} captured with keys: ${extractExceptionKeysForMessage(candidate)}` + return { - $exception_type: isEvent(candidate) ? candidate.constructor.name : 'Error', - $exception_message: `Non-Error ${'exception'} captured with keys: ${extractExceptionKeysForMessage(candidate)}`, + $exception_list: [ + { + type: exceptionType, + value: exceptionMessage, + mechanism: { + handled, + synthetic, + }, + }, + ], $exception_level: isSeverityLevel(candidate.level) ? candidate.level : 'error', } } -export function errorToProperties([event, source, lineno, colno, error]: ErrorEventArgs): ErrorProperties { - // some properties are not optional but, it's useful to start off without them enforced - let errorProperties: Omit & { - $exception_type?: string - $exception_message?: string - $exception_level?: string - } = {} - - if (isUndefined(error) && isString(event)) { - let name = 'Error' - let message = event - const groups = event.match(ERROR_TYPES_PATTERN) - if (groups) { - name = groups[1] - message = groups[2] - } - errorProperties = { - $exception_type: name, - $exception_message: message, - } - } +export function errorToProperties( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + [event, _, __, ___, error]: ErrorEventArgs, + metadata?: ErrorMetadata +): ErrorProperties { + let errorProperties: ErrorProperties = { $exception_list: [] } const candidate = error || event @@ -142,48 +222,45 @@ export function errorToProperties([event, source, lineno, colno, error]: ErrorEv const domException = candidate as unknown as DOMException if (isErrorWithStack(candidate)) { - errorProperties = errorPropertiesFromError(candidate as Error) + errorProperties = errorPropertiesFromError(candidate as Error, metadata) } else { const name = domException.name || (isDOMError(domException) ? 'DOMError' : 'DOMException') const message = domException.message ? `${name}: ${domException.message}` : name - errorProperties = errorPropertiesFromString(message) - errorProperties.$exception_type = isDOMError(domException) ? 'DOMError' : 'DOMException' - errorProperties.$exception_message = errorProperties.$exception_message || message + const exceptionType = isDOMError(domException) ? 'DOMError' : 'DOMException' + errorProperties = errorPropertiesFromString(message, { + ...metadata, + overrideExceptionType: exceptionType, + defaultExceptionMessage: message, + }) } if ('code' in domException) { errorProperties['$exception_DOMException_code'] = `${domException.code}` } + return errorProperties } else if (isErrorEvent(candidate as ErrorEvent) && (candidate as ErrorEvent).error) { - errorProperties = errorPropertiesFromError((candidate as ErrorEvent).error as Error) + return errorPropertiesFromError((candidate as ErrorEvent).error as Error, metadata) } else if (isError(candidate)) { - errorProperties = errorPropertiesFromError(candidate) + return errorPropertiesFromError(candidate, metadata) } else if (isPlainObject(candidate) || isEvent(candidate)) { // group these by using the keys available on the object const objectException = candidate as Record - errorProperties = errorPropertiesFromObject(objectException) - errorProperties.$exception_is_synthetic = true - } else { - // If none of previous checks were valid, then it must be a string - errorProperties.$exception_type = errorProperties.$exception_type || 'Error' - errorProperties.$exception_message = errorProperties.$exception_message || candidate - errorProperties.$exception_is_synthetic = true - } + return errorPropertiesFromObject(objectException) + } else if (isUndefined(error) && isString(event)) { + let name = 'Error' + let message = event + const groups = event.match(ERROR_TYPES_PATTERN) + if (groups) { + name = groups[1] + message = groups[2] + } - return { - ...errorProperties, - // now we make sure the mandatory fields that were made optional are present - $exception_type: errorProperties.$exception_type || 'UnknownErrorType', - $exception_message: errorProperties.$exception_message || '', - $exception_level: isSeverityLevel(errorProperties.$exception_level) - ? errorProperties.$exception_level - : 'error', - ...(source - ? { - $exception_source: source, // TODO get this from URL if not present - } - : {}), - ...(lineno ? { $exception_lineno: lineno } : {}), - ...(colno ? { $exception_colno: colno } : {}), + return errorPropertiesFromString(message, { + ...metadata, + overrideExceptionType: name, + defaultExceptionMessage: message, + }) + } else { + return errorPropertiesFromString(candidate as string, metadata) } } @@ -208,29 +285,17 @@ export function unhandledRejectionToProperties([ev]: [ev: PromiseRejectionEvent] // no-empty } - // some properties are not optional but, it's useful to start off without them enforced - let errorProperties: Omit & { - $exception_type?: string - $exception_message?: string - $exception_level?: string - } = {} if (isPrimitive(error)) { - errorProperties = { - $exception_message: `Non-Error promise rejection captured with value: ${String(error)}`, - } + return errorPropertiesFromString(`Non-Error promise rejection captured with value: ${String(error)}`, { + handled: false, + synthetic: false, + overrideExceptionType: 'UnhandledRejection', + }) } else { - errorProperties = errorToProperties([error as string | Event]) - } - errorProperties.$exception_handled = false - - return { - ...errorProperties, - // now we make sure the mandatory fields that were made optional are present - $exception_type: (errorProperties.$exception_type = 'UnhandledRejection'), - $exception_message: (errorProperties.$exception_message = - errorProperties.$exception_message || (ev as any).reason || String(error)), - $exception_level: isSeverityLevel(errorProperties.$exception_level) - ? errorProperties.$exception_level - : 'error', + return errorToProperties([error as string | Event], { + handled: false, + overrideExceptionType: 'UnhandledRejection', + defaultExceptionMessage: (ev as any).reason || String(error), + }) } } diff --git a/src/extensions/sentry-integration.ts b/src/extensions/sentry-integration.ts index 4930fdfd0..b3864365d 100644 --- a/src/extensions/sentry-integration.ts +++ b/src/extensions/sentry-integration.ts @@ -97,6 +97,7 @@ export function createEventProcessor( // added manually to avoid any dependency on the lazily loaded content $exception_message: any $exception_type: any + $exception_list: any $exception_personURL: string $exception_level: SeverityLevel $level: SeverityLevel @@ -106,6 +107,7 @@ export function createEventProcessor( $exception_type: exceptions[0]?.type, $exception_personURL: personUrl, $exception_level: event.level, + $exception_list: exceptions, // Sentry Exception Properties $sentry_event_id: event.event_id, $sentry_exception: event.exception, diff --git a/src/posthog-core.ts b/src/posthog-core.ts index 3dee01b08..2a3fe640d 100644 --- a/src/posthog-core.ts +++ b/src/posthog-core.ts @@ -1831,9 +1831,17 @@ export class PostHog { error, ]) : { - $exception_type: error.name, - $exception_message: error.message, $exception_level: 'error', + $exception_list: [ + { + type: error.name, + value: error.message, + mechanism: { + handled: true, + synthetic: false, + }, + }, + ], ...additionalProperties, } diff --git a/src/types.ts b/src/types.ts index 9133fad89..e18ced49f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -570,27 +570,17 @@ export type ErrorEventArgs = [ error?: Error | undefined ] +export type ErrorMetadata = { + handled?: boolean + synthetic?: boolean + overrideExceptionType?: string + overrideExceptionMessage?: string + defaultExceptionType?: string + defaultExceptionMessage?: string +} + // levels originally copied from Sentry to work with the sentry integration // and to avoid relying on a frequently changing @sentry/types dependency // but provided as an array of literal types, so we can constrain the level below export const severityLevels = ['fatal', 'error', 'warning', 'log', 'info', 'debug'] as const export declare type SeverityLevel = typeof severityLevels[number] - -export interface ErrorProperties { - $exception_type: string - $exception_message: string - $exception_level: SeverityLevel - $exception_source?: string - $exception_lineno?: number - $exception_colno?: number - $exception_DOMException_code?: string - $exception_is_synthetic?: boolean - $exception_stack_trace_raw?: string - $exception_handled?: boolean - $exception_personURL?: string -} - -export interface ErrorConversions { - errorToProperties: (args: ErrorEventArgs) => ErrorProperties - unhandledRejectionToProperties: (args: [ev: PromiseRejectionEvent]) => ErrorProperties -} diff --git a/src/utils/globals.ts b/src/utils/globals.ts index 5400083d4..d1fb123f5 100644 --- a/src/utils/globals.ts +++ b/src/utils/globals.ts @@ -1,6 +1,7 @@ +import { ErrorProperties } from '../extensions/exception-autocapture/error-conversion' import type { PostHog } from '../posthog-core' import { SessionIdManager } from '../sessionid' -import { ErrorEventArgs, ErrorProperties, Properties } from '../types' +import { ErrorEventArgs, ErrorMetadata, Properties } from '../types' /* * Global helpers to protect access to browser globals in a way that is safer for different targets @@ -37,7 +38,10 @@ interface PostHogExtensions { loadSiteApp?: (posthog: PostHog, appUrl: string, callback: (error?: string | Event, event?: Event) => void) => void - parseErrorAsProperties?: ([event, source, lineno, colno, error]: ErrorEventArgs) => ErrorProperties + parseErrorAsProperties?: ( + [event, source, lineno, colno, error]: ErrorEventArgs, + metadata?: ErrorMetadata + ) => ErrorProperties errorWrappingFunctions?: { wrapOnError: (captureFn: (props: Properties) => void) => () => void wrapUnhandledRejection: (captureFn: (props: Properties) => void) => () => void