diff --git a/packages/core/src/logs/DdLogs.ts b/packages/core/src/logs/DdLogs.ts index 49e7a97d9..7893d4308 100644 --- a/packages/core/src/logs/DdLogs.ts +++ b/packages/core/src/logs/DdLogs.ts @@ -33,6 +33,10 @@ const isLogWithError = ( args: LogArguments | LogWithErrorArguments ): args is LogWithErrorArguments => { return ( + (args.length > 3 && + (args[1] !== undefined || + args[2] !== undefined || + args[3] !== undefined)) || typeof args[1] === 'string' || typeof args[2] === 'string' || typeof args[3] === 'string' || @@ -105,6 +109,7 @@ class DdLogsWrapper implements DdLogsType { args[6] ); } + return this.log(args[0], validateContext(args[1]), 'error'); }; @@ -180,7 +185,7 @@ class DdLogsWrapper implements DdLogsType { stacktrace: string | undefined, context: object, status: 'debug' | 'info' | 'warn' | 'error', - fingerprint?: string, + fingerprint: string = '', source?: ErrorSource ): Promise => { const rawLogEvent: RawLogWithError = { @@ -190,7 +195,7 @@ class DdLogsWrapper implements DdLogsType { stacktrace, context, status, - fingerprint: fingerprint ?? '', + fingerprint, source }; @@ -208,10 +213,6 @@ class DdLogsWrapper implements DdLogsType { [DdAttributes.errorSourceType]: 'react-native' }; - if (fingerprint && fingerprint !== '') { - updatedContext[DdAttributes.errorFingerprint] = fingerprint; - } - return await this.nativeLogs[`${status}WithError`]( mappedEvent.message, (mappedEvent as NativeLogWithError).errorKind, diff --git a/packages/core/src/logs/__tests__/DdLogs.test.ts b/packages/core/src/logs/__tests__/DdLogs.test.ts index f99280bfe..0925ad85d 100644 --- a/packages/core/src/logs/__tests__/DdLogs.test.ts +++ b/packages/core/src/logs/__tests__/DdLogs.test.ts @@ -12,8 +12,9 @@ import { InternalLog } from '../../InternalLog'; import { SdkVerbosity } from '../../SdkVerbosity'; import type { DdNativeLogsType } from '../../nativeModulesTypes'; import { ErrorSource } from '../../rum/types'; +import { GlobalState } from '../../sdk/GlobalState/GlobalState'; import { DdLogs } from '../DdLogs'; -import type { LogEventMapper } from '../types'; +import type { LogEvent, LogEventMapper } from '../types'; jest.mock('../../InternalLog', () => { return { @@ -24,7 +25,34 @@ jest.mock('../../InternalLog', () => { }; }); +const initializeSdk = async () => { + if (GlobalState.instance.isInitialized) { + return; + } + + // GIVEN + const fakeAppId = '1'; + const fakeClientToken = '2'; + const fakeEnvName = 'env'; + const configuration = new DdSdkReactNativeConfiguration( + fakeClientToken, + fakeEnvName, + fakeAppId, + false, + false, + true // Track Errors + ); + + NativeModules.DdSdk.initialize.mockResolvedValue(null); + + // WHEN + await DdSdkReactNative.initialize(configuration); +}; + describe('DdLogs', () => { + beforeAll(async () => { + await initializeSdk(); + }); describe('log event mapper', () => { beforeEach(() => { jest.clearAllMocks(); @@ -194,35 +222,45 @@ describe('DdLogs', () => { ); }); - it('console errors can be filtered with mappers when trackErrors=true', async () => { + it('fingerprint can be injected with mappers in error logs', async () => { // GIVEN - const fakeAppId = '1'; - const fakeClientToken = '2'; - const fakeEnvName = 'env'; - const configuration = new DdSdkReactNativeConfiguration( - fakeClientToken, - fakeEnvName, - fakeAppId, - false, - false, - true // Track Errors + const errorFingerprint = 'my-custom-fingerprint'; + + // Register log event mapper to add error fingerprint + DdLogs.registerLogEventMapper((logEvent: LogEvent) => { + logEvent.fingerprint = errorFingerprint; + return logEvent; + }); + + console.error('test-error'); + expect(NativeModules.DdLogs.errorWithError).toHaveBeenCalledWith( + 'test-error', + 'Error', + 'test-error', + '', + { + '_dd.error.fingerprint': errorFingerprint, + '_dd.error.source_type': 'react-native', + '_dd.error_log.is_crash': true + } ); + }); - // Register log event mapper to filter console log events - configuration.logEventMapper = logEvent => { + it('console errors can be filtered with mappers when trackErrors=true', async () => { + // GIVEN + // Log event mapper to filter console log events + DdLogs.registerLogEventMapper(logEvent => { if (logEvent.source === ErrorSource.CONSOLE) { return null; } return logEvent; - }; - - NativeModules.DdSdk.initialize.mockResolvedValue(null); + }); // WHEN - await DdSdkReactNative.initialize(configuration); - console.error('console-error-message'); + + // THEN expect(NativeModules.DdLogs.error).not.toHaveBeenCalled(); expect(InternalLog.log).toHaveBeenCalledWith( 'error log dropped by log mapper: "console-error-message"', @@ -257,24 +295,7 @@ describe('DdLogs', () => { }); it('console errors are reported in logs when trackErrors=true', async () => { - // GIVEN - const fakeAppId = '1'; - const fakeClientToken = '2'; - const fakeEnvName = 'env'; - const configuration = new DdSdkReactNativeConfiguration( - fakeClientToken, - fakeEnvName, - fakeAppId, - false, - false, - true // Track Errors - ); - - NativeModules.DdSdk.initialize.mockResolvedValue(null); - - // WHEN - await DdSdkReactNative.initialize(configuration); - + // trackErrors is true in initializeSdk() configuration console.error('console-error-message'); expect(NativeModules.DdLogs.error).not.toHaveBeenCalled(); expect(InternalLog.log).toHaveBeenCalledWith( diff --git a/packages/core/src/logs/eventMapper.ts b/packages/core/src/logs/eventMapper.ts index 2bbd398cb..e1e21fc2d 100644 --- a/packages/core/src/logs/eventMapper.ts +++ b/packages/core/src/logs/eventMapper.ts @@ -4,10 +4,12 @@ * Copyright 2016-Present Datadog, Inc. */ +import { DdAttributes } from '../rum/DdAttributes'; import type { Attributes } from '../sdk/AttributesSingleton/types'; import { EventMapper } from '../sdk/EventMappers/EventMapper'; import type { UserInfo } from '../sdk/UserInfoSingleton/types'; +import { InternalLogEvent } from './types'; import type { LogEvent, LogEventMapper, @@ -20,12 +22,17 @@ import type { export const formatLogEventToNativeLog = ( logEvent: LogEvent ): NativeLog | NativeLogWithError => { - return logEvent; + return new InternalLogEvent(logEvent); }; export const formatRawLogToNativeEvent = ( rawLog: RawLog | RawLogWithError ): NativeLog | NativeLogWithError => { + if ((rawLog as RawLogWithError).fingerprint) { + (rawLog.context as any)[ + DdAttributes.errorFingerprint + ] = (rawLog as RawLogWithError).fingerprint; + } return rawLog; }; @@ -36,11 +43,11 @@ export const formatRawLogToLogEvent = ( attributes: Attributes; } ): LogEvent => { - return { + return new InternalLogEvent({ ...rawLog, userInfo: additionalInformation.userInfo, attributes: additionalInformation.attributes - }; + }); }; export const generateEventMapper = ( diff --git a/packages/core/src/logs/types.ts b/packages/core/src/logs/types.ts index 9c1b3cb09..391bc3fb9 100644 --- a/packages/core/src/logs/types.ts +++ b/packages/core/src/logs/types.ts @@ -4,6 +4,7 @@ * Copyright 2016-Present Datadog, Inc. */ +import { DdAttributes } from '../rum/DdAttributes'; import type { ErrorSource } from '../rum/types'; import type { UserInfo } from '../sdk/UserInfoSingleton/types'; @@ -91,6 +92,38 @@ export type LogEvent = { readonly attributes?: object; }; +export class InternalLogEvent implements LogEvent { + message: string; + context: object; + errorKind?: string | undefined; + errorMessage?: string | undefined; + stacktrace?: string | undefined; + source?: ErrorSource | undefined; + status: LogStatus; + userInfo: UserInfo; + attributes?: object | undefined; + + public get fingerprint(): string | undefined { + return (this.context as any)[DdAttributes.errorFingerprint] as string; + } + public set fingerprint(value: string | undefined) { + (this.context as any)[DdAttributes.errorFingerprint] = value; + } + + constructor(props: LogEvent) { + this.message = props.message; + this.context = props.context; + this.errorKind = props.errorKind; + this.errorMessage = props.errorMessage; + this.stacktrace = props.stacktrace; + this.fingerprint = props.fingerprint; + this.status = props.status; + this.source = props.source; + this.userInfo = props.userInfo; + this.attributes = props.attributes; + } +} + export type LogEventMapper = (logEvent: LogEvent) => LogEvent | null; export type LogArguments = [message: string, context?: object];