diff --git a/packages/core/src/domain/console/consoleObservable.ts b/packages/core/src/domain/console/consoleObservable.ts index 044d134911..00a577e3b1 100644 --- a/packages/core/src/domain/console/consoleObservable.ts +++ b/packages/core/src/domain/console/consoleObservable.ts @@ -1,4 +1,10 @@ -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' @@ -6,6 +12,7 @@ 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 @@ -13,6 +20,7 @@ export interface ConsoleLog { stack?: string handlingStack?: string fingerprint?: string + causes?: RawErrorCause[] } let consoleObservablesByApi: { [k in ConsoleApiName]?: Observable } = {} @@ -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 { @@ -68,6 +78,7 @@ function buildConsoleLog(params: unknown[], api: ConsoleApiName, handlingStack: stack, handlingStack, fingerprint, + causes, } } diff --git a/packages/core/src/domain/error/trackRuntimeError.ts b/packages/core/src/domain/error/trackRuntimeError.ts index 3a893048c2..5ad19c0de2 100644 --- a/packages/core/src/domain/error/trackRuntimeError.ts +++ b/packages/core/src/domain/error/trackRuntimeError.ts @@ -11,7 +11,7 @@ export type UnhandledErrorCallback = (stackTrace: StackTrace, originalError?: an export function trackRuntimeError(errorObservable: Observable) { const handleRuntimeError = (stackTrace: StackTrace, originalError?: any) => { - const test = computeRawError({ + const rawError = computeRawError({ stackTrace, originalError, startClocks: clocksNow(), @@ -19,7 +19,7 @@ export function trackRuntimeError(errorObservable: Observable) { source: ErrorSource.SOURCE, handling: ErrorHandling.UNHANDLED, }) - errorObservable.notify(test) + errorObservable.notify(rawError) } const { stop: stopInstrumentingOnError } = instrumentOnError(handleRuntimeError) const { stop: stopInstrumentingOnUnhandledRejection } = instrumentUnhandledRejection(handleRuntimeError) diff --git a/packages/logs/src/domain/console/consoleCollection.spec.ts b/packages/logs/src/domain/console/consoleCollection.spec.ts index fa0368444c..e826f54b67 100644 --- a/packages/logs/src/domain/console/consoleCollection.spec.ts +++ b/packages/logs/src/domain/console/consoleCollection.spec.ts @@ -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' @@ -66,6 +67,7 @@ describe('console collection', () => { expect(rawLogsEvents[0].rawLogsEvent.error).toEqual({ stack: undefined, fingerprint: undefined, + causes: undefined, }) }) @@ -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', + }, + ], }) }) }) diff --git a/packages/logs/src/domain/console/consoleCollection.ts b/packages/logs/src/domain/console/consoleCollection.ts index 6cac473053..62f3b3cbc3 100644 --- a/packages/logs/src/domain/console/consoleCollection.ts +++ b/packages/logs/src/domain/console/consoleCollection.ts @@ -31,6 +31,7 @@ export function startConsoleCollection(configuration: LogsConfiguration, lifeCyc ? { stack: log.stack, fingerprint: log.fingerprint, + causes: log.causes, } : undefined, status: LogStatusForApi[log.api], diff --git a/packages/logs/src/domain/logger.spec.ts b/packages/logs/src/domain/logger.spec.ts index 566bfa831d..72f8f2d46d 100644 --- a/packages/logs/src/domain/logger.spec.ts +++ b/packages/logs/src/domain/logger.spec.ts @@ -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' @@ -40,6 +41,7 @@ describe('Logger', () => { kind: 'SyntaxError', message: 'My Error', stack: jasmine.stringMatching(/^SyntaxError: My Error/), + causes: undefined, }, }) }) @@ -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', + }, + ], + }, + }, + }) }) }) }) diff --git a/packages/logs/src/domain/logger.ts b/packages/logs/src/domain/logger.ts index 5abda87870..7fea00850a 100644 --- a/packages/logs/src/domain/logger.ts +++ b/packages/logs/src/domain/logger.ts @@ -12,7 +12,7 @@ import { NonErrorPrefix, } from '@datadog/browser-core' -import type { LogsEvent } from '../logsEvent.types' +import type { RawLoggerLogsEvent } from '../rawLogsEvent.types' export interface LogsMessage { message: string @@ -58,7 +58,7 @@ export class Logger { @monitored log(message: string, messageContext?: object, status: StatusType = StatusType.info, error?: Error) { - let errorContext: LogsEvent['error'] + let errorContext: RawLoggerLogsEvent['error'] if (error !== undefined && error !== null) { const stackTrace = error instanceof Error ? computeStackTrace(error) : undefined @@ -75,6 +75,7 @@ export class Logger { stack: rawError.stack, kind: rawError.type, message: rawError.message, + causes: rawError.causes, } } diff --git a/packages/logs/src/domain/runtimeError/runtimeErrorCollection.spec.ts b/packages/logs/src/domain/runtimeError/runtimeErrorCollection.spec.ts index ef6916022b..f0516a773c 100644 --- a/packages/logs/src/domain/runtimeError/runtimeErrorCollection.spec.ts +++ b/packages/logs/src/domain/runtimeError/runtimeErrorCollection.spec.ts @@ -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' @@ -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, @@ -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 }, diff --git a/packages/logs/src/domain/runtimeError/runtimeErrorCollection.ts b/packages/logs/src/domain/runtimeError/runtimeErrorCollection.ts index 3946832b7c..7345c02823 100644 --- a/packages/logs/src/domain/runtimeError/runtimeErrorCollection.ts +++ b/packages/logs/src/domain/runtimeError/runtimeErrorCollection.ts @@ -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, diff --git a/packages/logs/src/logsEvent.types.ts b/packages/logs/src/logsEvent.types.ts index d1286f6dda..dd85e1ec63 100644 --- a/packages/logs/src/logsEvent.types.ts +++ b/packages/logs/src/logsEvent.types.ts @@ -69,6 +69,23 @@ export interface LogsEvent { * Stacktrace of the error */ stack?: string + /** + * Fingerprint of the error + */ + fingerprint?: string + /** + * Message of the error + */ + message?: string + /** + * Flattened causes of the error + */ + causes?: Array<{ + message: string + source: string + type?: string + stack?: string + }> [k: string]: unknown } diff --git a/packages/logs/src/rawLogsEvent.types.ts b/packages/logs/src/rawLogsEvent.types.ts index 0d0ee3eaf9..fb504a6024 100644 --- a/packages/logs/src/rawLogsEvent.types.ts +++ b/packages/logs/src/rawLogsEvent.types.ts @@ -1,4 +1,4 @@ -import type { Context, ErrorSource, TimeStamp, User } from '@datadog/browser-core' +import type { Context, ErrorSource, RawErrorCause, TimeStamp, User } from '@datadog/browser-core' import type { StatusType } from './domain/logger' export type RawLogsEvent = @@ -10,10 +10,11 @@ export type RawLogsEvent = | RawRuntimeLogsEvent type Error = { + message?: string kind?: string stack?: string fingerprint?: string - [k: string]: unknown + causes?: RawErrorCause[] } interface CommonRawLogsEvent {