Skip to content

Commit

Permalink
{"base_commit":"d861d52525e61bae247a8a21a6d6be87e1cd92cb","head_commi…
Browse files Browse the repository at this point in the history
…t":"66c3c870ed05bc74e2e36e2d52a7925907f834c8","base_branch":"staging-17"}
  • Loading branch information
dd-mergequeue[bot] authored Apr 26, 2023
2 parents 4bc5e6d + 66c3c87 commit 804b538
Show file tree
Hide file tree
Showing 17 changed files with 152 additions and 6 deletions.
25 changes: 25 additions & 0 deletions packages/core/src/domain/console/consoleObservable.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,4 +110,29 @@ describe('console error observable', () => {
expect(stack).toContain('TypeError: foo')
}
})

it('should retrieve fingerprint from error', () => {
interface DatadogError extends Error {
dd_fingerprint?: string
}
const error = new Error('foo')
;(error as DatadogError).dd_fingerprint = 'my-fingerprint'

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

const consoleLog = notifyLog.calls.mostRecent().args[0]
expect(consoleLog.fingerprint).toBe('my-fingerprint')
})

it('should sanitize error fingerprint', () => {
const error = new Error('foo')
;(error as any).dd_fingerprint = 2

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

const consoleLog = notifyLog.calls.mostRecent().args[0]
expect(consoleLog.fingerprint).toBe('2')
})
})
12 changes: 10 additions & 2 deletions packages/core/src/domain/console/consoleObservable.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { computeStackTrace } from '../tracekit'
import { createHandlingStack, formatErrorMessage, toStackTraceString } from '../error/error'
import { createHandlingStack, formatErrorMessage, toStackTraceString, isErrorWithFingerprint } from '../error/error'
import { mergeObservables, Observable } from '../../tools/observable'
import { ConsoleApiName } from '../../tools/display'
import { callMonitored } from '../../tools/monitor'
Expand All @@ -12,9 +12,10 @@ export interface ConsoleLog {
api: ConsoleApiName
stack?: string
handlingStack?: string
fingerprint?: string
}

const consoleObservablesByApi: { [k in ConsoleApiName]?: Observable<ConsoleLog> } = {}
let consoleObservablesByApi: { [k in ConsoleApiName]?: Observable<ConsoleLog> } = {}

export function initConsoleObservable(apis: ConsoleApiName[]) {
const consoleObservables = apis.map((api) => {
Expand All @@ -27,6 +28,10 @@ export function initConsoleObservable(apis: ConsoleApiName[]) {
return mergeObservables<ConsoleLog>(...consoleObservables)
}

export function resetConsoleObservable() {
consoleObservablesByApi = {}
}

/* eslint-disable no-console */
function createConsoleObservable(api: ConsoleApiName) {
const observable = new Observable<ConsoleLog>(() => {
Expand All @@ -53,10 +58,12 @@ function buildConsoleLog(params: unknown[], api: ConsoleApiName, handlingStack:
// Todo: remove console error prefix in the next major version
let message = params.map((param) => formatConsoleParameters(param)).join(' ')
let stack
let fingerprint

if (api === ConsoleApiName.error) {
const firstErrorParam = find(params, (param: unknown): param is Error => param instanceof Error)
stack = firstErrorParam ? toStackTraceString(computeStackTrace(firstErrorParam)) : undefined
fingerprint = isErrorWithFingerprint(firstErrorParam) ? String(firstErrorParam.dd_fingerprint) : undefined
message = `console error: ${message}`
}

Expand All @@ -65,6 +72,7 @@ function buildConsoleLog(params: unknown[], api: ConsoleApiName, handlingStack:
message,
stack,
handlingStack,
fingerprint,
}
}

Expand Down
6 changes: 6 additions & 0 deletions packages/core/src/domain/error/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export function computeRawError({
: NO_ERROR_STACK_PRESENT_MESSAGE
const causes = isErrorInstance ? flattenErrorCauses(originalError as ErrorWithCause, source) : undefined
const type = stackTrace?.name
const fingerprint = isErrorWithFingerprint(originalError) ? String(originalError.dd_fingerprint) : undefined

return {
startClocks,
Expand All @@ -53,6 +54,7 @@ export function computeRawError({
message,
stack,
causes,
fingerprint,
}
}

Expand All @@ -66,6 +68,10 @@ function hasUsableStack(isErrorInstance: boolean, stackTrace?: StackTrace): stac
return stackTrace.stack.length > 0 && (stackTrace.stack.length > 1 || stackTrace.stack[0].url !== undefined)
}

export function isErrorWithFingerprint(error: unknown): error is { dd_fingerprint: unknown } {
return error instanceof Error && 'dd_fingerprint' in error
}

export function toStackTraceString(stack: StackTrace) {
let result = formatErrorMessage(stack)
stack.stack.forEach((frame) => {
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/domain/error/error.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export interface RawError {
handling?: ErrorHandling
handlingStack?: string
causes?: RawErrorCause[]
fingerprint?: string
}

export const ErrorSource = {
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ export { initFetchObservable, FetchResolveContext, FetchStartContext, FetchConte
export { createPageExitObservable, PageExitEvent, PageExitReason, isPageExitReason } from './browser/pageExitObservable'
export * from './browser/addEventListener'
export * from './tools/timer'
export { initConsoleObservable, ConsoleLog } from './domain/console/consoleObservable'
export { initConsoleObservable, resetConsoleObservable, ConsoleLog } from './domain/console/consoleObservable'
export { BoundedBuffer } from './tools/boundedBuffer'
export { catchUserErrors } from './tools/catchUserErrors'
export { createContextManager, ContextManager } from './tools/serialisation/contextManager'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,28 @@ describe('console collection', () => {
expect(rawLogsEvents[0].rawLogsEvent.error).toEqual({
origin: ErrorSource.CONSOLE,
stack: undefined,
fingerprint: undefined,
})
})

it('should retrieve fingerprint from console error', () => {
;({ stop: stopConsoleCollection } = startConsoleCollection(
validateAndBuildLogsConfiguration({ ...initConfiguration, forwardErrorsToLogs: true })!,
lifeCycle
))
interface DatadogError extends Error {
dd_fingerprint?: string
}
const error = new Error('foo')
;(error as DatadogError).dd_fingerprint = 'my-fingerprint'

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

expect(rawLogsEvents[0].rawLogsEvent.error).toEqual({
origin: ErrorSource.CONSOLE,
stack: jasmine.any(String),
fingerprint: 'my-fingerprint',
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export function startConsoleCollection(configuration: LogsConfiguration, lifeCyc
? {
origin: ErrorSource.CONSOLE, // Todo: Remove in the next major release
stack: log.stack,
fingerprint: log.fingerprint,
}
: undefined,
status: LogStatusForApi[log.api],
Expand Down
1 change: 1 addition & 0 deletions packages/logs/src/rawLogsEvent.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ type Error = {
kind?: string
origin: ErrorSource // Todo: Remove in the next major release
stack?: string
fingerprint?: string
[k: string]: unknown
}

Expand Down
18 changes: 17 additions & 1 deletion packages/rum-core/src/domain/assembly.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,23 @@ describe('rum assembly', () => {
})
})

it('should reject modification on non sensitive and non context field', () => {
describe('allowed customer provided field', () => {
it('should allow modification of the error fingerprint', () => {
const { lifeCycle } = setupBuilder
.withConfiguration({
beforeSend: (event) => (event.error.fingerprint = 'my_fingerprint'),
})
.build()

notifyRawRumEvent(lifeCycle, {
rawRumEvent: createRawRumEvent(RumEventType.ERROR),
})

expect((serverRumEvents[0] as RumErrorEvent).error.fingerprint).toBe('my_fingerprint')
})
})

it('should reject modification of field not sensitive, context or customer provided', () => {
const { lifeCycle } = setupBuilder
.withConfiguration({
beforeSend: (event: RumEvent) => ((event.view as any).id = 'modified'),
Expand Down
1 change: 1 addition & 0 deletions packages/rum-core/src/domain/assembly.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ export function startRumAssembly(
'error.message': 'string',
'error.stack': 'string',
'error.resource.url': 'string',
'error.fingerprint': 'string',
},
USER_CUSTOMIZABLE_FIELD_PATHS,
VIEW_MODIFIABLE_FIELD_PATHS
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ describe('error collection', () => {
handling: ErrorHandling.HANDLED,
source_type: 'browser',
causes: undefined,
fingerprint: undefined,
},
type: RumEventType.ERROR,
view: {
Expand Down Expand Up @@ -118,6 +119,39 @@ describe('error collection', () => {
expect(error?.causes?.[1].source).toEqual(ErrorSource.CUSTOM)
})

it('should extract fingerprint from error', () => {
const { rawRumEvents } = setupBuilder.build()

interface DatadogError extends Error {
dd_fingerprint?: string
}
const error = new Error('foo')
;(error as DatadogError).dd_fingerprint = 'my-fingerprint'

addError({
error,
handlingStack: 'Error: handling foo',
startClocks: { relative: 1234 as RelativeTime, timeStamp: 123456789 as TimeStamp },
})

expect((rawRumEvents[0].rawRumEvent as RawRumErrorEvent).error.fingerprint).toEqual('my-fingerprint')
})

it('should sanitize error fingerprint', () => {
const { rawRumEvents } = setupBuilder.build()

const error = new Error('foo')
;(error as any).dd_fingerprint = 2

addError({
error,
handlingStack: 'Error: handling foo',
startClocks: { relative: 1234 as RelativeTime, timeStamp: 123456789 as TimeStamp },
})

expect((rawRumEvents[0].rawRumEvent as RawRumErrorEvent).error.fingerprint).toEqual('2')
})

it('should save the specified customer context', () => {
const { rawRumEvents } = setupBuilder.build()
addError({
Expand Down Expand Up @@ -218,6 +252,7 @@ describe('error collection', () => {
handling: undefined,
source_type: 'browser',
causes: undefined,
fingerprint: undefined,
},
view: {
in_foreground: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ function processError(
handling: error.handling,
causes: error.causes,
source_type: 'browser',
fingerprint: error.fingerprint,
},
type: RumEventType.ERROR as const,
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { RawError, Subscription } from '@datadog/browser-core'
import { ErrorHandling, ErrorSource, Observable, clocksNow } from '@datadog/browser-core'
import { ErrorHandling, ErrorSource, Observable, clocksNow, resetConsoleObservable } from '@datadog/browser-core'
import type { Clock } from '@datadog/browser-core/test'
import { mockClock } from '@datadog/browser-core/test'
import { trackConsoleError } from './trackConsoleError'
Expand All @@ -20,6 +20,7 @@ describe('trackConsoleError', () => {
})

afterEach(() => {
resetConsoleObservable()
subscription.unsubscribe()
clock.cleanup()
})
Expand All @@ -32,9 +33,31 @@ describe('trackConsoleError', () => {
startClocks: clocksNow(),
message: jasmine.any(String),
stack: jasmine.any(String),
fingerprint: undefined,
source: ErrorSource.CONSOLE,
handling: ErrorHandling.HANDLED,
handlingStack: jasmine.any(String),
})
})

it('should retrieve fingerprint from console error', () => {
interface DatadogError extends Error {
dd_fingerprint?: string
}
const error = new Error('foo')
;(error as DatadogError).dd_fingerprint = 'my-fingerprint'

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

expect(notifyLog).toHaveBeenCalledWith({
startClocks: clocksNow(),
message: jasmine.any(String),
stack: jasmine.any(String),
source: ErrorSource.CONSOLE,
handling: ErrorHandling.HANDLED,
handlingStack: jasmine.any(String),
fingerprint: 'my-fingerprint',
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export function trackConsoleError(errorObservable: Observable<RawError>) {
startClocks: clocksNow(),
message: consoleError.message,
stack: consoleError.stack,
fingerprint: consoleError.fingerprint,
source: ErrorSource.CONSOLE,
handling: ErrorHandling.HANDLED,
handlingStack: consoleError.handlingStack,
Expand Down
1 change: 1 addition & 0 deletions packages/rum-core/src/rawRumEvent.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export interface RawRumErrorEvent {
type?: string
stack?: string
handling_stack?: string
fingerprint?: string
source: ErrorSource
message: string
handling?: ErrorHandling
Expand Down
4 changes: 4 additions & 0 deletions packages/rum-core/src/rumEvent.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,10 @@ export type RumErrorEvent = CommonProperties &
* Whether this error crashed the host application
*/
readonly is_crash?: boolean
/**
* Fingerprint used for Error Tracking custom grouping
*/
fingerprint?: string
/**
* The type of the error
*/
Expand Down
2 changes: 1 addition & 1 deletion rum-events-format

0 comments on commit 804b538

Please sign in to comment.