From 45b60c1808b1471c3d15ed30d2ac3ed6bdc94da1 Mon Sep 17 00:00:00 2001 From: Marco Saia Date: Wed, 12 Jun 2024 16:44:38 +0200 Subject: [PATCH 1/3] Validate context argument in API calls --- packages/core/src/logs/DdLogs.ts | 17 +- .../core/src/logs/__tests__/DdLogs.test.ts | 543 ++++++++++++++++++ packages/core/src/rum/DdRum.ts | 38 +- packages/core/src/trace/DdTrace.ts | 14 +- .../src/utils/__tests__/argsUtils.test.ts | 59 ++ packages/core/src/utils/argsUtils.ts | 28 + 6 files changed, 679 insertions(+), 20 deletions(-) create mode 100644 packages/core/src/utils/__tests__/argsUtils.test.ts create mode 100644 packages/core/src/utils/argsUtils.ts diff --git a/packages/core/src/logs/DdLogs.ts b/packages/core/src/logs/DdLogs.ts index 8f510d39c..5b1863c18 100644 --- a/packages/core/src/logs/DdLogs.ts +++ b/packages/core/src/logs/DdLogs.ts @@ -7,6 +7,7 @@ import { DATADOG_MESSAGE_PREFIX, InternalLog } from '../InternalLog'; import { SdkVerbosity } from '../SdkVerbosity'; import type { DdNativeLogsType } from '../nativeModulesTypes'; +import { validateContext } from '../utils/argsUtils'; import { generateEventMapper } from './eventMapper'; import type { @@ -49,11 +50,11 @@ class DdLogsWrapper implements DdLogsType { args[1], args[2], args[3], - args[4] || {}, + validateContext(args[4]), 'debug' ); } - return this.log(args[0], args[1] || {}, 'debug'); + return this.log(args[0], validateContext(args[1]), 'debug'); }; info = (...args: LogArguments | LogWithErrorArguments): Promise => { @@ -63,11 +64,11 @@ class DdLogsWrapper implements DdLogsType { args[1], args[2], args[3], - args[4] || {}, + validateContext(args[4]), 'info' ); } - return this.log(args[0], args[1] || {}, 'info'); + return this.log(args[0], validateContext(args[1]), 'info'); }; warn = (...args: LogArguments | LogWithErrorArguments): Promise => { @@ -77,11 +78,11 @@ class DdLogsWrapper implements DdLogsType { args[1], args[2], args[3], - args[4] || {}, + validateContext(args[4]), 'warn' ); } - return this.log(args[0], args[1] || {}, 'warn'); + return this.log(args[0], validateContext(args[1]), 'warn'); }; error = (...args: LogArguments | LogWithErrorArguments): Promise => { @@ -91,11 +92,11 @@ class DdLogsWrapper implements DdLogsType { args[1], args[2], args[3], - args[4] || {}, + validateContext(args[4]), 'error' ); } - return this.log(args[0], args[1] || {}, 'error'); + return this.log(args[0], validateContext(args[1]), 'error'); }; /** diff --git a/packages/core/src/logs/__tests__/DdLogs.test.ts b/packages/core/src/logs/__tests__/DdLogs.test.ts index 68758b485..8bb2ee03d 100644 --- a/packages/core/src/logs/__tests__/DdLogs.test.ts +++ b/packages/core/src/logs/__tests__/DdLogs.test.ts @@ -7,6 +7,7 @@ import { NativeModules } from 'react-native'; import { InternalLog } from '../../InternalLog'; +import { SdkVerbosity } from '../../SdkVerbosity'; import type { DdNativeLogsType } from '../../nativeModulesTypes'; import { DdLogs } from '../DdLogs'; import type { LogEventMapper } from '../types'; @@ -217,4 +218,546 @@ describe('DdLogs', () => { ); }); }); + + describe('log context', () => { + beforeEach(() => { + jest.clearAllMocks(); + DdLogs.unregisterLogEventMapper(); + }); + + describe('debug logs', () => { + it('native context is empty W context is undefined', async () => { + await DdLogs.debug('message', undefined); + expect(NativeModules.DdLogs.debug).toHaveBeenCalledWith( + 'message', + {} + ); + }); + + it('native context is an object with nested property W context is an array', async () => { + await DdLogs.debug('message', [1, 2, 3]); + expect(InternalLog.log).toHaveBeenNthCalledWith( + 1, + expect.anything(), + SdkVerbosity.WARN + ); + expect( + NativeModules.DdLogs.debug + ).toHaveBeenCalledWith('message', { context: [1, 2, 3] }); + }); + + it('native context is empty W context is raw type', async () => { + const obj: any = 123; + await DdLogs.debug('message', obj); + expect(InternalLog.log).toHaveBeenNthCalledWith( + 1, + expect.anything(), + SdkVerbosity.ERROR + ); + expect(NativeModules.DdLogs.debug).toHaveBeenCalledWith( + 'message', + {} + ); + }); + + it('native context is unmodified W context is a valid object', async () => { + await DdLogs.debug('message', { test: '123' }); + expect( + NativeModules.DdLogs.debug + ).toHaveBeenCalledWith('message', { test: '123' }); + }); + }); + + describe('warn logs', () => { + it('native context is empty W context is undefined', async () => { + await DdLogs.warn('message', undefined); + expect(NativeModules.DdLogs.warn).toHaveBeenCalledWith( + 'message', + {} + ); + }); + + it('native context is an object with nested property W context is an array', async () => { + await DdLogs.warn('message', [1, 2, 3]); + expect(InternalLog.log).toHaveBeenNthCalledWith( + 1, + expect.anything(), + SdkVerbosity.WARN + ); + expect( + NativeModules.DdLogs.warn + ).toHaveBeenCalledWith('message', { context: [1, 2, 3] }); + }); + + it('native context is empty W context is raw type', async () => { + const obj: any = 123; + await DdLogs.warn('message', obj); + expect(InternalLog.log).toHaveBeenNthCalledWith( + 1, + expect.anything(), + SdkVerbosity.ERROR + ); + expect(NativeModules.DdLogs.warn).toHaveBeenCalledWith( + 'message', + {} + ); + }); + + it('native context is unmodified W context is a valid object', async () => { + await DdLogs.warn('message', { test: '123' }); + expect( + NativeModules.DdLogs.warn + ).toHaveBeenCalledWith('message', { test: '123' }); + }); + }); + + describe('info logs', () => { + it('native context is empty W context is undefined', async () => { + await DdLogs.info('message', undefined); + expect(NativeModules.DdLogs.info).toHaveBeenCalledWith( + 'message', + {} + ); + }); + + it('native context is an object with nested property W context is an array', async () => { + await DdLogs.info('message', [1, 2, 3]); + expect(InternalLog.log).toHaveBeenNthCalledWith( + 1, + expect.anything(), + SdkVerbosity.WARN + ); + expect( + NativeModules.DdLogs.info + ).toHaveBeenCalledWith('message', { context: [1, 2, 3] }); + }); + + it('native context is empty W context is raw type', async () => { + const obj: any = 123; + await DdLogs.info('message', obj); + expect(InternalLog.log).toHaveBeenNthCalledWith( + 1, + expect.anything(), + SdkVerbosity.ERROR + ); + expect(NativeModules.DdLogs.info).toHaveBeenCalledWith( + 'message', + {} + ); + }); + + it('native context is unmodified W context is a valid object', async () => { + await DdLogs.info('message', { test: '123' }); + expect( + NativeModules.DdLogs.info + ).toHaveBeenCalledWith('message', { test: '123' }); + }); + }); + + describe('error logs', () => { + it('native context is empty W context is undefined', async () => { + await DdLogs.error('message', undefined); + expect(NativeModules.DdLogs.error).toHaveBeenCalledWith( + 'message', + {} + ); + }); + + it('native context is an object with nested property W context is an array', async () => { + await DdLogs.error('message', [1, 2, 3]); + expect(InternalLog.log).toHaveBeenNthCalledWith( + 1, + expect.anything(), + SdkVerbosity.WARN + ); + expect( + NativeModules.DdLogs.error + ).toHaveBeenCalledWith('message', { context: [1, 2, 3] }); + }); + + it('native context is empty W context is raw type', async () => { + const obj: any = 123; + await DdLogs.error('message', obj); + expect(InternalLog.log).toHaveBeenNthCalledWith( + 1, + expect.anything(), + SdkVerbosity.ERROR + ); + expect(NativeModules.DdLogs.error).toHaveBeenCalledWith( + 'message', + {} + ); + }); + + it('native context is unmodified W context is a valid object', async () => { + await DdLogs.error('message', { test: '123' }); + expect( + NativeModules.DdLogs.error + ).toHaveBeenCalledWith('message', { test: '123' }); + }); + }); + }); + + describe('log with error context', () => { + beforeEach(() => { + jest.clearAllMocks(); + DdLogs.unregisterLogEventMapper(); + }); + + describe('debug logs', () => { + it('native context is empty W context is undefined', async () => { + await DdLogs.debug( + 'message', + 'kind', + 'message', + 'stacktrace', + undefined + ); + expect( + NativeModules.DdLogs.debugWithError + ).toHaveBeenCalledWith( + 'message', + 'kind', + 'message', + 'stacktrace', + { '_dd.error.source_type': 'react-native' } + ); + }); + + it('native context is an object with nested property W context is an array', async () => { + await DdLogs.debug('message', 'kind', 'message', 'stacktrace', [ + 1, + 2, + 3 + ]); + + expect(InternalLog.log).toHaveBeenNthCalledWith( + 1, + expect.anything(), + SdkVerbosity.WARN + ); + + expect( + NativeModules.DdLogs.debugWithError + ).toHaveBeenCalledWith( + 'message', + 'kind', + 'message', + 'stacktrace', + { + context: [1, 2, 3], + '_dd.error.source_type': 'react-native' + } + ); + }); + + it('native context is empty W context is raw type', async () => { + const obj: any = 123; + await DdLogs.debug( + 'message', + 'kind', + 'message', + 'stacktrace', + obj + ); + + expect(InternalLog.log).toHaveBeenNthCalledWith( + 1, + expect.anything(), + SdkVerbosity.ERROR + ); + + expect( + NativeModules.DdLogs.debugWithError + ).toHaveBeenCalledWith( + 'message', + 'kind', + 'message', + 'stacktrace', + { '_dd.error.source_type': 'react-native' } + ); + }); + + it('native context is unmodified W context is a valid object', async () => { + await DdLogs.debug('message', 'kind', 'message', 'stacktrace', { + test: '123' + }); + expect( + NativeModules.DdLogs.debugWithError + ).toHaveBeenCalledWith( + 'message', + 'kind', + 'message', + 'stacktrace', + { test: '123', '_dd.error.source_type': 'react-native' } + ); + }); + }); + + describe('warn logs', () => { + it('native context is empty W context is undefined', async () => { + await DdLogs.warn( + 'message', + 'kind', + 'message', + 'stacktrace', + undefined + ); + expect( + NativeModules.DdLogs.warnWithError + ).toHaveBeenCalledWith( + 'message', + 'kind', + 'message', + 'stacktrace', + { '_dd.error.source_type': 'react-native' } + ); + }); + + it('native context is an object with nested property W context is an array', async () => { + await DdLogs.warn('message', 'kind', 'message', 'stacktrace', [ + 1, + 2, + 3 + ]); + + expect(InternalLog.log).toHaveBeenNthCalledWith( + 1, + expect.anything(), + SdkVerbosity.WARN + ); + + expect(NativeModules.DdLogs.warnWithError).toHaveBeenCalledWith( + 'message', + 'kind', + 'message', + 'stacktrace', + { + context: [1, 2, 3], + '_dd.error.source_type': 'react-native' + } + ); + }); + + it('native context is empty W context is raw type', async () => { + const obj: any = 123; + await DdLogs.warn( + 'message', + 'kind', + 'message', + 'stacktrace', + obj + ); + + expect(InternalLog.log).toHaveBeenNthCalledWith( + 1, + expect.anything(), + SdkVerbosity.ERROR + ); + + expect( + NativeModules.DdLogs.warnWithError + ).toHaveBeenCalledWith( + 'message', + 'kind', + 'message', + 'stacktrace', + { '_dd.error.source_type': 'react-native' } + ); + }); + + it('native context is unmodified W context is a valid object', async () => { + await DdLogs.warn('message', 'kind', 'message', 'stacktrace', { + test: '123' + }); + expect( + NativeModules.DdLogs.warnWithError + ).toHaveBeenCalledWith( + 'message', + 'kind', + 'message', + 'stacktrace', + { test: '123', '_dd.error.source_type': 'react-native' } + ); + }); + }); + + describe('info logs', () => { + it('native context is empty W context is undefined', async () => { + await DdLogs.info( + 'message', + 'kind', + 'message', + 'stacktrace', + undefined + ); + expect( + NativeModules.DdLogs.infoWithError + ).toHaveBeenCalledWith( + 'message', + 'kind', + 'message', + 'stacktrace', + { '_dd.error.source_type': 'react-native' } + ); + }); + + it('native context is an object with nested property W context is an array', async () => { + await DdLogs.info('message', 'kind', 'message', 'stacktrace', [ + 1, + 2, + 3 + ]); + + expect(InternalLog.log).toHaveBeenNthCalledWith( + 1, + expect.anything(), + SdkVerbosity.WARN + ); + + expect(NativeModules.DdLogs.infoWithError).toHaveBeenCalledWith( + 'message', + 'kind', + 'message', + 'stacktrace', + { + context: [1, 2, 3], + '_dd.error.source_type': 'react-native' + } + ); + }); + + it('native context is empty W context is raw type', async () => { + const obj: any = 123; + await DdLogs.info( + 'message', + 'kind', + 'message', + 'stacktrace', + obj + ); + + expect(InternalLog.log).toHaveBeenNthCalledWith( + 1, + expect.anything(), + SdkVerbosity.ERROR + ); + + expect( + NativeModules.DdLogs.infoWithError + ).toHaveBeenCalledWith( + 'message', + 'kind', + 'message', + 'stacktrace', + { '_dd.error.source_type': 'react-native' } + ); + }); + + it('native context is unmodified W context is a valid object', async () => { + await DdLogs.info('message', 'kind', 'message', 'stacktrace', { + test: '123' + }); + expect( + NativeModules.DdLogs.infoWithError + ).toHaveBeenCalledWith( + 'message', + 'kind', + 'message', + 'stacktrace', + { test: '123', '_dd.error.source_type': 'react-native' } + ); + }); + }); + + describe('error logs', () => { + it('native context is empty W context is undefined', async () => { + await DdLogs.error( + 'message', + 'kind', + 'message', + 'stacktrace', + undefined + ); + expect( + NativeModules.DdLogs.errorWithError + ).toHaveBeenCalledWith( + 'message', + 'kind', + 'message', + 'stacktrace', + { '_dd.error.source_type': 'react-native' } + ); + }); + + it('native context is an object with nested property W context is an array', async () => { + await DdLogs.error('message', 'kind', 'message', 'stacktrace', [ + 1, + 2, + 3 + ]); + + expect(InternalLog.log).toHaveBeenNthCalledWith( + 1, + expect.anything(), + SdkVerbosity.WARN + ); + + expect( + NativeModules.DdLogs.errorWithError + ).toHaveBeenCalledWith( + 'message', + 'kind', + 'message', + 'stacktrace', + { + context: [1, 2, 3], + '_dd.error.source_type': 'react-native' + } + ); + }); + + it('native context is empty W context is raw type', async () => { + const obj: any = 123; + await DdLogs.error( + 'message', + 'kind', + 'message', + 'stacktrace', + obj + ); + + expect(InternalLog.log).toHaveBeenNthCalledWith( + 1, + expect.anything(), + SdkVerbosity.ERROR + ); + + expect( + NativeModules.DdLogs.errorWithError + ).toHaveBeenCalledWith( + 'message', + 'kind', + 'message', + 'stacktrace', + { '_dd.error.source_type': 'react-native' } + ); + }); + + it('native context is unmodified W context is a valid object', async () => { + await DdLogs.error('message', 'kind', 'message', 'stacktrace', { + test: '123' + }); + expect( + NativeModules.DdLogs.errorWithError + ).toHaveBeenCalledWith( + 'message', + 'kind', + 'message', + 'stacktrace', + { test: '123', '_dd.error.source_type': 'react-native' } + ); + }); + }); + }); }); diff --git a/packages/core/src/rum/DdRum.ts b/packages/core/src/rum/DdRum.ts index 5dbe2896e..81092d133 100644 --- a/packages/core/src/rum/DdRum.ts +++ b/packages/core/src/rum/DdRum.ts @@ -12,6 +12,7 @@ import { SdkVerbosity } from '../SdkVerbosity'; import type { DdNativeRumType } from '../nativeModulesTypes'; import { bufferVoidNativeCall } from '../sdk/DatadogProvider/Buffer/bufferNativeCall'; import { DdSdk } from '../sdk/DdSdk'; +import { validateContext } from '../utils/argsUtils'; import { DefaultTimeProvider } from '../utils/time-provider/DefaultTimeProvider'; import type { TimeProvider } from '../utils/time-provider/TimeProvider'; @@ -51,7 +52,12 @@ class DdRumWrapper implements DdRumType { SdkVerbosity.DEBUG ); return bufferVoidNativeCall(() => - this.nativeRum.startView(key, name, context, timestampMs) + this.nativeRum.startView( + key, + name, + validateContext(context), + timestampMs + ) ); }; @@ -62,7 +68,7 @@ class DdRumWrapper implements DdRumType { ): Promise => { InternalLog.log(`Stopping RUM View #${key}`, SdkVerbosity.DEBUG); return bufferVoidNativeCall(() => - this.nativeRum.stopView(key, context, timestampMs) + this.nativeRum.stopView(key, validateContext(context), timestampMs) ); }; @@ -78,7 +84,12 @@ class DdRumWrapper implements DdRumType { ); this.lastActionData = { type, name }; return bufferVoidNativeCall(() => - this.nativeRum.startAction(type, name, context, timestampMs) + this.nativeRum.startAction( + type, + name, + validateContext(context), + timestampMs + ) ); }; @@ -114,7 +125,7 @@ class DdRumWrapper implements DdRumType { const mappedEvent = this.actionEventMapper.applyEventMapper({ type, name, - context, + context: validateContext(context), timestampMs }); if (!mappedEvent) { @@ -161,7 +172,7 @@ class DdRumWrapper implements DdRumType { return [ args[0], args[1], - args[2] || {}, + validateContext(args[2]), args[3] || this.timeProvider.now() ]; } @@ -174,7 +185,7 @@ class DdRumWrapper implements DdRumType { return [ type, name, - args[0] || {}, + validateContext(args[0]), args[1] || this.timeProvider.now() ]; } @@ -202,7 +213,7 @@ class DdRumWrapper implements DdRumType { const mappedEvent = this.actionEventMapper.applyEventMapper({ type, name, - context, + context: validateContext(context), timestampMs, actionContext }); @@ -234,8 +245,15 @@ class DdRumWrapper implements DdRumType { `Starting RUM Resource #${key} ${method}: ${url}`, SdkVerbosity.DEBUG ); + return bufferVoidNativeCall(() => - this.nativeRum.startResource(key, method, url, context, timestampMs) + this.nativeRum.startResource( + key, + method, + url, + validateContext(context), + timestampMs + ) ); }; @@ -253,7 +271,7 @@ class DdRumWrapper implements DdRumType { statusCode, kind, size, - context, + context: validateContext(context), timestampMs, resourceContext }); @@ -304,7 +322,7 @@ class DdRumWrapper implements DdRumType { message, source, stacktrace, - context, + context: validateContext(context), timestampMs }); if (!mappedEvent) { diff --git a/packages/core/src/trace/DdTrace.ts b/packages/core/src/trace/DdTrace.ts index 2775d7c35..b106a97d8 100644 --- a/packages/core/src/trace/DdTrace.ts +++ b/packages/core/src/trace/DdTrace.ts @@ -12,6 +12,7 @@ import { bufferNativeCallWithId } from '../sdk/DatadogProvider/Buffer/bufferNativeCall'; import type { DdTraceType } from '../types'; +import { validateContext } from '../utils/argsUtils'; import { DefaultTimeProvider } from '../utils/time-provider/DefaultTimeProvider'; const timeProvider = new DefaultTimeProvider(); @@ -27,7 +28,11 @@ class DdTraceWrapper implements DdTraceType { timestampMs: number = timeProvider.now() ): Promise => { const spanId = bufferNativeCallReturningId(() => - this.nativeTrace.startSpan(operation, context, timestampMs) + this.nativeTrace.startSpan( + operation, + validateContext(context), + timestampMs + ) ); InternalLog.log( `Starting span “${operation}” #${spanId}`, @@ -43,7 +48,12 @@ class DdTraceWrapper implements DdTraceType { ): Promise => { InternalLog.log(`Finishing span #${spanId}`, SdkVerbosity.DEBUG); return bufferNativeCallWithId( - id => this.nativeTrace.finishSpan(id, context, timestampMs), + id => + this.nativeTrace.finishSpan( + id, + validateContext(context), + timestampMs + ), spanId ); }; diff --git a/packages/core/src/utils/__tests__/argsUtils.test.ts b/packages/core/src/utils/__tests__/argsUtils.test.ts new file mode 100644 index 000000000..db3cc24da --- /dev/null +++ b/packages/core/src/utils/__tests__/argsUtils.test.ts @@ -0,0 +1,59 @@ +import { InternalLog } from '../../InternalLog'; +import { SdkVerbosity } from '../../SdkVerbosity'; +import { validateContext } from '../argsUtils'; + +jest.mock('../../InternalLog', () => { + return { + InternalLog: { + log: jest.fn() + }, + DATADOG_MESSAGE_PREFIX: 'DATADOG:' + }; +}); + +describe('argsUtils', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('validateContext', () => { + it('returns empty object if context is null', () => { + expect(validateContext(null)).toEqual({}); + expect(validateContext(undefined)).toEqual({}); + }); + + it('returns empty object with error if context is raw type', () => { + expect(validateContext('raw-type')).toEqual({}); + expect(InternalLog.log).toHaveBeenNthCalledWith( + 1, + expect.anything(), + SdkVerbosity.ERROR + ); + }); + + it('nests array inside of new object if context is an array', () => { + const context = [{ a: 1, b: 2 }, 1, true]; + const validatedContext = validateContext(context); + + expect(InternalLog.log).toHaveBeenNthCalledWith( + 1, + expect.anything(), + SdkVerbosity.WARN + ); + + expect(validatedContext).toEqual({ + context + }); + }); + + it('returns unmodified context if it is a valid object', () => { + const context = { + testA: 1, + testB: {} + }; + const validatedContext = validateContext(context); + + expect(validatedContext).toEqual(context); + }); + }); +}); diff --git a/packages/core/src/utils/argsUtils.ts b/packages/core/src/utils/argsUtils.ts new file mode 100644 index 000000000..8fe70502b --- /dev/null +++ b/packages/core/src/utils/argsUtils.ts @@ -0,0 +1,28 @@ +import { InternalLog } from '../InternalLog'; +import { SdkVerbosity } from '../SdkVerbosity'; + +export const validateContext = (context: any) => { + if (!context) { + return {}; + } + + // eslint-disable-next-line eqeqeq + if (context.constructor == Object) { + return context; + } + + if (Array.isArray(context)) { + InternalLog.log( + "The given context is an array, it will be nested in 'context' property inside a new object.", + SdkVerbosity.WARN + ); + return { context }; + } + + InternalLog.log( + `The given context (${context}) is invalid - it must be an object. Context will be empty.`, + SdkVerbosity.ERROR + ); + + return {}; +}; From f8008352525119d8c1a975e093c2cd2b9194c196 Mon Sep 17 00:00:00 2001 From: Marco Saia Date: Thu, 13 Jun 2024 11:22:03 +0200 Subject: [PATCH 2/3] Added context validation RUM tests --- packages/core/src/rum/DdRum.ts | 174 +++--- packages/core/src/rum/__tests__/DdRum.test.ts | 552 ++++++++++++++++++ 2 files changed, 639 insertions(+), 87 deletions(-) diff --git a/packages/core/src/rum/DdRum.ts b/packages/core/src/rum/DdRum.ts index 81092d133..e13660374 100644 --- a/packages/core/src/rum/DdRum.ts +++ b/packages/core/src/rum/DdRum.ts @@ -116,93 +116,6 @@ class DdRumWrapper implements DdRumType { this.timeProvider = timeProvider; }; - private callNativeStopAction = ( - type: RumActionType, - name: string, - context: object, - timestampMs: number - ): Promise => { - const mappedEvent = this.actionEventMapper.applyEventMapper({ - type, - name, - context: validateContext(context), - timestampMs - }); - if (!mappedEvent) { - return bufferVoidNativeCall(() => - this.nativeRum.stopAction( - type, - name, - { - '_dd.action.drop_action': true - }, - timestampMs - ) - ); - } - - return bufferVoidNativeCall(() => - this.nativeRum.stopAction( - mappedEvent.type, - mappedEvent.name, - mappedEvent.context, - mappedEvent.timestampMs - ) - ); - }; - - private getStopActionNativeCallArgs = ( - args: - | [ - type: RumActionType, - name: string, - context?: object, - timestampMs?: number - ] - | [context?: object, timestampMs?: number] - ): - | [ - type: RumActionType, - name: string, - context: object, - timestampMs: number - ] - | null => { - if (isNewStopActionAPI(args)) { - return [ - args[0], - args[1], - validateContext(args[2]), - args[3] || this.timeProvider.now() - ]; - } - if (isOldStopActionAPI(args)) { - if (this.lastActionData) { - DdSdk.telemetryDebug( - 'DDdRum.stopAction called with the old signature' - ); - const { type, name } = this.lastActionData; - return [ - type, - name, - validateContext(args[0]), - args[1] || this.timeProvider.now() - ]; - } - InternalLog.log( - 'DdRum.startAction needs to be called before DdRum.stopAction', - SdkVerbosity.WARN - ); - } else { - InternalLog.log( - 'DdRum.stopAction was called with wrong arguments', - SdkVerbosity.WARN - ); - } - - return null; - }; - addAction = ( type: RumActionType, name: string, @@ -402,6 +315,93 @@ class DdRumWrapper implements DdRumType { unregisterActionEventMapper() { this.actionEventMapper = generateActionEventMapper(undefined); } + + private callNativeStopAction = ( + type: RumActionType, + name: string, + context: object, + timestampMs: number + ): Promise => { + const mappedEvent = this.actionEventMapper.applyEventMapper({ + type, + name, + context: validateContext(context), + timestampMs + }); + if (!mappedEvent) { + return bufferVoidNativeCall(() => + this.nativeRum.stopAction( + type, + name, + { + '_dd.action.drop_action': true + }, + timestampMs + ) + ); + } + + return bufferVoidNativeCall(() => + this.nativeRum.stopAction( + mappedEvent.type, + mappedEvent.name, + mappedEvent.context, + mappedEvent.timestampMs + ) + ); + }; + + private getStopActionNativeCallArgs = ( + args: + | [ + type: RumActionType, + name: string, + context?: object, + timestampMs?: number + ] + | [context?: object, timestampMs?: number] + ): + | [ + type: RumActionType, + name: string, + context: object, + timestampMs: number + ] + | null => { + if (isNewStopActionAPI(args)) { + return [ + args[0], + args[1], + validateContext(args[2]), + args[3] || this.timeProvider.now() + ]; + } + if (isOldStopActionAPI(args)) { + if (this.lastActionData) { + DdSdk.telemetryDebug( + 'DDdRum.stopAction called with the old signature' + ); + const { type, name } = this.lastActionData; + return [ + type, + name, + validateContext(args[0]), + args[1] || this.timeProvider.now() + ]; + } + InternalLog.log( + 'DdRum.startAction needs to be called before DdRum.stopAction', + SdkVerbosity.WARN + ); + } else { + InternalLog.log( + 'DdRum.stopAction was called with wrong arguments', + SdkVerbosity.WARN + ); + } + + return null; + }; } const isNewStopActionAPI = ( diff --git a/packages/core/src/rum/__tests__/DdRum.test.ts b/packages/core/src/rum/__tests__/DdRum.test.ts index f155ad927..cf4cd0f19 100644 --- a/packages/core/src/rum/__tests__/DdRum.test.ts +++ b/packages/core/src/rum/__tests__/DdRum.test.ts @@ -8,6 +8,8 @@ import { NativeModules } from 'react-native'; import { DdSdkReactNative } from '../../DdSdkReactNative'; +import { InternalLog } from '../../InternalLog'; +import { SdkVerbosity } from '../../SdkVerbosity'; import { BufferSingleton } from '../../sdk/DatadogProvider/Buffer/BufferSingleton'; import { DdSdk } from '../../sdk/DdSdk'; import { DdRum } from '../DdRum'; @@ -24,12 +26,562 @@ jest.mock('../../utils/time-provider/DefaultTimeProvider', () => { }; }); +jest.mock('../../InternalLog', () => { + return { + InternalLog: { + log: jest.fn() + }, + DATADOG_MESSAGE_PREFIX: 'DATADOG:' + }; +}); + describe('DdRum', () => { beforeEach(() => { jest.clearAllMocks(); BufferSingleton.onInitialization(); }); + describe('Context validation', () => { + describe('DdRum.startView', () => { + test('uses given context when context is valid', async () => { + const context = { + testA: 123, + testB: 'ok' + }; + await DdRum.startView('key', 'name', context); + + expect(NativeModules.DdRum.startView).toHaveBeenCalledWith( + expect.anything(), + expect.anything(), + context, + expect.anything() + ); + }); + + test('uses empty context with error when context is invalid or null', async () => { + const context: any = 123; + await DdRum.startView('key', 'name', context); + + expect(InternalLog.log).toHaveBeenNthCalledWith( + 2, + expect.anything(), + SdkVerbosity.ERROR + ); + + expect(NativeModules.DdRum.startView).toHaveBeenCalledWith( + expect.anything(), + expect.anything(), + {}, + expect.anything() + ); + }); + + test('nests given context in new object when context is array', async () => { + const context: any = [123, '456']; + await DdRum.startView('key', 'name', context); + + expect(InternalLog.log).toHaveBeenNthCalledWith( + 2, + expect.anything(), + SdkVerbosity.WARN + ); + + expect(NativeModules.DdRum.startView).toHaveBeenCalledWith( + expect.anything(), + expect.anything(), + { context }, + expect.anything() + ); + }); + }); + + describe('DdRum.stopView', () => { + test('uses given context when context is valid', async () => { + const context = { + testA: 123, + testB: 'ok' + }; + await DdRum.startView('key', 'name'); + await DdRum.stopView('key', context); + + expect(NativeModules.DdRum.stopView).toHaveBeenCalledWith( + 'key', + context, + expect.anything() + ); + }); + + test('uses empty context with error when context is invalid or null', async () => { + const context: any = 123; + + await DdRum.startView('key', 'name'); + await DdRum.stopView('key', context); + + expect(InternalLog.log).toHaveBeenNthCalledWith( + 3, + expect.anything(), + SdkVerbosity.ERROR + ); + + expect(NativeModules.DdRum.stopView).toHaveBeenCalledWith( + 'key', + {}, + expect.anything() + ); + }); + + test('nests given context in new object when context is array', async () => { + const context: any = [123, '456']; + + await DdRum.startView('key', 'name'); + await DdRum.stopView('key', context); + + expect(InternalLog.log).toHaveBeenNthCalledWith( + 3, + expect.anything(), + SdkVerbosity.WARN + ); + + expect(NativeModules.DdRum.stopView).toHaveBeenCalledWith( + 'key', + { context }, + expect.anything() + ); + }); + }); + + describe('DdRum.startAction', () => { + test('uses given context when context is valid', async () => { + const context = { + testA: 123, + testB: 'ok' + }; + await DdRum.startAction(RumActionType.SCROLL, 'name', context); + + expect(NativeModules.DdRum.startAction).toHaveBeenCalledWith( + expect.anything(), + expect.anything(), + context, + expect.anything() + ); + }); + + test('uses empty context with error when context is invalid or null', async () => { + const context: any = 123; + await DdRum.startAction(RumActionType.SCROLL, 'name', context); + + expect(InternalLog.log).toHaveBeenNthCalledWith( + 2, + expect.anything(), + SdkVerbosity.ERROR + ); + + expect(NativeModules.DdRum.startAction).toHaveBeenCalledWith( + expect.anything(), + expect.anything(), + {}, + expect.anything() + ); + }); + + test('nests given context in new object when context is array', async () => { + const context: any = [123, '456']; + await DdRum.startAction(RumActionType.SCROLL, 'name', context); + + expect(InternalLog.log).toHaveBeenNthCalledWith( + 2, + expect.anything(), + SdkVerbosity.WARN + ); + + expect(NativeModules.DdRum.startAction).toHaveBeenCalledWith( + expect.anything(), + expect.anything(), + { context }, + expect.anything() + ); + }); + }); + + describe('DdRum.stopAction', () => { + describe('New API', () => { + test('uses given context when context is valid', async () => { + const context = { + testA: 123, + testB: 'ok' + }; + await DdRum.startAction(RumActionType.SCROLL, 'name'); + await DdRum.stopAction( + RumActionType.SCROLL, + 'name', + context + ); + + expect(NativeModules.DdRum.stopAction).toHaveBeenCalledWith( + RumActionType.SCROLL, + 'name', + context, + expect.anything() + ); + }); + + test('uses empty context with error when context is invalid or null', async () => { + const context: any = 123; + + await DdRum.startAction(RumActionType.SCROLL, 'name'); + await DdRum.stopAction( + RumActionType.SCROLL, + 'name', + context + ); + + expect(InternalLog.log).toHaveBeenNthCalledWith( + 3, + expect.anything(), + SdkVerbosity.ERROR + ); + + expect(NativeModules.DdRum.stopAction).toHaveBeenCalledWith( + RumActionType.SCROLL, + 'name', + {}, + expect.anything() + ); + }); + + test('nests given context in new object when context is array', async () => { + const context: any = [123, '456']; + + await DdRum.startAction(RumActionType.SCROLL, 'name'); + await DdRum.stopAction( + RumActionType.SCROLL, + 'name', + context + ); + + expect(InternalLog.log).toHaveBeenNthCalledWith( + 3, + expect.anything(), + SdkVerbosity.WARN + ); + + expect(NativeModules.DdRum.stopAction).toHaveBeenCalledWith( + RumActionType.SCROLL, + 'name', + { context }, + expect.anything() + ); + }); + }); + + describe('Old API', () => { + test('uses given context when context is valid', async () => { + const context = { + testA: 123, + testB: 'ok' + }; + await DdRum.startAction(RumActionType.SCROLL, 'name'); + await DdRum.stopAction(context); + + expect(NativeModules.DdRum.stopAction).toHaveBeenCalledWith( + RumActionType.SCROLL, + 'name', + context, + expect.anything() + ); + }); + + test('uses empty context with error when context is invalid or null', async () => { + await DdRum.startAction(RumActionType.SCROLL, 'name'); + await DdRum.stopAction(undefined); + + expect(NativeModules.DdRum.stopAction).toHaveBeenCalledWith( + RumActionType.SCROLL, + 'name', + {}, + expect.anything() + ); + }); + + test('nests given context in new object when context is array', async () => { + const context: any = [123, '456']; + + await DdRum.startAction(RumActionType.SCROLL, 'name'); + await DdRum.stopAction(context); + + expect(InternalLog.log).toHaveBeenNthCalledWith( + 3, + expect.anything(), + SdkVerbosity.WARN + ); + + expect(NativeModules.DdRum.stopAction).toHaveBeenCalledWith( + RumActionType.SCROLL, + 'name', + { context }, + expect.anything() + ); + }); + }); + }); + + describe('DdRum.startResource', () => { + test('uses given context when context is valid', async () => { + const context = { + testA: 123, + testB: 'ok' + }; + await DdRum.startResource('key', 'method', 'url', context); + + expect(NativeModules.DdRum.startResource).toHaveBeenCalledWith( + 'key', + 'method', + 'url', + context, + expect.anything() + ); + }); + + test('uses empty context with error when context is invalid or null', async () => { + const context: any = 123; + + await DdRum.startResource('key', 'method', 'url', context); + + expect(InternalLog.log).toHaveBeenNthCalledWith( + 2, + expect.anything(), + SdkVerbosity.ERROR + ); + + expect(NativeModules.DdRum.startResource).toHaveBeenCalledWith( + 'key', + 'method', + 'url', + {}, + expect.anything() + ); + }); + + test('nests given context in new object when context is array', async () => { + const context: any = [123, '456']; + + await DdRum.startResource('key', 'method', 'url', context); + + expect(InternalLog.log).toHaveBeenNthCalledWith( + 2, + expect.anything(), + SdkVerbosity.WARN + ); + + expect(NativeModules.DdRum.startResource).toHaveBeenCalledWith( + 'key', + 'method', + 'url', + { context }, + expect.anything() + ); + }); + }); + + describe('DdRum.stopResource', () => { + test('uses given context when context is valid', async () => { + const context = { + testA: 123, + testB: 'ok' + }; + + await DdRum.startResource('key', 'method', 'url', {}); + await DdRum.stopResource('key', 200, 'other', -1, context); + + expect(NativeModules.DdRum.stopResource).toHaveBeenCalledWith( + 'key', + 200, + 'other', + -1, + context, + expect.anything() + ); + }); + + test('uses empty context with error when context is invalid or null', async () => { + const context: any = 123; + + await DdRum.startResource('key', 'method', 'url', {}); + await DdRum.stopResource('key', 200, 'other', -1, context); + + expect(InternalLog.log).toHaveBeenNthCalledWith( + 2, + expect.anything(), + SdkVerbosity.ERROR + ); + + expect(NativeModules.DdRum.stopResource).toHaveBeenCalledWith( + 'key', + 200, + 'other', + -1, + {}, + expect.anything() + ); + }); + + test('nests given context in new object when context is array', async () => { + const context: any = [123, '456']; + + await DdRum.startResource('key', 'method', 'url', {}); + await DdRum.stopResource('key', 200, 'other', -1, context); + + expect(InternalLog.log).toHaveBeenNthCalledWith( + 2, + expect.anything(), + SdkVerbosity.WARN + ); + + expect(NativeModules.DdRum.stopResource).toHaveBeenCalledWith( + 'key', + 200, + 'other', + -1, + { context }, + expect.anything() + ); + }); + }); + + describe('DdRum.addAction', () => { + test('uses given context when context is valid', async () => { + const context = { + testA: 123, + testB: 'ok' + }; + await DdRum.addAction(RumActionType.SCROLL, 'name', context); + + expect(NativeModules.DdRum.addAction).toHaveBeenCalledWith( + expect.anything(), + expect.anything(), + context, + expect.anything() + ); + }); + + test('uses empty context with error when context is invalid or null', async () => { + const context: any = 123; + await DdRum.addAction(RumActionType.SCROLL, 'name', context); + + expect(InternalLog.log).toHaveBeenNthCalledWith( + 1, + expect.anything(), + SdkVerbosity.ERROR + ); + + expect(NativeModules.DdRum.addAction).toHaveBeenCalledWith( + expect.anything(), + expect.anything(), + {}, + expect.anything() + ); + }); + + test('nests given context in new object when context is array', async () => { + const context: any = [123, '456']; + await DdRum.addAction(RumActionType.SCROLL, 'name', context); + + expect(InternalLog.log).toHaveBeenNthCalledWith( + 1, + expect.anything(), + SdkVerbosity.WARN + ); + + expect(NativeModules.DdRum.addAction).toHaveBeenCalledWith( + expect.anything(), + expect.anything(), + { context }, + expect.anything() + ); + }); + }); + + describe('DdRum.addError', () => { + test('uses given context when context is valid', async () => { + const context = { + testA: 123, + testB: 'ok' + }; + + await DdRum.addError( + 'error', + ErrorSource.CUSTOM, + 'stacktrace', + context + ); + + expect(NativeModules.DdRum.addError).toHaveBeenCalledWith( + 'error', + ErrorSource.CUSTOM, + 'stacktrace', + { + ...context, + '_dd.error.source_type': 'react-native' + }, + expect.anything() + ); + }); + + test('uses empty context with error when context is invalid or null', async () => { + const context: any = 123; + await DdRum.addError( + 'error', + ErrorSource.CUSTOM, + 'stacktrace', + context + ); + + expect(InternalLog.log).toHaveBeenNthCalledWith( + 1, + expect.anything(), + SdkVerbosity.ERROR + ); + + expect(NativeModules.DdRum.addError).toHaveBeenCalledWith( + 'error', + ErrorSource.CUSTOM, + 'stacktrace', + { + '_dd.error.source_type': 'react-native' + }, + expect.anything() + ); + }); + + test('nests given context in new object when context is array', async () => { + const context: any = [123, '456']; + await DdRum.addError( + 'error', + ErrorSource.CUSTOM, + 'stacktrace', + context + ); + + expect(InternalLog.log).toHaveBeenNthCalledWith( + 1, + expect.anything(), + SdkVerbosity.WARN + ); + + expect(NativeModules.DdRum.addError).toHaveBeenCalledWith( + 'error', + ErrorSource.CUSTOM, + 'stacktrace', + { + context, + '_dd.error.source_type': 'react-native' + }, + expect.anything() + ); + }); + }); + }); + describe('DdRum.stopAction', () => { test('calls the native SDK when called with new API', async () => { await DdRum.stopAction( From 988a827d15fdbd3fb2be2f596d9a850834647c06 Mon Sep 17 00:00:00 2001 From: Marco Saia Date: Thu, 13 Jun 2024 11:44:05 +0200 Subject: [PATCH 3/3] Added context validation Traces tests --- .../core/src/trace/__tests__/DdTrace.test.ts | 139 ++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 packages/core/src/trace/__tests__/DdTrace.test.ts diff --git a/packages/core/src/trace/__tests__/DdTrace.test.ts b/packages/core/src/trace/__tests__/DdTrace.test.ts new file mode 100644 index 000000000..566d9908d --- /dev/null +++ b/packages/core/src/trace/__tests__/DdTrace.test.ts @@ -0,0 +1,139 @@ +import { NativeModules } from 'react-native'; + +import { InternalLog } from '../../InternalLog'; +import { SdkVerbosity } from '../../SdkVerbosity'; +import { BufferSingleton } from '../../sdk/DatadogProvider/Buffer/BufferSingleton'; +import { DdTrace } from '../DdTrace'; + +jest.mock('../../utils/time-provider/DefaultTimeProvider', () => { + return { + DefaultTimeProvider: jest.fn().mockImplementation(() => { + return { now: jest.fn().mockReturnValue(456) }; + }) + }; +}); + +jest.mock('../../InternalLog', () => { + return { + InternalLog: { + log: jest.fn() + }, + DATADOG_MESSAGE_PREFIX: 'DATADOG:' + }; +}); + +describe('DdTrace', () => { + beforeEach(() => { + jest.clearAllMocks(); + BufferSingleton.onInitialization(); + }); + + describe('Context validation', () => { + describe('DdTrace.startSpan', () => { + test('uses given context when context is valid', async () => { + const context = { + testA: 123, + testB: 'ok' + }; + await DdTrace.startSpan('operation', context); + + expect(NativeModules.DdTrace.startSpan).toHaveBeenCalledWith( + 'operation', + context, + expect.anything() + ); + }); + + test('uses empty context with error when context is invalid or null', async () => { + const context: any = 123; + await DdTrace.startSpan('operation', context); + + expect(InternalLog.log).toHaveBeenNthCalledWith( + 1, + expect.anything(), + SdkVerbosity.ERROR + ); + + expect(NativeModules.DdTrace.startSpan).toHaveBeenCalledWith( + 'operation', + {}, + expect.anything() + ); + }); + + test('nests given context in new object when context is array', async () => { + const context: any = [123, '456']; + await DdTrace.startSpan('operation', context); + + expect(InternalLog.log).toHaveBeenNthCalledWith( + 1, + expect.anything(), + SdkVerbosity.WARN + ); + + expect(NativeModules.DdTrace.startSpan).toHaveBeenCalledWith( + 'operation', + { context }, + expect.anything() + ); + }); + }); + + describe('DdTrace.finishSpan', () => { + test('uses given context when context is valid', async () => { + const context = { + testA: 123, + testB: 'ok' + }; + + const spanId = await DdTrace.startSpan('operation', {}); + await DdTrace.finishSpan(spanId, context); + + expect(NativeModules.DdTrace.finishSpan).toHaveBeenCalledWith( + spanId, + context, + expect.anything() + ); + }); + + test('uses empty context with error when context is invalid or null', async () => { + const context: any = 123; + await DdTrace.startSpan('operation', context); + + const spanId = await DdTrace.startSpan('operation', {}); + await DdTrace.finishSpan(spanId, context); + + expect(InternalLog.log).toHaveBeenNthCalledWith( + 1, + expect.anything(), + SdkVerbosity.ERROR + ); + + expect(NativeModules.DdTrace.finishSpan).toHaveBeenCalledWith( + spanId, + {}, + expect.anything() + ); + }); + + test('nests given context in new object when context is array', async () => { + const context: any = [123, '456']; + + const spanId = await DdTrace.startSpan('operation', {}); + await DdTrace.finishSpan(spanId, context); + + expect(InternalLog.log).toHaveBeenNthCalledWith( + 3, + expect.anything(), + SdkVerbosity.WARN + ); + + expect(NativeModules.DdTrace.finishSpan).toHaveBeenCalledWith( + spanId, + { context }, + expect.anything() + ); + }); + }); + }); +});