Skip to content
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

✨ [RUMF-1353] Add Error.cause property #1740

Merged
merged 28 commits into from
Oct 3, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
10d8251
[RUMF-1353] Add Error.cause property
Sep 16, 2022
99d7246
remove ts-ignore comments in favour of as TS chaining
Sep 16, 2022
f0e2da8
use ? instead of undefined in type def
Sep 16, 2022
2a6bcfb
mimic formatUnknownError logic in flattenErrorCauses regarding cause …
Sep 20, 2022
1be956a
fix missing export
Sep 21, 2022
5262202
Merge branch 'main' into william/error-cause-property
liywjl Sep 21, 2022
c659327
stack should use type string
Sep 21, 2022
b2f1340
Update packages/core/src/tools/error.spec.ts
liywjl Sep 21, 2022
20776e7
deprecate formatUnknownError in favour of computeRawError
Sep 21, 2022
10a72fc
revert rum-events-format update
Sep 21, 2022
6642d84
add test for flattenErrorCauses with stack
Sep 21, 2022
6a868a1
pass handling as prop
Sep 21, 2022
0f13557
use DEFAULT_RAW_ERROR_PARMS in error spec
Sep 22, 2022
d723a80
causes can be undefinded in errorCollection spec
Sep 22, 2022
6931d26
use originalError name and simplify flattenErrorCauses
Sep 22, 2022
7e7c4c5
update test name
Sep 22, 2022
d66c278
simplify test logic
Sep 22, 2022
4e500b7
Update packages/core/src/tools/error.ts
liywjl Sep 22, 2022
bf6bcdd
fix unit test
Sep 22, 2022
a332178
add test to check stacktrace comes from cause Error
Sep 22, 2022
1e55af3
update test naming
Sep 22, 2022
6d72cf5
update test name and object casting
Sep 22, 2022
de566e7
define error inline in test
Sep 22, 2022
0603c01
merge tests to reduce number of tests
Sep 23, 2022
9c6fd18
🐛 fix wrong stack trace being used in cause errors
Sep 23, 2022
ee8585a
check the cause type is the Error type name
Sep 23, 2022
3536ec1
Merge branch 'main' into william/error-cause-property
liywjl Sep 23, 2022
b8c98fc
Merge branch 'main' into william/error-cause-property
liywjl Oct 3, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 12 additions & 12 deletions packages/core/src/domain/error/trackRuntimeError.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
import type { RawError } from '../../tools/error'
import { ErrorSource, ErrorHandling, formatUnknownError } from '../../tools/error'
import { ErrorSource, computeRawError, ErrorHandling } from '../../tools/error'
import type { Observable } from '../../tools/observable'
import { clocksNow } from '../../tools/timeUtils'
import { startUnhandledErrorCollection } from '../tracekit'

export function trackRuntimeError(errorObservable: Observable<RawError>) {
return startUnhandledErrorCollection((stackTrace, errorObject) => {
const { stack, message, type } = formatUnknownError(stackTrace, errorObject, 'Uncaught')
errorObservable.notify({
message,
stack,
type,
source: ErrorSource.SOURCE,
startClocks: clocksNow(),
originalError: errorObject,
handling: ErrorHandling.UNHANDLED,
})
return startUnhandledErrorCollection((stackTrace, originalError) => {
errorObservable.notify(
computeRawError({
stackTrace,
originalError,
startClocks: clocksNow(),
nonErrorPrefix: 'Uncaught',
source: ErrorSource.SOURCE,
handling: ErrorHandling.UNHANDLED,
})
)
})
}
4 changes: 3 additions & 1 deletion packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,11 @@ export { instrumentMethod, instrumentMethodAndCallOriginal, instrumentSetter } f
export {
ErrorSource,
ErrorHandling,
formatUnknownError,
computeRawError,
createHandlingStack,
RawError,
RawErrorCause,
ErrorWithCause,
toStackTraceString,
getFileFromStackTraceString,
} from './tools/error'
Expand Down
157 changes: 146 additions & 11 deletions packages/core/src/tools/error.spec.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,25 @@
import type { StackTrace } from '../domain/tracekit'
import { createHandlingStack, formatUnknownError, getFileFromStackTraceString } from './error'

describe('formatUnknownError', () => {
import type { RawErrorCause, ErrorWithCause } from './error'
import { clocksNow } from './timeUtils'
import {
createHandlingStack,
computeRawError,
getFileFromStackTraceString,
flattenErrorCauses,
ErrorSource,
ErrorHandling,
} from './error'

describe('computeRawError', () => {
const NOT_COMPUTED_STACK_TRACE: StackTrace = { name: undefined, message: undefined, stack: [] } as any
const DEFAULT_RAW_ERROR_PARMS = {
startClocks: clocksNow(),
nonErrorPrefix: 'Uncaught',
source: ErrorSource.CUSTOM,
}

it('should format an error', () => {
const stack: StackTrace = {
const stackTrace: StackTrace = {
message: 'oh snap!',
name: 'TypeError',
stack: [
Expand Down Expand Up @@ -33,7 +47,12 @@ describe('formatUnknownError', () => {
],
}

const formatted = formatUnknownError(stack, undefined, 'Uncaught')
const formatted = computeRawError({
...DEFAULT_RAW_ERROR_PARMS,
stackTrace,
originalError: undefined,
handling: ErrorHandling.HANDLED,
})

expect(formatted.message).toEqual('oh snap!')
expect(formatted.type).toEqual('TypeError')
Expand All @@ -44,32 +63,114 @@ describe('formatUnknownError', () => {
})

it('should format an error with an empty message', () => {
const stack: StackTrace = {
const stackTrace: StackTrace = {
message: '',
name: 'TypeError',
stack: [],
}

const formatted = formatUnknownError(stack, undefined, 'Uncaught')
const formatted = computeRawError({
...DEFAULT_RAW_ERROR_PARMS,
stackTrace,
originalError: undefined,
handling: ErrorHandling.HANDLED,
})

expect(formatted.message).toEqual('Empty message')
})

it('should format a string error', () => {
const errorObject = 'oh snap!'
const error = 'oh snap!'

const formatted = formatUnknownError(NOT_COMPUTED_STACK_TRACE, errorObject, 'Uncaught')
const formatted = computeRawError({
...DEFAULT_RAW_ERROR_PARMS,
stackTrace: NOT_COMPUTED_STACK_TRACE,
originalError: error,
handling: ErrorHandling.HANDLED,
})

expect(formatted.message).toEqual('Uncaught "oh snap!"')
})

it('should format an object error', () => {
const errorObject = { foo: 'bar' }
const error = { foo: 'bar' }

const formatted = formatUnknownError(NOT_COMPUTED_STACK_TRACE, errorObject, 'Uncaught')
const formatted = computeRawError({
...DEFAULT_RAW_ERROR_PARMS,
stackTrace: NOT_COMPUTED_STACK_TRACE,
originalError: error,
handling: ErrorHandling.HANDLED,
})

expect(formatted.message).toEqual('Uncaught {"foo":"bar"}')
})

it('should set handling according to given parameter', () => {
const error = { foo: 'bar' }

expect(
computeRawError({
...DEFAULT_RAW_ERROR_PARMS,
stackTrace: NOT_COMPUTED_STACK_TRACE,
originalError: error,
handling: ErrorHandling.HANDLED,
}).handling
).toEqual(ErrorHandling.HANDLED)

expect(
computeRawError({
...DEFAULT_RAW_ERROR_PARMS,
stackTrace: NOT_COMPUTED_STACK_TRACE,
originalError: error,
handling: ErrorHandling.UNHANDLED,
}).handling
).toEqual(ErrorHandling.UNHANDLED)
})

it('should compute an object error with causes', () => {
const stackTrace: StackTrace = {
message: 'some typeError message',
name: 'TypeError',
stack: [],
}

const error = new Error('foo: bar') as ErrorWithCause
error.stack = 'Error: foo: bar\n at <anonymous>:1:15'

const nestedError = new Error('biz: buz') as ErrorWithCause
nestedError.stack = 'NestedError: biz: buz\n at <anonymous>:2:15'

const deepNestedError = new TypeError('fiz: buz') as ErrorWithCause
deepNestedError.stack = 'NestedError: fiz: buz\n at <anonymous>:3:15'

error.cause = nestedError
nestedError.cause = deepNestedError

const formatted = computeRawError({
...DEFAULT_RAW_ERROR_PARMS,
stackTrace,
originalError: error,
handling: ErrorHandling.HANDLED,
source: ErrorSource.SOURCE,
})

expect(formatted?.type).toEqual('TypeError')
expect(formatted?.message).toEqual('some typeError message')
expect(formatted.causes?.length).toBe(2)
expect(formatted.stack).toContain('TypeError: some typeError message')

const causes = formatted.causes as RawErrorCause[]

expect(causes[0].message).toContain(nestedError.message)
expect(causes[0].source).toContain(ErrorSource.SOURCE)
expect(causes[0].type).toEqual(nestedError.name)
expect(causes[0].stack).toContain('Error: biz: buz')

expect(causes[1].message).toContain(deepNestedError.message)
expect(causes[1].source).toContain(ErrorSource.SOURCE)
expect(causes[1].type).toEqual(deepNestedError.name)
expect(causes[1].stack).toContain('Error: fiz: buz')
})
})

describe('getFileFromStackTraceString', () => {
Expand Down Expand Up @@ -107,3 +208,37 @@ describe('createHandlingStack', () => {
at userCallOne @ (.*)`)
})
})

describe('flattenErrorCauses', () => {
it('should return undefined if no cause found', () => {
const error = new Error('foo') as ErrorWithCause
const errorCauses = flattenErrorCauses(error, ErrorSource.LOGGER)
expect(errorCauses).toEqual(undefined)
})

it('should return undefined if cause is not of type Error', () => {
const error = new Error('foo') as ErrorWithCause
const nestedError = { biz: 'buz', cause: new Error('boo') } as unknown as Error

error.cause = nestedError

const errorCauses = flattenErrorCauses(error, ErrorSource.LOGGER)
expect(errorCauses?.length).toEqual(undefined)
})

it('should use error to extract stack trace', () => {
const error = new Error('foo') as ErrorWithCause

error.cause = new Error('bar')

const errorCauses = flattenErrorCauses(error, ErrorSource.LOGGER)
expect(errorCauses?.[0].type).toEqual('Error')
})

it('should only return the first 10 errors if nested chain is longer', () => {
const error = new Error('foo') as ErrorWithCause
error.cause = error
Comment on lines +238 to +240
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice one!

const errorCauses = flattenErrorCauses(error, ErrorSource.LOGGER)
expect(errorCauses?.length).toEqual(10)
})
})
65 changes: 58 additions & 7 deletions packages/core/src/tools/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,17 @@ import { callMonitored } from './monitor'
import type { ClocksState } from './timeUtils'
import { jsonStringify, noop } from './utils'

export interface ErrorWithCause extends Error {
cause?: Error
}

export type RawErrorCause = {
message: string
source: string
type?: string
stack?: string
}

export interface RawError {
startClocks: ClocksState
message: string
Expand All @@ -13,6 +24,7 @@ export interface RawError {
originalError?: unknown
handling?: ErrorHandling
handlingStack?: string
causes?: RawErrorCause[]
}

export const ErrorSource = {
Expand All @@ -32,26 +44,49 @@ export const enum ErrorHandling {

export type ErrorSource = typeof ErrorSource[keyof typeof ErrorSource]

export function formatUnknownError(
stackTrace: StackTrace | undefined,
errorObject: any,
nonErrorPrefix: string,
type RawErrorParams = {
stackTrace?: StackTrace
originalError: unknown

handlingStack?: string
) {
if (!stackTrace || (stackTrace.message === undefined && !(errorObject instanceof Error))) {
startClocks: ClocksState
nonErrorPrefix: string
source: ErrorSource
handling: ErrorHandling
}

export function computeRawError({
stackTrace,
originalError,
handlingStack,
startClocks,
nonErrorPrefix,
source,
handling,
}: RawErrorParams): RawError {
if (!stackTrace || (stackTrace.message === undefined && !(originalError instanceof Error))) {
return {
message: `${nonErrorPrefix} ${jsonStringify(errorObject)!}`,
startClocks,
source,
handling,
originalError,
message: `${nonErrorPrefix} ${jsonStringify(originalError)!}`,
stack: 'No stack, consider using an instance of Error',
handlingStack,
type: stackTrace && stackTrace.name,
}
}

return {
startClocks,
source,
handling,
originalError,
message: stackTrace.message || 'Empty message',
stack: toStackTraceString(stackTrace),
handlingStack,
type: stackTrace.name,
causes: flattenErrorCauses(originalError as ErrorWithCause, source),
}
}

Expand Down Expand Up @@ -110,3 +145,19 @@ export function createHandlingStack(): string {

return formattedStack!
}

export function flattenErrorCauses(error: ErrorWithCause, parentSource: ErrorSource): RawErrorCause[] | undefined {
let currentError = error
const causes: RawErrorCause[] = []
while (currentError?.cause instanceof Error && causes.length < 10) {
const stackTrace = computeStackTrace(currentError.cause)
causes.push({
message: currentError.cause.message,
source: parentSource,
type: stackTrace?.name,
stack: stackTrace && toStackTraceString(stackTrace),
bcaudan marked this conversation as resolved.
Show resolved Hide resolved
})
currentError = currentError.cause
}
return causes.length ? causes : undefined
}
Loading