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

✨ [RUM-2902] Add error causes to context when logging an error #2602

Merged
merged 8 commits into from
Feb 22, 2024
13 changes: 12 additions & 1 deletion packages/core/src/domain/console/consoleObservable.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,26 @@
import { createHandlingStack, formatErrorMessage, toStackTraceString, tryToGetFingerprint } from '../error/error'
import {
createHandlingStack,
flattenErrorCauses,
formatErrorMessage,
toStackTraceString,
tryToGetFingerprint,
} from '../error/error'
import { mergeObservables, Observable } from '../../tools/observable'
import { ConsoleApiName, globalConsole } from '../../tools/display'
import { callMonitored } from '../../tools/monitor'
import { sanitize } from '../../tools/serialisation/sanitize'
import { find } from '../../tools/utils/polyfills'
import { jsonStringify } from '../../tools/serialisation/jsonStringify'
import { computeStackTrace } from '../error/computeStackTrace'
import type { RawErrorCause } from '../error/error.types'

export interface ConsoleLog {
message: string
api: ConsoleApiName
stack?: string
handlingStack?: string
fingerprint?: string
causes?: RawErrorCause[]
}

let consoleObservablesByApi: { [k in ConsoleApiName]?: Observable<ConsoleLog> } = {}
Expand Down Expand Up @@ -55,11 +63,13 @@ function buildConsoleLog(params: unknown[], api: ConsoleApiName, handlingStack:
const message = params.map((param) => formatConsoleParameters(param)).join(' ')
let stack
let fingerprint
let causes

if (api === ConsoleApiName.error) {
const firstErrorParam = find(params, (param: unknown): param is Error => param instanceof Error)
stack = firstErrorParam ? toStackTraceString(computeStackTrace(firstErrorParam)) : undefined
fingerprint = tryToGetFingerprint(firstErrorParam)
causes = firstErrorParam ? flattenErrorCauses(firstErrorParam, 'console') : undefined
}

return {
Expand All @@ -68,6 +78,7 @@ function buildConsoleLog(params: unknown[], api: ConsoleApiName, handlingStack:
stack,
handlingStack,
fingerprint,
causes,
}
}

Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/domain/error/trackRuntimeError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@ export type UnhandledErrorCallback = (stackTrace: StackTrace, originalError?: an

export function trackRuntimeError(errorObservable: Observable<RawError>) {
const handleRuntimeError = (stackTrace: StackTrace, originalError?: any) => {
const test = computeRawError({
const rawError = computeRawError({
stackTrace,
originalError,
startClocks: clocksNow(),
nonErrorPrefix: NonErrorPrefix.UNCAUGHT,
source: ErrorSource.SOURCE,
handling: ErrorHandling.UNHANDLED,
})
errorObservable.notify(test)
errorObservable.notify(rawError)
}
const { stop: stopInstrumentingOnError } = instrumentOnError(handleRuntimeError)
const { stop: stopInstrumentingOnUnhandledRejection } = instrumentUnhandledRejection(handleRuntimeError)
Expand Down
43 changes: 43 additions & 0 deletions packages/logs/src/domain/console/consoleCollection.spec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { ErrorWithCause } from '@datadog/browser-core'
import { ErrorSource, noop, objectEntries } from '@datadog/browser-core'
import type { RawConsoleLogsEvent } from '../../rawLogsEvent.types'
import { validateAndBuildLogsConfiguration } from '../configuration'
Expand Down Expand Up @@ -66,6 +67,7 @@ describe('console collection', () => {
expect(rawLogsEvents[0].rawLogsEvent.error).toEqual({
stack: undefined,
fingerprint: undefined,
causes: undefined,
})
})

Expand All @@ -86,6 +88,47 @@ describe('console collection', () => {
expect(rawLogsEvents[0].rawLogsEvent.error).toEqual({
stack: jasmine.any(String),
fingerprint: 'my-fingerprint',
causes: undefined,
})
})

it('should retrieve causes from console error', () => {
;({ stop: stopConsoleCollection } = startConsoleCollection(
validateAndBuildLogsConfiguration({ ...initConfiguration, forwardErrorsToLogs: true })!,
lifeCycle
))
const error = new Error('High level error') as ErrorWithCause
error.stack = 'Error: High level error'

const nestedError = new Error('Mid level error') as ErrorWithCause
nestedError.stack = 'Error: Mid level error'

const deepNestedError = new TypeError('Low level error') as ErrorWithCause
deepNestedError.stack = 'TypeError: Low level error'

nestedError.cause = deepNestedError
error.cause = nestedError

// eslint-disable-next-line no-console
console.error(error)

expect(rawLogsEvents[0].rawLogsEvent.error).toEqual({
stack: jasmine.any(String),
fingerprint: undefined,
causes: [
{
source: ErrorSource.CONSOLE,
type: 'Error',
stack: jasmine.any(String),
message: 'Mid level error',
},
{
source: ErrorSource.CONSOLE,
type: 'TypeError',
stack: jasmine.any(String),
message: 'Low level error',
},
],
})
})
})
Expand Down
1 change: 1 addition & 0 deletions packages/logs/src/domain/console/consoleCollection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export function startConsoleCollection(configuration: LogsConfiguration, lifeCyc
? {
stack: log.stack,
fingerprint: log.fingerprint,
causes: log.causes,
}
: undefined,
status: LogStatusForApi[log.api],
Expand Down
54 changes: 48 additions & 6 deletions packages/logs/src/domain/logger.spec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { ErrorWithCause } from '@datadog/browser-core'
import { NO_ERROR_STACK_PRESENT_MESSAGE, createCustomerDataTracker, noop } from '@datadog/browser-core'
import type { LogsMessage } from './logger'
import { HandlerType, Logger, STATUSES, StatusType } from './logger'
Expand Down Expand Up @@ -40,6 +41,7 @@ describe('Logger', () => {
kind: 'SyntaxError',
message: 'My Error',
stack: jasmine.stringMatching(/^SyntaxError: My Error/),
causes: undefined,
},
})
})
Expand Down Expand Up @@ -71,19 +73,59 @@ describe('Logger', () => {
kind: undefined,
message: 'Provided "My Error"',
stack: NO_ERROR_STACK_PRESENT_MESSAGE,
causes: undefined,
},
},
status: 'error',
})
})

it("'logger.error' should have an empty context if no Error object is provided", () => {
logger.error('message')
describe('when using logger.error', () => {
it("'logger.error' should have an empty context if no Error object is provided", () => {
logger.error('message')

expect(getLoggedMessage(0)).toEqual({
message: 'message',
status: 'error',
context: undefined,
expect(getLoggedMessage(0)).toEqual({
message: 'message',
status: 'error',
context: undefined,
})
})

it('should include causes when provided with an error', () => {
const error = new Error('High level error') as ErrorWithCause
error.stack = 'Error: High level error'

const nestedError = new Error('Mid level error') as ErrorWithCause
nestedError.stack = 'Error: Mid level error'

const deepNestedError = new TypeError('Low level error') as ErrorWithCause
deepNestedError.stack = 'TypeError: Low level error'

nestedError.cause = deepNestedError
error.cause = nestedError

logger.log('Logging message', {}, StatusType.error, error)

expect(getLoggedMessage(0)).toEqual({
message: 'Logging message',
status: 'error',
context: {
error: {
stack: 'Error: High level error',
kind: 'Error',
message: 'High level error',
causes: [
{ message: 'Mid level error', source: 'logger', type: 'Error', stack: 'Error: Mid level error' },
{
message: 'Low level error',
source: 'logger',
type: 'TypeError',
stack: 'TypeError: Low level error',
},
],
},
},
})
})
})
})
Expand Down
1 change: 1 addition & 0 deletions packages/logs/src/domain/logger.ts
N-Boutaib marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ export class Logger {
stack: rawError.stack,
kind: rawError.type,
message: rawError.message,
causes: rawError.causes,
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { ErrorWithCause } from '@datadog/browser-core'
import { ErrorSource } from '@datadog/browser-core'
import type { RawRuntimeLogsEvent } from '../../rawLogsEvent.types'
import type { LogsConfiguration } from '../configuration'
Expand Down Expand Up @@ -39,7 +40,7 @@ describe('runtime error collection', () => {
setTimeout(() => {
expect(rawLogsEvents[0].rawLogsEvent).toEqual({
date: jasmine.any(Number),
error: { kind: 'Error', stack: jasmine.any(String) },
error: { kind: 'Error', stack: jasmine.any(String), causes: undefined },
message: 'error!',
status: StatusType.error,
origin: ErrorSource.SOURCE,
Expand All @@ -48,6 +49,52 @@ describe('runtime error collection', () => {
}, 10)
})

it('should send runtime errors with causes', (done) => {
const error = new Error('High level error') as ErrorWithCause
error.stack = 'Error: High level error'

const nestedError = new Error('Mid level error') as ErrorWithCause
nestedError.stack = 'Error: Mid level error'

const deepNestedError = new TypeError('Low level error') as ErrorWithCause
deepNestedError.stack = 'TypeError: Low level error'

nestedError.cause = deepNestedError
error.cause = nestedError
;({ stop: stopRuntimeErrorCollection } = startRuntimeErrorCollection(configuration, lifeCycle))
setTimeout(() => {
throw error
})

setTimeout(() => {
expect(rawLogsEvents[0].rawLogsEvent).toEqual({
date: jasmine.any(Number),
error: {
kind: 'Error',
stack: jasmine.any(String),
causes: [
{
source: ErrorSource.SOURCE,
type: 'Error',
stack: jasmine.any(String),
message: 'Mid level error',
},
{
source: ErrorSource.SOURCE,
type: 'TypeError',
stack: jasmine.any(String),
message: 'Low level error',
},
],
},
message: 'High level error',
status: StatusType.error,
origin: ErrorSource.SOURCE,
})
done()
}, 10)
})

it('should not send runtime errors when forwardErrorsToLogs is false', (done) => {
;({ stop: stopRuntimeErrorCollection } = startRuntimeErrorCollection(
{ ...configuration, forwardErrorsToLogs: false },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export function startRuntimeErrorCollection(configuration: LogsConfiguration, li
error: {
kind: rawError.type,
stack: rawError.stack,
causes: rawError.causes,
},
origin: ErrorSource.SOURCE,
status: StatusType.error,
Expand Down