From 3d9bd487d8cae9279e87d267e62e3f6b68c48fa2 Mon Sep 17 00:00:00 2001 From: "Xavier F. Gouchet" Date: Mon, 7 Apr 2025 14:55:53 +0200 Subject: [PATCH 001/410] RUM-9023 use session id to sample network traces --- .../distributedTracing/distributedTracing.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/core/src/rum/instrumentation/resourceTracking/distributedTracing/distributedTracing.tsx b/packages/core/src/rum/instrumentation/resourceTracking/distributedTracing/distributedTracing.tsx index e3f273bab..39df08bb8 100644 --- a/packages/core/src/rum/instrumentation/resourceTracking/distributedTracing/distributedTracing.tsx +++ b/packages/core/src/rum/instrumentation/resourceTracking/distributedTracing/distributedTracing.tsx @@ -77,7 +77,12 @@ export const generateTracingAttributesWithSampling = ( } const traceId = TracingIdentifier.createTraceId(); - const hash = Number(traceId.id.multiply(knuthFactor).remainder(twoPow64)); + // for a UUID with value aaaaaaaa-bbbb-Mccc-Nddd-1234567890ab + // we use as the input id the last part : 0x1234567890ab + const baseId = rumSessionId + ? BigInt(rumSessionId.split('-')[4], 16) + : traceId.id; + const hash = Number(baseId.multiply(knuthFactor).remainder(twoPow64)); const threshold = (tracingSamplingRate / 100) * Number(twoPow64); const isSampled = hash <= threshold; From 9ebd011944c71b22559e13d8a184356a245aad67 Mon Sep 17 00:00:00 2001 From: "Xavier F. Gouchet" Date: Mon, 28 Apr 2025 13:55:23 +0200 Subject: [PATCH 002/410] RUM-7747 update default tracing sampling rate --- packages/core/ios/Sources/RNDdSdkConfiguration.swift | 2 +- packages/core/src/DdSdkReactNativeConfiguration.tsx | 2 +- .../src/__tests__/DdSdkReactNativeConfiguration.test.ts | 6 +++--- .../sdk/DatadogProvider/__tests__/initialization.test.tsx | 2 +- .../__tests__/FileBasedConfiguration.test.ts | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/core/ios/Sources/RNDdSdkConfiguration.swift b/packages/core/ios/Sources/RNDdSdkConfiguration.swift index 9ed2d8fb6..ad08406db 100644 --- a/packages/core/ios/Sources/RNDdSdkConfiguration.swift +++ b/packages/core/ios/Sources/RNDdSdkConfiguration.swift @@ -194,7 +194,7 @@ extension NSArray { internal struct DefaultConfiguration { static let nativeCrashReportEnabled = false static let sessionSamplingRate = 100.0 - static let resourceTracingSamplingRate = 20.0 + static let resourceTracingSamplingRate = 100.0 static let longTaskThresholdMs = 0.0 static let nativeLongTaskThresholdMs = 200.0 static let nativeViewTracking = false diff --git a/packages/core/src/DdSdkReactNativeConfiguration.tsx b/packages/core/src/DdSdkReactNativeConfiguration.tsx index 158d258eb..9be08fa28 100644 --- a/packages/core/src/DdSdkReactNativeConfiguration.tsx +++ b/packages/core/src/DdSdkReactNativeConfiguration.tsx @@ -109,7 +109,7 @@ export const formatFirstPartyHosts = ( export const DEFAULTS = { nativeCrashReportEnabled: false, sessionSamplingRate: 100.0, - resourceTracingSamplingRate: 20.0, + resourceTracingSamplingRate: 100.0, site: 'US1', longTaskThresholdMs: 0, nativeLongTaskThresholdMs: 200, diff --git a/packages/core/src/__tests__/DdSdkReactNativeConfiguration.test.ts b/packages/core/src/__tests__/DdSdkReactNativeConfiguration.test.ts index 6d9d081af..b1522ef7f 100644 --- a/packages/core/src/__tests__/DdSdkReactNativeConfiguration.test.ts +++ b/packages/core/src/__tests__/DdSdkReactNativeConfiguration.test.ts @@ -51,7 +51,7 @@ describe('DdSdkReactNativeConfiguration', () => { "nativeViewTracking": false, "proxyConfig": undefined, "resourceEventMapper": null, - "resourceTracingSamplingRate": 20, + "resourceTracingSamplingRate": 100, "serviceName": undefined, "sessionSamplingRate": 100, "site": "US1", @@ -79,7 +79,7 @@ describe('DdSdkReactNativeConfiguration', () => { trackInteractions: true, trackResources: true, firstPartyHosts: ['api.com'], - resourceTracingSamplingRate: 100, + resourceTracingSamplingRate: 80, logEventMapper: event => event, errorEventMapper: event => event, resourceEventMapper: event => event, @@ -161,7 +161,7 @@ describe('DdSdkReactNativeConfiguration', () => { "type": "https", }, "resourceEventMapper": [Function], - "resourceTracingSamplingRate": 100, + "resourceTracingSamplingRate": 80, "serviceName": "com.test.app", "sessionSamplingRate": 80, "site": "EU", diff --git a/packages/core/src/sdk/DatadogProvider/__tests__/initialization.test.tsx b/packages/core/src/sdk/DatadogProvider/__tests__/initialization.test.tsx index ff5b4a808..0a52678e6 100644 --- a/packages/core/src/sdk/DatadogProvider/__tests__/initialization.test.tsx +++ b/packages/core/src/sdk/DatadogProvider/__tests__/initialization.test.tsx @@ -93,7 +93,7 @@ describe('DatadogProvider', () => { "nativeLongTaskThresholdMs": 200, "nativeViewTracking": false, "proxyConfig": undefined, - "resourceTracingSamplingRate": 20, + "resourceTracingSamplingRate": 100, "sampleRate": 100, "serviceName": undefined, "site": "US1", diff --git a/packages/core/src/sdk/FileBasedConfiguration/__tests__/FileBasedConfiguration.test.ts b/packages/core/src/sdk/FileBasedConfiguration/__tests__/FileBasedConfiguration.test.ts index 514a01cfb..a21bb0e07 100644 --- a/packages/core/src/sdk/FileBasedConfiguration/__tests__/FileBasedConfiguration.test.ts +++ b/packages/core/src/sdk/FileBasedConfiguration/__tests__/FileBasedConfiguration.test.ts @@ -133,7 +133,7 @@ describe('FileBasedConfiguration', () => { "nativeViewTracking": false, "proxyConfig": undefined, "resourceEventMapper": null, - "resourceTracingSamplingRate": 20, + "resourceTracingSamplingRate": 100, "serviceName": undefined, "sessionSamplingRate": 100, "site": "US1", From 472e51de19a4526883537fe041635aab05d77c22 Mon Sep 17 00:00:00 2001 From: Sergio Barrio Date: Tue, 2 Sep 2025 10:49:34 +0200 Subject: [PATCH 003/410] Remove fatal errors from logs --- .../DdRumErrorTracking.test.tsx | 202 +----------------- .../core/src/logs/__tests__/DdLogs.test.ts | 4 +- .../instrumentation/DdRumErrorTracking.tsx | 41 +--- 3 files changed, 14 insertions(+), 233 deletions(-) diff --git a/packages/core/src/__tests__/rum/instrumentation/DdRumErrorTracking.test.tsx b/packages/core/src/__tests__/rum/instrumentation/DdRumErrorTracking.test.tsx index 7f66bb58e..fc34de5b0 100644 --- a/packages/core/src/__tests__/rum/instrumentation/DdRumErrorTracking.test.tsx +++ b/packages/core/src/__tests__/rum/instrumentation/DdRumErrorTracking.test.tsx @@ -6,17 +6,13 @@ import { NativeModules } from 'react-native'; -import type { - DdNativeLogsType, - DdNativeRumType -} from '../../../nativeModulesTypes'; +import type { DdNativeRumType } from '../../../nativeModulesTypes'; import { DdRumErrorTracking } from '../../../rum/instrumentation/DdRumErrorTracking'; import { BufferSingleton } from '../../../sdk/DatadogProvider/Buffer/BufferSingleton'; jest.mock('../../../utils/jsUtils'); const DdRum = NativeModules.DdRum as DdNativeRumType; -const DdLogs = NativeModules.DdLogs as DdNativeLogsType; let baseErrorHandlerCalled = false; const baseErrorHandler = (error: any, isFatal?: boolean) => { @@ -77,19 +73,6 @@ it('M intercept and send a RUM event W onGlobalError() {no message}', async () = '' ); expect(baseErrorHandlerCalled).toStrictEqual(true); - expect(DdLogs.errorWithError).toHaveBeenCalledTimes(1); - expect(DdLogs.errorWithError).toHaveBeenCalledWith( - '[object Object]', - 'Error', - '[object Object]', - 'doSomething() at ./path/to/file.js:67:3', - { - '_dd.error.raw': error, - '_dd.error.is_crash': is_fatal, - '_dd.error.source_type': 'react-native', - '_dd.error_log.is_crash': true - } - ); }); it('M intercept and send a RUM event W onGlobalError() {empty stack trace}', async () => { @@ -119,19 +102,6 @@ it('M intercept and send a RUM event W onGlobalError() {empty stack trace}', asy '' ); expect(baseErrorHandlerCalled).toStrictEqual(true); - expect(DdLogs.errorWithError).toHaveBeenCalledTimes(1); - expect(DdLogs.errorWithError).toHaveBeenCalledWith( - 'Something bad happened', - 'Error', - 'Something bad happened', - '', - { - '_dd.error.raw': error, - '_dd.error.is_crash': is_fatal, - '_dd.error.source_type': 'react-native', - '_dd.error_log.is_crash': true - } - ); }); it('M intercept and send a RUM event W onGlobalError() {Error object}', async () => { @@ -162,19 +132,6 @@ it('M intercept and send a RUM event W onGlobalError() {Error object}', async () '/packages/core/src/__tests__/rum/instrumentation/DdRumErrorTracking.test.tsx' ); expect(baseErrorHandlerCalled).toStrictEqual(true); - expect(DdLogs.errorWithError).toHaveBeenCalledTimes(1); - expect(DdLogs.errorWithError).toHaveBeenCalledWith( - 'Something bad happened', - 'Error', - 'Something bad happened', - expect.stringContaining('Error: Something bad happened'), - { - '_dd.error.raw': error, - '_dd.error.is_crash': is_fatal, - '_dd.error.source_type': 'react-native', - '_dd.error_log.is_crash': true - } - ); }); it('M intercept and send a RUM event W onGlobalError() {CustomError object}', async () => { @@ -209,19 +166,6 @@ it('M intercept and send a RUM event W onGlobalError() {CustomError object}', as '/packages/core/src/__tests__/rum/instrumentation/DdRumErrorTracking.test.tsx' ); expect(baseErrorHandlerCalled).toStrictEqual(true); - expect(DdLogs.errorWithError).toHaveBeenCalledTimes(1); - expect(DdLogs.errorWithError).toHaveBeenCalledWith( - 'Something bad happened', - 'CustomError', - 'Something bad happened', - expect.stringContaining('Error: Something bad happened'), - { - '_dd.error.raw': error, - '_dd.error.is_crash': is_fatal, - '_dd.error.source_type': 'react-native', - '_dd.error_log.is_crash': true - } - ); }); it('M intercept and send a RUM event W onGlobalError() {with source file info}', async () => { @@ -254,19 +198,6 @@ it('M intercept and send a RUM event W onGlobalError() {with source file info}', '' ); expect(baseErrorHandlerCalled).toStrictEqual(true); - expect(DdLogs.errorWithError).toHaveBeenCalledTimes(1); - expect(DdLogs.errorWithError).toHaveBeenCalledWith( - 'Something bad happened', - 'Error', - 'Something bad happened', - 'at ./path/to/file.js:1038:57', - { - '_dd.error.raw': error, - '_dd.error.is_crash': is_fatal, - '_dd.error.source_type': 'react-native', - '_dd.error_log.is_crash': true - } - ); }); it('M intercept and send a RUM event W onGlobalError() {with component stack}', async () => { @@ -301,19 +232,6 @@ it('M intercept and send a RUM event W onGlobalError() {with component stack}', '' ); expect(baseErrorHandlerCalled).toStrictEqual(true); - expect(DdLogs.errorWithError).toHaveBeenCalledTimes(1); - expect(DdLogs.errorWithError).toHaveBeenCalledWith( - 'Something bad happened', - 'Error', - 'Something bad happened', - 'doSomething() at ./path/to/file.js:67:3,nestedCall() at ./path/to/file.js:1064:9,root() at ./path/to/index.js:10:1', - { - '_dd.error.raw': error, - '_dd.error.is_crash': is_fatal, - '_dd.error.source_type': 'react-native', - '_dd.error_log.is_crash': true - } - ); }); it('M intercept and send a RUM event W onGlobalError() {with stack}', async () => { @@ -348,19 +266,6 @@ it('M intercept and send a RUM event W onGlobalError() {with stack}', async () = '' ); expect(baseErrorHandlerCalled).toStrictEqual(true); - expect(DdLogs.errorWithError).toHaveBeenCalledTimes(1); - expect(DdLogs.errorWithError).toHaveBeenCalledWith( - 'Something bad happened', - 'Error', - 'Something bad happened', - 'doSomething() at ./path/to/file.js:67:3,nestedCall() at ./path/to/file.js:1064:9,root() at ./path/to/index.js:10:1', - { - '_dd.error.raw': error, - '_dd.error.is_crash': is_fatal, - '_dd.error.source_type': 'react-native', - '_dd.error_log.is_crash': true - } - ); }); it('M intercept and send a RUM event W onGlobalError() {with stacktrace}', async () => { @@ -396,19 +301,6 @@ it('M intercept and send a RUM event W onGlobalError() {with stacktrace}', async ); expect(baseErrorHandlerCalled).toStrictEqual(true); expect(baseErrorHandlerCalled).toStrictEqual(true); - expect(DdLogs.errorWithError).toHaveBeenCalledTimes(1); - expect(DdLogs.errorWithError).toHaveBeenCalledWith( - 'Something bad happened', - 'Error', - 'Something bad happened', - 'doSomething() at ./path/to/file.js:67:3,nestedCall() at ./path/to/file.js:1064:9,root() at ./path/to/index.js:10:1', - { - '_dd.error.raw': error, - '_dd.error.is_crash': is_fatal, - '_dd.error.source_type': 'react-native', - '_dd.error_log.is_crash': true - } - ); }); it('M not report error in console handler W onGlobalError() {with console reporting handler}', async () => { @@ -450,19 +342,6 @@ it('M not report error in console handler W onGlobalError() {with console report expect(consoleReportingErrorHandler).toBeCalledTimes(1); expect(baseConsoleErrorCalled).toStrictEqual(false); expect(baseErrorHandlerCalled).toStrictEqual(true); - expect(DdLogs.errorWithError).toHaveBeenCalledTimes(1); - expect(DdLogs.errorWithError).toHaveBeenCalledWith( - 'Something bad happened', - 'Error', - 'Something bad happened', - 'doSomething() at ./path/to/file.js:67:3,nestedCall() at ./path/to/file.js:1064:9,root() at ./path/to/index.js:10:1', - { - '_dd.error.raw': error, - '_dd.error.is_crash': is_fatal, - '_dd.error.source_type': 'react-native', - '_dd.error_log.is_crash': true - } - ); }); it('M intercept and send a RUM event W onConsole() {Error with source file info}', async () => { @@ -493,17 +372,6 @@ it('M intercept and send a RUM event W onConsole() {Error with source file info} '' ); expect(baseConsoleErrorCalled).toStrictEqual(true); - expect(DdLogs.errorWithError).toHaveBeenCalledTimes(1); - expect(DdLogs.errorWithError).toHaveBeenCalledWith( - 'Oops I did it again! Something bad happened', - 'Error', - 'Oops I did it again! Something bad happened', - 'at ./path/to/file.js:1038:57', - { - '_dd.error.source_type': 'react-native', - '_dd.error_log.is_crash': true - } - ); }); it('M intercept and send a RUM event W onConsole() {Error with component stack}', async () => { @@ -536,17 +404,6 @@ it('M intercept and send a RUM event W onConsole() {Error with component stack}' '' ); expect(baseConsoleErrorCalled).toStrictEqual(true); - expect(DdLogs.errorWithError).toHaveBeenCalledTimes(1); - expect(DdLogs.errorWithError).toHaveBeenCalledWith( - 'Oops I did it again! Something bad happened', - 'Error', - 'Oops I did it again! Something bad happened', - 'doSomething() at ./path/to/file.js:67:3,nestedCall() at ./path/to/file.js:1064:9,root() at ./path/to/index.js:10:1', - { - '_dd.error.source_type': 'react-native', - '_dd.error_log.is_crash': true - } - ); }); it('M intercept and send a RUM event W onConsole() {message only}', async () => { @@ -571,17 +428,6 @@ it('M intercept and send a RUM event W onConsole() {message only}', async () => '' ); expect(baseConsoleErrorCalled).toStrictEqual(true); - expect(DdLogs.errorWithError).toHaveBeenCalledTimes(1); - expect(DdLogs.errorWithError).toHaveBeenCalledWith( - 'Something bad happened', - 'Error', - 'Something bad happened', - '', - { - '_dd.error.source_type': 'react-native', - '_dd.error_log.is_crash': true - } - ); }); it('M intercept and send a RUM event W onConsole() {Error with source file and name}', async () => { @@ -613,17 +459,6 @@ it('M intercept and send a RUM event W onConsole() {Error with source file and n '' ); expect(baseConsoleErrorCalled).toStrictEqual(true); - expect(DdLogs.errorWithError).toHaveBeenCalledTimes(1); - expect(DdLogs.errorWithError).toHaveBeenCalledWith( - 'Oops I did it again! Something bad happened', - 'CustomConsoleError', - 'Oops I did it again! Something bad happened', - 'at ./path/to/file.js:1038:57', - { - '_dd.error.source_type': 'react-native', - '_dd.error_log.is_crash': true - } - ); }); describe.each([ @@ -661,17 +496,6 @@ describe.each([ '' ); expect(baseConsoleErrorCalled).toStrictEqual(true); - expect(DdLogs.errorWithError).toHaveBeenCalledTimes(1); - expect(DdLogs.errorWithError).toHaveBeenCalledWith( - errorMessage, - 'Error', - errorMessage, - '', - { - '_dd.error.source_type': 'react-native', - '_dd.error_log.is_crash': true - } - ); }); }); @@ -704,19 +528,6 @@ it('M intercept and send a RUM event W on error() {called from RNErrorHandler}', '/packages/core/src/__tests__/rum/instrumentation/DdRumErrorTracking.test.tsx' ); expect(baseErrorHandlerCalled).toStrictEqual(true); - expect(DdLogs.errorWithError).toHaveBeenCalledTimes(1); - expect(DdLogs.errorWithError).toHaveBeenCalledWith( - 'Something bad happened', - 'Error', - 'Something bad happened', - expect.stringContaining('Error: Something bad happened'), - { - '_dd.error.raw': error, - '_dd.error.is_crash': is_fatal, - '_dd.error.source_type': 'react-native', - '_dd.error_log.is_crash': true - } - ); }); it('M intercept and send a RUM event W onConsole() {called from RNErrorHandler}', async () => { @@ -742,17 +553,6 @@ it('M intercept and send a RUM event W onConsole() {called from RNErrorHandler}' '' ); expect(baseConsoleErrorCalled).toStrictEqual(true); - expect(DdLogs.errorWithError).toHaveBeenCalledTimes(1); - expect(DdLogs.errorWithError).toHaveBeenCalledWith( - 'Oops I did it again!', - 'Error', - 'Oops I did it again!', - '', - { - '_dd.error.source_type': 'react-native', - '_dd.error_log.is_crash': true - } - ); }); /** diff --git a/packages/core/src/logs/__tests__/DdLogs.test.ts b/packages/core/src/logs/__tests__/DdLogs.test.ts index f99280bfe..6a530ec36 100644 --- a/packages/core/src/logs/__tests__/DdLogs.test.ts +++ b/packages/core/src/logs/__tests__/DdLogs.test.ts @@ -225,7 +225,7 @@ describe('DdLogs', () => { console.error('console-error-message'); expect(NativeModules.DdLogs.error).not.toHaveBeenCalled(); expect(InternalLog.log).toHaveBeenCalledWith( - 'error log dropped by log mapper: "console-error-message"', + 'Adding RUM Error “console-error-message”', 'debug' ); @@ -278,7 +278,7 @@ describe('DdLogs', () => { console.error('console-error-message'); expect(NativeModules.DdLogs.error).not.toHaveBeenCalled(); expect(InternalLog.log).toHaveBeenCalledWith( - 'Tracking error log "console-error-message"', + 'Adding RUM Error “console-error-message”', 'debug' ); }); diff --git a/packages/core/src/rum/instrumentation/DdRumErrorTracking.tsx b/packages/core/src/rum/instrumentation/DdRumErrorTracking.tsx index 04b01290c..5a45bdc65 100644 --- a/packages/core/src/rum/instrumentation/DdRumErrorTracking.tsx +++ b/packages/core/src/rum/instrumentation/DdRumErrorTracking.tsx @@ -8,7 +8,6 @@ import type { ErrorHandlerCallback } from 'react-native'; import { InternalLog } from '../../InternalLog'; import { SdkVerbosity } from '../../SdkVerbosity'; -import { DdLogs } from '../../logs/DdLogs'; import { getErrorMessage, getErrorStackTrace, @@ -71,8 +70,7 @@ export class DdRumErrorTracking { static onGlobalError = (error: any, isFatal?: boolean): void => { const message = getErrorMessage(error); const stacktrace = getErrorStackTrace(error); - const errorName = getErrorName(error); - this.reportError(message, ErrorSource.SOURCE, stacktrace, errorName, { + this.reportError(message, ErrorSource.SOURCE, stacktrace, { '_dd.error.is_crash': isFatal, '_dd.error.raw': error }).then(async () => { @@ -131,39 +129,22 @@ export class DdRumErrorTracking { }) .join(' '); - this.reportError(message, ErrorSource.CONSOLE, stack, errorName).then( - () => { - DdRumErrorTracking.defaultConsoleError.apply(console, params); - } - ); + this.reportError(message, ErrorSource.CONSOLE, stack).then(() => { + DdRumErrorTracking.defaultConsoleError.apply(console, params); + }); }; private static reportError = ( message: string, source: ErrorSource, stacktrace: string, - errorName: string, context: object = {} - ): Promise<[void, void]> => { - return Promise.all([ - DdRum.addError( - message, - source, - stacktrace, - getErrorContext(context) - ), - DdLogs.error( - message, - errorName, - message, - stacktrace, - { - ...context, - '_dd.error_log.is_crash': true - }, - undefined, - source - ) - ]); + ): Promise => { + return DdRum.addError( + message, + source, + stacktrace, + getErrorContext(context) + ); }; } From db5f2e2bfadb5441a7f2d1495bfc8c9dd9d191c6 Mon Sep 17 00:00:00 2001 From: "Xavier F. Gouchet" Date: Mon, 28 Apr 2025 13:55:23 +0200 Subject: [PATCH 004/410] RUM-7747 update default tracing sampling rate --- packages/core/ios/Sources/RNDdSdkConfiguration.swift | 2 +- packages/core/src/DdSdkReactNativeConfiguration.tsx | 2 +- .../src/__tests__/DdSdkReactNativeConfiguration.test.ts | 6 +++--- .../sdk/DatadogProvider/__tests__/initialization.test.tsx | 2 +- .../__tests__/FileBasedConfiguration.test.ts | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/core/ios/Sources/RNDdSdkConfiguration.swift b/packages/core/ios/Sources/RNDdSdkConfiguration.swift index a765e9759..66437c481 100644 --- a/packages/core/ios/Sources/RNDdSdkConfiguration.swift +++ b/packages/core/ios/Sources/RNDdSdkConfiguration.swift @@ -194,7 +194,7 @@ extension NSArray { internal struct DefaultConfiguration { static let nativeCrashReportEnabled = false static let sessionSamplingRate = 100.0 - static let resourceTracingSamplingRate = 20.0 + static let resourceTracingSamplingRate = 100.0 static let longTaskThresholdMs = 0.0 static let nativeLongTaskThresholdMs = 200.0 static let nativeViewTracking = false diff --git a/packages/core/src/DdSdkReactNativeConfiguration.tsx b/packages/core/src/DdSdkReactNativeConfiguration.tsx index 158d258eb..9be08fa28 100644 --- a/packages/core/src/DdSdkReactNativeConfiguration.tsx +++ b/packages/core/src/DdSdkReactNativeConfiguration.tsx @@ -109,7 +109,7 @@ export const formatFirstPartyHosts = ( export const DEFAULTS = { nativeCrashReportEnabled: false, sessionSamplingRate: 100.0, - resourceTracingSamplingRate: 20.0, + resourceTracingSamplingRate: 100.0, site: 'US1', longTaskThresholdMs: 0, nativeLongTaskThresholdMs: 200, diff --git a/packages/core/src/__tests__/DdSdkReactNativeConfiguration.test.ts b/packages/core/src/__tests__/DdSdkReactNativeConfiguration.test.ts index 6d9d081af..b1522ef7f 100644 --- a/packages/core/src/__tests__/DdSdkReactNativeConfiguration.test.ts +++ b/packages/core/src/__tests__/DdSdkReactNativeConfiguration.test.ts @@ -51,7 +51,7 @@ describe('DdSdkReactNativeConfiguration', () => { "nativeViewTracking": false, "proxyConfig": undefined, "resourceEventMapper": null, - "resourceTracingSamplingRate": 20, + "resourceTracingSamplingRate": 100, "serviceName": undefined, "sessionSamplingRate": 100, "site": "US1", @@ -79,7 +79,7 @@ describe('DdSdkReactNativeConfiguration', () => { trackInteractions: true, trackResources: true, firstPartyHosts: ['api.com'], - resourceTracingSamplingRate: 100, + resourceTracingSamplingRate: 80, logEventMapper: event => event, errorEventMapper: event => event, resourceEventMapper: event => event, @@ -161,7 +161,7 @@ describe('DdSdkReactNativeConfiguration', () => { "type": "https", }, "resourceEventMapper": [Function], - "resourceTracingSamplingRate": 100, + "resourceTracingSamplingRate": 80, "serviceName": "com.test.app", "sessionSamplingRate": 80, "site": "EU", diff --git a/packages/core/src/sdk/DatadogProvider/__tests__/initialization.test.tsx b/packages/core/src/sdk/DatadogProvider/__tests__/initialization.test.tsx index 590c34b98..c654cd24b 100644 --- a/packages/core/src/sdk/DatadogProvider/__tests__/initialization.test.tsx +++ b/packages/core/src/sdk/DatadogProvider/__tests__/initialization.test.tsx @@ -93,7 +93,7 @@ describe('DatadogProvider', () => { "nativeLongTaskThresholdMs": 200, "nativeViewTracking": false, "proxyConfig": undefined, - "resourceTracingSamplingRate": 20, + "resourceTracingSamplingRate": 100, "sampleRate": 100, "serviceName": undefined, "site": "US1", diff --git a/packages/core/src/sdk/FileBasedConfiguration/__tests__/FileBasedConfiguration.test.ts b/packages/core/src/sdk/FileBasedConfiguration/__tests__/FileBasedConfiguration.test.ts index 523cf67cc..716243e86 100644 --- a/packages/core/src/sdk/FileBasedConfiguration/__tests__/FileBasedConfiguration.test.ts +++ b/packages/core/src/sdk/FileBasedConfiguration/__tests__/FileBasedConfiguration.test.ts @@ -134,7 +134,7 @@ describe('FileBasedConfiguration', () => { "nativeViewTracking": false, "proxyConfig": undefined, "resourceEventMapper": null, - "resourceTracingSamplingRate": 20, + "resourceTracingSamplingRate": 100, "serviceName": undefined, "sessionSamplingRate": 100, "site": "US1", From 79a75a879aad517e985c00c2a87a9e232040095b Mon Sep 17 00:00:00 2001 From: "Xavier F. Gouchet" Date: Mon, 7 Apr 2025 14:55:53 +0200 Subject: [PATCH 005/410] RUM-9023 use session id to sample network traces --- .../distributedTracing/distributedTracing.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/core/src/rum/instrumentation/resourceTracking/distributedTracing/distributedTracing.tsx b/packages/core/src/rum/instrumentation/resourceTracking/distributedTracing/distributedTracing.tsx index e529f3cd1..9c4fcbff7 100644 --- a/packages/core/src/rum/instrumentation/resourceTracking/distributedTracing/distributedTracing.tsx +++ b/packages/core/src/rum/instrumentation/resourceTracking/distributedTracing/distributedTracing.tsx @@ -77,7 +77,12 @@ export const generateTracingAttributesWithSampling = ( } const traceId = TracingIdentifier.createTraceId(); - const hash = Number(traceId.id.multiply(knuthFactor).remainder(twoPow64)); + // for a UUID with value aaaaaaaa-bbbb-Mccc-Nddd-1234567890ab + // we use as the input id the last part : 0x1234567890ab + const baseId = rumSessionId + ? BigInt(rumSessionId.split('-')[4], 16) + : traceId.id; + const hash = Number(baseId.multiply(knuthFactor).remainder(twoPow64)); const threshold = (tracingSamplingRate / 100) * Number(twoPow64); const isSampled = hash <= threshold; From 75212c2dab20b7c4e21c77e041bdaa2e1f2ce91b Mon Sep 17 00:00:00 2001 From: Sergio Barrio Date: Tue, 2 Sep 2025 17:06:28 +0200 Subject: [PATCH 006/410] Improve module wrapper singleton creation --- packages/core/src/logs/DdLogs.ts | 4 +- packages/core/src/rum/DdRum.ts | 5 +- packages/core/src/trace/DdTrace.ts | 10 ++- .../utils/__tests__/singletonUtils.test.ts | 75 +++++++++++++++++++ packages/core/src/utils/singletonUtils.ts | 12 +++ 5 files changed, 101 insertions(+), 5 deletions(-) create mode 100644 packages/core/src/utils/__tests__/singletonUtils.test.ts create mode 100644 packages/core/src/utils/singletonUtils.ts diff --git a/packages/core/src/logs/DdLogs.ts b/packages/core/src/logs/DdLogs.ts index 9ac35fa73..e00e4fc0d 100644 --- a/packages/core/src/logs/DdLogs.ts +++ b/packages/core/src/logs/DdLogs.ts @@ -10,6 +10,7 @@ import type { DdNativeLogsType } from '../nativeModulesTypes'; import { DdAttributes } from '../rum/DdAttributes'; import type { ErrorSource } from '../rum/types'; import { validateContext } from '../utils/argsUtils'; +import { getGlobalInstance } from '../utils/singletonUtils'; import { generateEventMapper } from './eventMapper'; import type { @@ -21,6 +22,7 @@ import type { RawLogWithError } from './types'; +const LOGS_MODULE = 'com.datadog.reactnative.logs'; const SDK_NOT_INITIALIZED_MESSAGE = 'DD_INTERNAL_LOG_SENT_BEFORE_SDK_INIT'; const generateEmptyPromise = () => new Promise(resolve => resolve()); @@ -240,4 +242,4 @@ class DdLogsWrapper implements DdLogsType { } } -export const DdLogs = new DdLogsWrapper(); +export const DdLogs = getGlobalInstance(LOGS_MODULE, () => new DdLogsWrapper()); diff --git a/packages/core/src/rum/DdRum.ts b/packages/core/src/rum/DdRum.ts index 8943ed5b0..45cafca5b 100644 --- a/packages/core/src/rum/DdRum.ts +++ b/packages/core/src/rum/DdRum.ts @@ -13,6 +13,7 @@ import { DdSdk } from '../sdk/DdSdk'; import { GlobalState } from '../sdk/GlobalState/GlobalState'; import { validateContext } from '../utils/argsUtils'; import { getErrorContext } from '../utils/errorUtils'; +import { getGlobalInstance } from '../utils/singletonUtils'; import { DefaultTimeProvider } from '../utils/time-provider/DefaultTimeProvider'; import type { TimeProvider } from '../utils/time-provider/TimeProvider'; @@ -43,6 +44,8 @@ import type { PropagatorType } from './types'; +const RUM_MODULE = 'com.datadog.reactnative.rum'; + const generateEmptyPromise = () => new Promise(resolve => resolve()); class DdRumWrapper implements DdRumType { @@ -501,4 +504,4 @@ const isOldStopActionAPI = ( return typeof args[0] === 'object' || typeof args[0] === 'undefined'; }; -export const DdRum = new DdRumWrapper(); +export const DdRum = getGlobalInstance(RUM_MODULE, () => new DdRumWrapper()); diff --git a/packages/core/src/trace/DdTrace.ts b/packages/core/src/trace/DdTrace.ts index b106a97d8..119716914 100644 --- a/packages/core/src/trace/DdTrace.ts +++ b/packages/core/src/trace/DdTrace.ts @@ -13,8 +13,11 @@ import { } from '../sdk/DatadogProvider/Buffer/bufferNativeCall'; import type { DdTraceType } from '../types'; import { validateContext } from '../utils/argsUtils'; +import { getGlobalInstance } from '../utils/singletonUtils'; import { DefaultTimeProvider } from '../utils/time-provider/DefaultTimeProvider'; +const TRACE_MODULE = 'com.datadog.reactnative.trace'; + const timeProvider = new DefaultTimeProvider(); class DdTraceWrapper implements DdTraceType { @@ -59,6 +62,7 @@ class DdTraceWrapper implements DdTraceType { }; } -const DdTrace: DdTraceType = new DdTraceWrapper(); - -export { DdTrace }; +export const DdTrace: DdTraceType = getGlobalInstance( + TRACE_MODULE, + () => new DdTraceWrapper() +); diff --git a/packages/core/src/utils/__tests__/singletonUtils.test.ts b/packages/core/src/utils/__tests__/singletonUtils.test.ts new file mode 100644 index 000000000..f424562c6 --- /dev/null +++ b/packages/core/src/utils/__tests__/singletonUtils.test.ts @@ -0,0 +1,75 @@ +import { getGlobalInstance } from '../singletonUtils'; + +describe('singletonUtils', () => { + const createdSymbols: symbol[] = []; + const g = (globalThis as unknown) as Record; + + afterEach(() => { + for (const symbol of createdSymbols) { + delete g[symbol]; + } + + createdSymbols.length = 0; + jest.restoreAllMocks(); + }); + + it('only creates one instance for the same key', () => { + const key = 'com.datadog.reactnative.test'; + const symbol = Symbol.for(key); + createdSymbols.push(symbol); + + const objectConstructor = jest.fn(() => ({ id: 1 })); + const a = getGlobalInstance(key, objectConstructor); + const b = getGlobalInstance(key, objectConstructor); + + expect(a).toBe(b); + expect(objectConstructor).toHaveBeenCalledTimes(1); + expect(g[symbol]).toBe(a); + }); + + it('returns a pre-existing instance without creating a new one for the same key', () => { + const key = 'com.datadog.reactnative.test'; + const symbol = Symbol.for(key); + createdSymbols.push(symbol); + + const existing = { pre: true }; + g[symbol] = existing; + + const objectConstructor = jest.fn(() => ({ created: true })); + const result = getGlobalInstance(key, objectConstructor); + + expect(result).toBe(existing); + expect(objectConstructor).not.toHaveBeenCalled(); + }); + + it('creates a new instance for a different key', () => { + const keyA = 'com.datadog.reactnative.test.a'; + const keyB = 'com.datadog.reactnative.test.b'; + const symbolA = Symbol.for(keyA); + const symbolB = Symbol.for(keyB); + createdSymbols.push(symbolA, symbolB); + + const a = getGlobalInstance(keyA, () => ({ id: 'A' })); + const b = getGlobalInstance(keyB, () => ({ id: 'B' })); + + expect(a).not.toBe(b); + expect((a as any).id).toBe('A'); + expect((b as any).id).toBe('B'); + }); + + it('does not overwrite existing instance if called with a different constructor', () => { + const key = 'com.datadog.reactnative.test'; + const symbol = Symbol.for(key); + createdSymbols.push(symbol); + + const firstObjectConstructor = jest.fn(() => ({ id: 1 })); + const first = getGlobalInstance(key, firstObjectConstructor); + + const secondObjectConstructor = jest.fn(() => ({ id: 2 })); + const second = getGlobalInstance(key, secondObjectConstructor); + + expect(first).toBe(second); + expect(firstObjectConstructor).toHaveBeenCalledTimes(1); + expect(secondObjectConstructor).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/core/src/utils/singletonUtils.ts b/packages/core/src/utils/singletonUtils.ts new file mode 100644 index 000000000..9f00c2cd0 --- /dev/null +++ b/packages/core/src/utils/singletonUtils.ts @@ -0,0 +1,12 @@ +export const getGlobalInstance = ( + key: string, + objectConstructor: () => T +): T => { + const symbol = Symbol.for(key); + const g = (globalThis as unknown) as Record; + + if (!(symbol in g)) { + g[symbol] = objectConstructor(); + } + return g[symbol] as T; +}; From a237cbdb79382229181fd13f67cf67fb190338a3 Mon Sep 17 00:00:00 2001 From: Sergio Barrio Date: Fri, 5 Sep 2025 17:47:29 +0200 Subject: [PATCH 007/410] Use native sdk's core instance instead of the one inside RN SDK wrapper --- .../datadog/reactnative/DatadogSDKWrapper.kt | 90 +------------------ .../com/datadog/reactnative/DatadogWrapper.kt | 48 ---------- .../reactnative/DdLogsImplementation.kt | 3 +- .../reactnative/DdSdkImplementation.kt | 9 +- .../reactnative/DdSdkNativeInitialization.kt | 13 ++- .../reactnative/DdSdkReactNativePackage.kt | 3 +- .../com/datadog/reactnative/DdTelemetry.kt | 56 ++++++++++++ .../kotlin/com/datadog/reactnative/DdSdk.kt | 3 +- .../kotlin/com/datadog/reactnative/DdSdk.kt | 5 +- .../DdSdkNativeInitializationTest.kt | 4 + .../com/datadog/reactnative/DdSdkTest.kt | 6 +- .../DdInternalTestingImplementation.kt | 2 +- .../DdInternalTestingImplementationTest.kt | 74 ++++++++------- .../DdSessionReplayImplementation.kt | 8 +- 14 files changed, 140 insertions(+), 184 deletions(-) create mode 100644 packages/core/android/src/main/kotlin/com/datadog/reactnative/DdTelemetry.kt diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogSDKWrapper.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogSDKWrapper.kt index 7452c40ef..2b6e8800a 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogSDKWrapper.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogSDKWrapper.kt @@ -7,23 +7,14 @@ package com.datadog.reactnative import android.content.Context -import android.util.Log import com.datadog.android.Datadog -import com.datadog.android._InternalProxy import com.datadog.android.api.InternalLogger -import com.datadog.android.api.SdkCore import com.datadog.android.api.feature.FeatureSdkCore import com.datadog.android.core.InternalSdkCore import com.datadog.android.core.configuration.Configuration -import com.datadog.android.log.Logs -import com.datadog.android.log.LogsConfiguration import com.datadog.android.privacy.TrackingConsent import com.datadog.android.rum.GlobalRumMonitor -import com.datadog.android.rum.Rum -import com.datadog.android.rum.RumConfiguration import com.datadog.android.rum.RumMonitor -import com.datadog.android.trace.Trace -import com.datadog.android.trace.TraceConfiguration import com.datadog.android.webview.WebViewTracking import com.facebook.react.bridge.ReadableMap @@ -49,50 +40,18 @@ object DatadogSDKWrapperStorage { listener(ddCore) } } - - /** - * Sets instance of core SDK to be used to initialize features. - */ - fun setSdkCore(core: InternalSdkCore?) { - this.core = core - } - - /** - * Returns the core set by setSdkCore or the default core instance by default. - */ - fun getSdkCore(): SdkCore { - core?.let { - return it - } - Log.d( - DatadogSDKWrapperStorage::class.java.canonicalName, - "SdkCore was not set in DatadogSDKWrapperStorage, using default instance." - ) - return Datadog.getInstance() - } } internal class DatadogSDKWrapper : DatadogWrapper { override var bundleLogsWithRum = DefaultConfiguration.bundleLogsWithRum override var bundleLogsWithTraces = DefaultConfiguration.bundleLogsWithTraces - // We use Kotlin backing field here to initialize once the telemetry proxy - // and make sure it is only after SDK is initialized. - private var telemetryProxy: _InternalProxy._TelemetryProxy? = null - get() { - if (field == null && isInitialized()) { - field = Datadog._internalProxy()._telemetry - } - - return field - } - // We use Kotlin backing field here to initialize once the telemetry proxy // and make sure it is only after SDK is initialized. private var webViewProxy: WebViewTracking._InternalWebViewProxy? = null get() { if (field == null && isInitialized()) { - field = WebViewTracking._InternalWebViewProxy(DatadogSDKWrapperStorage.getSdkCore()) + field = WebViewTracking._InternalWebViewProxy(Datadog.getInstance()) } return field @@ -108,20 +67,7 @@ internal class DatadogSDKWrapper : DatadogWrapper { consent: TrackingConsent ) { val core = Datadog.initialize(context, configuration, consent) - DatadogSDKWrapperStorage.setSdkCore(core as InternalSdkCore) - DatadogSDKWrapperStorage.notifyOnInitializedListeners(core) - } - - override fun enableRum(configuration: RumConfiguration) { - Rum.enable(configuration, DatadogSDKWrapperStorage.getSdkCore()) - } - - override fun enableLogs(configuration: LogsConfiguration) { - Logs.enable(configuration, DatadogSDKWrapperStorage.getSdkCore()) - } - - override fun enableTrace(configuration: TraceConfiguration) { - Trace.enable(configuration, DatadogSDKWrapperStorage.getSdkCore()) + DatadogSDKWrapperStorage.notifyOnInitializedListeners(core as InternalSdkCore) } @Deprecated("Use setUserInfo instead; the user ID is now required.") @@ -160,34 +106,6 @@ internal class DatadogSDKWrapper : DatadogWrapper { Datadog.setTrackingConsent(trackingConsent) } - override fun sendTelemetryLog(message: String, attributes: ReadableMap, config: ReadableMap) { - val core = DatadogSDKWrapperStorage.getSdkCore() as FeatureSdkCore? - val logger = core?.internalLogger; - - val additionalProperties = attributes.toMap() - val telemetryConfig = config.toMap() - - logger?.log( - level = InternalLogger.Level.INFO, - target = InternalLogger.Target.TELEMETRY, - messageBuilder = { message }, - onlyOnce = (telemetryConfig["onlyOnce"] as? Boolean) ?: true, - additionalProperties = additionalProperties - ) - } - - override fun telemetryDebug(message: String) { - telemetryProxy?.debug(message) - } - - override fun telemetryError(message: String, stack: String?, kind: String?) { - telemetryProxy?.error(message, stack, kind) - } - - override fun telemetryError(message: String, throwable: Throwable?) { - telemetryProxy?.error(message, throwable) - } - override fun consumeWebviewEvent(message: String) { webViewProxy?.consumeWebviewEvent(message) } @@ -197,11 +115,11 @@ internal class DatadogSDKWrapper : DatadogWrapper { } override fun getRumMonitor(): RumMonitor { - return GlobalRumMonitor.get(DatadogSDKWrapperStorage.getSdkCore()) + return GlobalRumMonitor.get(Datadog.getInstance()) } override fun clearAllData() { - return Datadog.clearAllData(DatadogSDKWrapperStorage.getSdkCore()) + return Datadog.clearAllData(Datadog.getInstance()) } } diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogWrapper.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogWrapper.kt index 41e86f4d5..9ac591d80 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogWrapper.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogWrapper.kt @@ -64,33 +64,6 @@ interface DatadogWrapper { consent: TrackingConsent ) - /** - * Enables the RUM feature of the SDK. - * - * @param configuration the configuration for the RUM feature - */ - fun enableRum( - configuration: RumConfiguration - ) - - /** - * Enables the Logs feature of the SDK. - * - * @param configuration the configuration for the Logs feature - */ - fun enableLogs( - configuration: LogsConfiguration - ) - - /** - * Enables the Trace feature of the SDK. - * - * @param configuration the configuration for the Trace feature - */ - fun enableTrace( - configuration: TraceConfiguration - ) - /** * Sets the user information. * @@ -144,27 +117,6 @@ interface DatadogWrapper { */ fun setTrackingConsent(trackingConsent: TrackingConsent) - - /** - * Sends telemetry event with attributes. - */ - fun sendTelemetryLog(message: String, attributes: ReadableMap, config: ReadableMap) - - /** - * Sends telemetry debug event. - */ - fun telemetryDebug(message: String) - - /** - * Sends telemetry error. - */ - fun telemetryError(message: String, stack: String?, kind: String?) - - /** - * Sends telemetry error. - */ - fun telemetryError(message: String, throwable: Throwable?) - /** * Sends Webview events. */ diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdLogsImplementation.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdLogsImplementation.kt index 7223a2c72..d244e2099 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdLogsImplementation.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdLogsImplementation.kt @@ -7,6 +7,7 @@ package com.datadog.reactnative import android.util.Log as AndroidLog +import com.datadog.android.Datadog import com.datadog.android.log.Logger import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReadableMap @@ -22,7 +23,7 @@ class DdLogsImplementation( val bundleLogsWithRum = datadog.bundleLogsWithRum val bundleLogsWithTraces = datadog.bundleLogsWithTraces - logger ?: Logger.Builder(DatadogSDKWrapperStorage.getSdkCore()) + logger ?: Logger.Builder(Datadog.getInstance()) .setLogcatLogsEnabled(true) .setBundleWithRumEnabled(bundleLogsWithRum) .setBundleWithTraceEnabled(bundleLogsWithTraces) diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt index 7705483b2..c59ab54be 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt @@ -24,6 +24,7 @@ import java.util.concurrent.atomic.AtomicBoolean class DdSdkImplementation( private val reactContext: ReactApplicationContext, private val datadog: DatadogWrapper = DatadogSDKWrapper(), + private val ddTelemetry: DdTelemetry = DdTelemetry(), private val uiThreadExecutor: UiThreadExecutor = ReactUiThreadExecutor() ) { internal val appContext: Context = reactContext.applicationContext @@ -39,7 +40,7 @@ class DdSdkImplementation( fun initialize(configuration: ReadableMap, promise: Promise) { val ddSdkConfiguration = configuration.asDdSdkConfiguration() - val nativeInitialization = DdSdkNativeInitialization(appContext, datadog) + val nativeInitialization = DdSdkNativeInitialization(appContext, datadog, ddTelemetry) nativeInitialization.initialize(ddSdkConfiguration) this.frameRateProvider = createFrameRateProvider(ddSdkConfiguration) @@ -145,7 +146,7 @@ class DdSdkImplementation( * @param config Configuration object, can take 'onlyOnce: Boolean' */ fun sendTelemetryLog(message: String, attributes: ReadableMap, config: ReadableMap, promise: Promise) { - datadog.sendTelemetryLog(message, attributes, config) + ddTelemetry.sendTelemetryLog(message, attributes, config) promise.resolve(null) } @@ -154,7 +155,7 @@ class DdSdkImplementation( * @param message Debug message. */ fun telemetryDebug(message: String, promise: Promise) { - datadog.telemetryDebug(message) + ddTelemetry.telemetryDebug(message) promise.resolve(null) } @@ -165,7 +166,7 @@ class DdSdkImplementation( * @param kind Error kind. */ fun telemetryError(message: String, stack: String, kind: String, promise: Promise) { - datadog.telemetryError(message, stack, kind) + ddTelemetry.telemetryError(message, stack, kind) promise.resolve(null) } diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkNativeInitialization.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkNativeInitialization.kt index 67450add7..1dbf0b8ea 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkNativeInitialization.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkNativeInitialization.kt @@ -9,14 +9,17 @@ package com.datadog.reactnative import android.content.Context import android.content.pm.PackageManager import android.util.Log +import com.datadog.android.Datadog import com.datadog.android.DatadogSite import com.datadog.android.core.configuration.BatchProcessingLevel import com.datadog.android.core.configuration.BatchSize import com.datadog.android.core.configuration.Configuration import com.datadog.android.core.configuration.UploadFrequency import com.datadog.android.event.EventMapper +import com.datadog.android.log.Logs import com.datadog.android.log.LogsConfiguration import com.datadog.android.privacy.TrackingConsent +import com.datadog.android.rum.Rum import com.datadog.android.rum.RumConfiguration import com.datadog.android.rum._RumInternalProxy import com.datadog.android.rum.configuration.VitalsUpdateFrequency @@ -25,6 +28,7 @@ import com.datadog.android.rum.model.ActionEvent import com.datadog.android.rum.model.ResourceEvent import com.datadog.android.rum.tracking.ActivityViewTrackingStrategy import com.datadog.android.telemetry.model.TelemetryConfigurationEvent +import com.datadog.android.trace.Trace import com.datadog.android.trace.TraceConfiguration import com.google.gson.Gson import java.util.Locale @@ -37,6 +41,7 @@ import kotlin.time.Duration.Companion.seconds class DdSdkNativeInitialization internal constructor( private val appContext: Context, private val datadog: DatadogWrapper = DatadogSDKWrapper(), + private val ddTelemetry: DdTelemetry = DdTelemetry(), private val jsonFileReader: JSONFileReader = JSONFileReader() ) { internal fun initialize(ddSdkConfiguration: DdSdkConfiguration) { @@ -59,11 +64,11 @@ class DdSdkNativeInitialization internal constructor( datadog.initialize(appContext, sdkConfiguration, trackingConsent) - datadog.enableRum(rumConfiguration) + Rum.enable(rumConfiguration, Datadog.getInstance()) - datadog.enableTrace(traceConfiguration) + Logs.enable(logsConfiguration, Datadog.getInstance()) - datadog.enableLogs(logsConfiguration) + Trace.enable(traceConfiguration, Datadog.getInstance()) } private fun configureRumAndTracesForLogs(configuration: DdSdkConfiguration) { @@ -95,7 +100,7 @@ class DdSdkNativeInitialization internal constructor( try { appContext.packageManager.getPackageInfo(packageName, 0) } catch (e: PackageManager.NameNotFoundException) { - datadog.telemetryError(e.message ?: DdSdkImplementation.PACKAGE_INFO_NOT_FOUND_ERROR_MESSAGE, e) + ddTelemetry.telemetryError(e.message ?: DdSdkImplementation.PACKAGE_INFO_NOT_FOUND_ERROR_MESSAGE, e) return DdSdkImplementation.DEFAULT_APP_VERSION } diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkReactNativePackage.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkReactNativePackage.kt index bac9f49f5..3a5b022c1 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkReactNativePackage.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkReactNativePackage.kt @@ -18,9 +18,10 @@ import com.facebook.react.module.model.ReactModuleInfoProvider */ class DdSdkReactNativePackage : TurboReactPackage() { private val sdkWrapper = DatadogSDKWrapper() + private val ddTelemetry = DdTelemetry() override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? { return when (name) { - DdSdkImplementation.NAME -> DdSdk(reactContext, sdkWrapper) + DdSdkImplementation.NAME -> DdSdk(reactContext, sdkWrapper, ddTelemetry) DdRumImplementation.NAME -> DdRum(reactContext, sdkWrapper) DdTraceImplementation.NAME -> DdTrace(reactContext) DdLogsImplementation.NAME -> DdLogs(reactContext, sdkWrapper) diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdTelemetry.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdTelemetry.kt new file mode 100644 index 000000000..2d60df004 --- /dev/null +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdTelemetry.kt @@ -0,0 +1,56 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.reactnative + +import com.datadog.android.Datadog +import com.datadog.android._InternalProxy +import com.datadog.android.api.InternalLogger +import com.datadog.android.api.feature.FeatureSdkCore +import com.facebook.react.bridge.ReadableMap + +class DdTelemetry { + + // We use Kotlin backing field here to initialize once the telemetry proxy + // and make sure it is only after SDK is initialized. + private var telemetryProxy: _InternalProxy._TelemetryProxy? = null + get() { + if (field == null && Datadog.isInitialized()) { + field = Datadog._internalProxy()._telemetry + } + + return field + } + + fun sendTelemetryLog(message: String, attributes: ReadableMap, config: ReadableMap) { + val core = Datadog.getInstance() as FeatureSdkCore? + val logger = core?.internalLogger; + + val additionalProperties = attributes.toMap() + val telemetryConfig = config.toMap() + + logger?.log( + level = InternalLogger.Level.INFO, + target = InternalLogger.Target.TELEMETRY, + messageBuilder = { message }, + onlyOnce = (telemetryConfig["onlyOnce"] as? Boolean) ?: true, + additionalProperties = additionalProperties + ) + } + + fun telemetryDebug(message: String) { + telemetryProxy?.debug(message) + } + + fun telemetryError(message: String, stack: String?, kind: String?) { + telemetryProxy?.error(message, stack, kind) + } + + fun telemetryError(message: String, throwable: Throwable?) { + telemetryProxy?.error(message, throwable) + } +} + diff --git a/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdSdk.kt b/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdSdk.kt index d46e53ade..5bc470947 100644 --- a/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdSdk.kt +++ b/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdSdk.kt @@ -19,9 +19,10 @@ import com.facebook.react.modules.core.DeviceEventManagerModule class DdSdk( reactContext: ReactApplicationContext, datadogWrapper: DatadogWrapper = DatadogSDKWrapper() + ddTelemetry: DdTelemetry = DdTelemetry() ) : NativeDdSdkSpec(reactContext) { - private val implementation = DdSdkImplementation(reactContext, datadog = datadogWrapper) + private val implementation = DdSdkImplementation(reactContext, datadog = datadogWrapper, ddTelemetry) override fun getName(): String = DdSdkImplementation.NAME diff --git a/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt b/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt index b41eff1db..af8f87c29 100644 --- a/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt +++ b/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt @@ -17,10 +17,11 @@ import com.facebook.react.bridge.ReadableMap /** The entry point to initialize Datadog's features. */ class DdSdk( reactContext: ReactApplicationContext, - datadogWrapper: DatadogWrapper = DatadogSDKWrapper() + datadogWrapper: DatadogWrapper = DatadogSDKWrapper(), + ddTelemetry: DdTelemetry = DdTelemetry() ) : ReactContextBaseJavaModule(reactContext) { - private val implementation = DdSdkImplementation(reactContext, datadog = datadogWrapper) + private val implementation = DdSdkImplementation(reactContext, datadog = datadogWrapper, ddTelemetry) private var lifecycleEventListener: LifecycleEventListener? = null override fun getName(): String = DdSdkImplementation.NAME diff --git a/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkNativeInitializationTest.kt b/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkNativeInitializationTest.kt index d05d43c57..e25bfe999 100644 --- a/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkNativeInitializationTest.kt +++ b/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkNativeInitializationTest.kt @@ -48,6 +48,9 @@ internal class DdSdkNativeInitializationTest { @Mock lateinit var mockDatadog: DatadogWrapper + @Mock + lateinit var mockDdTelemetry: DdTelemetry + @Mock lateinit var mockJSONFileReader: JSONFileReader @@ -64,6 +67,7 @@ internal class DdSdkNativeInitializationTest { testedNativeInitialization = DdSdkNativeInitialization( mockContext, mockDatadog, + mockDdTelemetry, mockJSONFileReader ) } diff --git a/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt b/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt index 2d40963b6..62ad6acdb 100644 --- a/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt +++ b/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt @@ -120,6 +120,9 @@ internal class DdSdkTest { @Mock lateinit var mockDatadog: DatadogWrapper + @Mock + lateinit var mockDdTelemetry: DdTelemetry + @Forgery lateinit var fakeConfiguration: DdSdkConfiguration @@ -157,9 +160,8 @@ internal class DdSdkTest { answer.getArgument(0).run() true } - testedBridgeSdk = DdSdkImplementation(mockReactContext, mockDatadog, TestUiThreadExecutor()) + testedBridgeSdk = DdSdkImplementation(mockReactContext, mockDatadog, mockDdTelemetry, TestUiThreadExecutor()) - DatadogSDKWrapperStorage.setSdkCore(null) DatadogSDKWrapperStorage.onInitializedListeners.clear() } diff --git a/packages/internal-testing-tools/android/src/main/kotlin/com/datadog/reactnative/internaltesting/DdInternalTestingImplementation.kt b/packages/internal-testing-tools/android/src/main/kotlin/com/datadog/reactnative/internaltesting/DdInternalTestingImplementation.kt index ca143f18a..18c6c7e91 100644 --- a/packages/internal-testing-tools/android/src/main/kotlin/com/datadog/reactnative/internaltesting/DdInternalTestingImplementation.kt +++ b/packages/internal-testing-tools/android/src/main/kotlin/com/datadog/reactnative/internaltesting/DdInternalTestingImplementation.kt @@ -7,6 +7,7 @@ package com.datadog.reactnative.internaltesting import com.datadog.android.api.InternalLogger +import com.datadog.android.Datadog import com.datadog.android.api.context.DatadogContext import com.datadog.android.api.context.NetworkInfo import com.datadog.android.api.context.TimeInfo @@ -53,7 +54,6 @@ class DdInternalTestingImplementation { fun enable(promise: Promise) { DatadogSDKWrapperStorage.addOnInitializedListener { ddCore -> this.wrappedCore = StubSDKCore(ddCore) - DatadogSDKWrapperStorage.setSdkCore(this.wrappedCore) } promise.resolve(null) } diff --git a/packages/internal-testing-tools/android/src/test/kotlin/com/datadog/reactnative/internaltesting/DdInternalTestingImplementationTest.kt b/packages/internal-testing-tools/android/src/test/kotlin/com/datadog/reactnative/internaltesting/DdInternalTestingImplementationTest.kt index 6c278026a..c542ed6bc 100644 --- a/packages/internal-testing-tools/android/src/test/kotlin/com/datadog/reactnative/internaltesting/DdInternalTestingImplementationTest.kt +++ b/packages/internal-testing-tools/android/src/test/kotlin/com/datadog/reactnative/internaltesting/DdInternalTestingImplementationTest.kt @@ -7,6 +7,8 @@ package com.datadog.reactnative.internaltesting import android.content.Context +import com.datadog.android.Datadog +import com.datadog.android.api.SdkCore import com.datadog.android.api.context.DatadogContext import com.datadog.android.api.feature.Feature import com.datadog.android.api.feature.FeatureScope @@ -24,6 +26,7 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith import org.junit.jupiter.api.extension.Extensions import org.mockito.Mock +import org.mockito.Mockito import org.mockito.Mockito.mock import org.mockito.junit.jupiter.MockitoExtension import org.mockito.junit.jupiter.MockitoSettings @@ -57,37 +60,47 @@ internal class DdInternalTestingImplementationTest { @Test fun `M return captured events W enable()`() { - // Given - val mockFeature = MockFeature("mockFeature") - val mockFeatureScope = MockFeatureScope(mockFeature) - whenever(mockCore.getFeature(mockFeature.name)).doReturn( - mockFeatureScope - ) - whenever(mockCore.getDatadogContext()).doReturn( - mockContext - ) - - // When - testedInternalTesting.enable(mockPromise) - // Simulating DdSdkImplementation initialization - DatadogSDKWrapperStorage.setSdkCore(mockCore) - DatadogSDKWrapperStorage.notifyOnInitializedListeners(mockCore) - - val wrappedCore = DatadogSDKWrapperStorage.getSdkCore() as StubSDKCore - wrappedCore.registerFeature(mockFeature) - requireNotNull(wrappedCore.getFeature(mockFeature.name)) - .withWriteContext { _, eventBatchWriter -> - eventBatchWriter.write( - RawBatchEvent(data = "mock event for test".toByteArray()), - batchMetadata = null, - eventType = EventType.DEFAULT + Mockito.mockStatic(Datadog::class.java).use { datadogStatic -> + // Given + datadogStatic.`when` { + Datadog.getInstance() + }.thenReturn(mockCore) + + val mockFeature = MockFeature("mockFeature") + val mockFeatureScope = MockFeatureScope(mockFeature) + whenever(mockCore.getFeature(mockFeature.name)).doReturn( + mockFeatureScope + ) + whenever(mockCore.getDatadogContext()).doReturn( + mockContext + ) + + // When + testedInternalTesting.enable(mockPromise) + // Simulating DdSdkImplementation initialization + DatadogSDKWrapperStorage.notifyOnInitializedListeners(mockCore) + + val wrappedCore = Datadog.getInstance() as StubSDKCore + wrappedCore.registerFeature(mockFeature) + requireNotNull(wrappedCore.getFeature(mockFeature.name)) + .withWriteContext { _, eventBatchWriter -> + eventBatchWriter.write( + RawBatchEvent(data = "mock event for test".toByteArray()), + batchMetadata = null, + eventType = EventType.DEFAULT + ) + } + + // Then + assertThat( + wrappedCore.featureScopes[mockFeature.name] + ?.eventsWritten() + ?.first() + ) + .isEqualTo( + "mock event for test" ) - } - - // Then - assertThat(wrappedCore.featureScopes[mockFeature.name]?.eventsWritten()?.first()).isEqualTo( - "mock event for test" - ) + } } } @@ -96,6 +109,7 @@ internal class MockFeatureScope(private val feature: Feature) : FeatureScope { override fun sendEvent(event: Any) {} + @Suppress("UNCHECKED_CAST") override fun unwrap(): T { return feature as T } diff --git a/packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/DdSessionReplayImplementation.kt b/packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/DdSessionReplayImplementation.kt index acf13120a..0650728dc 100644 --- a/packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/DdSessionReplayImplementation.kt +++ b/packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/DdSessionReplayImplementation.kt @@ -7,10 +7,10 @@ package com.datadog.reactnative.sessionreplay import android.annotation.SuppressLint +import com.datadog.android.Datadog import com.datadog.android.api.feature.FeatureSdkCore import com.datadog.android.sessionreplay.SessionReplayConfiguration import com.datadog.android.sessionreplay._SessionReplayInternalProxy -import com.datadog.reactnative.DatadogSDKWrapperStorage import com.datadog.reactnative.sessionreplay.utils.text.TextViewUtils import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReactContext @@ -40,7 +40,7 @@ class DdSessionReplayImplementation( startRecordingImmediately: Boolean, promise: Promise ) { - val sdkCore = DatadogSDKWrapperStorage.getSdkCore() as FeatureSdkCore + val sdkCore = Datadog.getInstance() as FeatureSdkCore val logger = sdkCore.internalLogger val textViewUtils = TextViewUtils.create(reactContext, logger) val internalCallback = ReactNativeInternalCallback(reactContext) @@ -68,7 +68,7 @@ class DdSessionReplayImplementation( */ fun startRecording(promise: Promise) { sessionReplayProvider().startRecording( - DatadogSDKWrapperStorage.getSdkCore() as FeatureSdkCore + Datadog.getInstance() as FeatureSdkCore ) promise.resolve(null) } @@ -78,7 +78,7 @@ class DdSessionReplayImplementation( */ fun stopRecording(promise: Promise) { sessionReplayProvider().stopRecording( - DatadogSDKWrapperStorage.getSdkCore() as FeatureSdkCore + Datadog.getInstance() as FeatureSdkCore ) promise.resolve(null) } From 34560c2812ddd48307c2857642b6919fdf57adca Mon Sep 17 00:00:00 2001 From: Marco Saia Date: Mon, 22 Sep 2025 16:20:27 +0200 Subject: [PATCH 008/410] Fixed internal testing tools and unit tests --- .../com/datadog/reactnative/DatadogWrapper.kt | 8 +- .../reactnative/DdSdkNativeInitialization.kt | 2 - .../com/datadog/reactnative/DdTelemetry.kt | 40 + .../kotlin/com/datadog/reactnative/DdSdk.kt | 6 +- .../com/datadog/reactnative/DdSdkTest.kt | 2628 ++++++++++------- .../DdInternalTestingImplementation.kt | 6 + .../DdInternalTestingImplementationTest.kt | 4 +- 7 files changed, 1695 insertions(+), 999 deletions(-) diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogWrapper.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogWrapper.kt index 9ac591d80..19b25e587 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogWrapper.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogWrapper.kt @@ -18,7 +18,7 @@ import com.facebook.react.bridge.ReadableMap import java.lang.IllegalArgumentException /** - * Wrapper around [Datadog]. + * Wrapper around [com.datadog.android.Datadog]. */ @Suppress("ComplexInterface", "TooManyFunctions") interface DatadogWrapper { @@ -49,10 +49,8 @@ interface DatadogWrapper { /** * Initializes the Datadog SDK. * @param context your application context - * @param credentials your organization credentials * @param configuration the configuration for the SDK library - * @param trackingConsent as the initial state of the tracking consent flag. - * @see [Credentials] + * @param consent as the initial state of the tracking consent flag. * @see [Configuration] * @see [TrackingConsent] * @throws IllegalArgumentException if the env name is using illegal characters and your @@ -99,7 +97,7 @@ interface DatadogWrapper { /** * Sets the user information. - * @param extraUserInfo: The additional information. (To set the id, name or email please user setUserInfo). + * @param extraInfo: The additional information. (To set the id, name or email please user setUserInfo). */ fun addUserExtraInfo( extraInfo: Map diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkNativeInitialization.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkNativeInitialization.kt index 1dbf0b8ea..79114b184 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkNativeInitialization.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkNativeInitialization.kt @@ -65,9 +65,7 @@ class DdSdkNativeInitialization internal constructor( datadog.initialize(appContext, sdkConfiguration, trackingConsent) Rum.enable(rumConfiguration, Datadog.getInstance()) - Logs.enable(logsConfiguration, Datadog.getInstance()) - Trace.enable(traceConfiguration, Datadog.getInstance()) } diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdTelemetry.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdTelemetry.kt index 2d60df004..24354ce78 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdTelemetry.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdTelemetry.kt @@ -12,6 +12,13 @@ import com.datadog.android.api.InternalLogger import com.datadog.android.api.feature.FeatureSdkCore import com.facebook.react.bridge.ReadableMap +/** + * **[INTERNAL USAGE]** + * + * Utility class used by React Native modules to forward telemetry events to the Datadog SDK. + * + * This class is **public only for Datadog internal package visibility** and should not be used. + */ class DdTelemetry { // We use Kotlin backing field here to initialize once the telemetry proxy @@ -25,6 +32,15 @@ class DdTelemetry { return field } + /** + * **[INTERNAL USAGE]** + * + * Sends a telemetry log message with additional attributes and configuration options. + * + * @param message the message to log + * @param attributes additional key–value properties to include in the log + * @param config configuration options for the telemetry log (e.g. `onlyOnce` flag) + */ fun sendTelemetryLog(message: String, attributes: ReadableMap, config: ReadableMap) { val core = Datadog.getInstance() as FeatureSdkCore? val logger = core?.internalLogger; @@ -41,14 +57,38 @@ class DdTelemetry { ) } + /** + * **[INTERNAL USAGE]** + * + * Sends a debug-level telemetry message. + * + * @param message the debug message + */ fun telemetryDebug(message: String) { telemetryProxy?.debug(message) } + /** + * **[INTERNAL USAGE]** + * + * Sends an error-level telemetry message with optional details. + * + * @param message the error message + * @param stack an optional stack trace string + * @param kind an optional error kind or category + */ fun telemetryError(message: String, stack: String?, kind: String?) { telemetryProxy?.error(message, stack, kind) } + /** + * **[INTERNAL USAGE]** + * + * Sends an error-level telemetry message with an attached [Throwable]. + * + * @param message the error message + * @param throwable the throwable associated with the error + */ fun telemetryError(message: String, throwable: Throwable?) { telemetryProxy?.error(message, throwable) } diff --git a/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt b/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt index af8f87c29..17acd6d20 100644 --- a/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt +++ b/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt @@ -21,7 +21,11 @@ class DdSdk( ddTelemetry: DdTelemetry = DdTelemetry() ) : ReactContextBaseJavaModule(reactContext) { - private val implementation = DdSdkImplementation(reactContext, datadog = datadogWrapper, ddTelemetry) + private val implementation = DdSdkImplementation( + reactContext, + datadog = datadogWrapper, + ddTelemetry + ) private var lifecycleEventListener: LifecycleEventListener? = null override fun getName(): String = DdSdkImplementation.NAME diff --git a/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt b/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt index 62ad6acdb..f71507ade 100644 --- a/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt +++ b/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt @@ -16,8 +16,10 @@ import com.datadog.android.core.configuration.BatchSize import com.datadog.android.core.configuration.Configuration import com.datadog.android.core.configuration.UploadFrequency import com.datadog.android.event.EventMapper +import com.datadog.android.log.Logs import com.datadog.android.log.LogsConfiguration import com.datadog.android.privacy.TrackingConsent +import com.datadog.android.rum.Rum import com.datadog.android.rum.RumConfiguration import com.datadog.android.rum.RumPerformanceMetric import com.datadog.android.rum._RumInternalProxy @@ -27,6 +29,7 @@ import com.datadog.android.rum.model.ActionEvent import com.datadog.android.rum.model.ResourceEvent import com.datadog.android.rum.tracking.ActivityViewTrackingStrategy import com.datadog.android.telemetry.model.TelemetryConfigurationEvent +import com.datadog.android.trace.Trace import com.datadog.android.trace.TraceConfiguration import com.datadog.android.trace.TracingHeaderType import com.datadog.tools.unit.GenericAssert.Companion.assertThat @@ -160,7 +163,12 @@ internal class DdSdkTest { answer.getArgument(0).run() true } - testedBridgeSdk = DdSdkImplementation(mockReactContext, mockDatadog, mockDdTelemetry, TestUiThreadExecutor()) + testedBridgeSdk = DdSdkImplementation( + mockReactContext, + mockDatadog, + mockDdTelemetry, + TestUiThreadExecutor() + ) DatadogSDKWrapperStorage.onInitializedListeners.clear() } @@ -181,35 +189,49 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(sdkConfigCaptor.firstValue) - .hasField("coreConfig") { - it.hasFieldEqualTo("needsClearTextHttp", false) - it.hasFieldEqualTo("firstPartyHostsWithHeaderTypes", emptyMap()) + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } - .hasFieldEqualTo("clientToken", fakeConfiguration.clientToken) - .hasFieldEqualTo("env", fakeConfiguration.env) - .hasFieldEqualTo("variant", "") - .hasFieldEqualTo("crashReportsEnabled", true) - .hasFieldEqualTo( - "additionalConfig", - fakeConfiguration.additionalConfig?.filterValues { it != null }.orEmpty() - ) - assertThat(rumConfigCaptor.firstValue) - .hasFieldEqualTo("applicationId", fakeConfiguration.applicationId) + assertThat(sdkConfigCaptor.firstValue) + .hasField("coreConfig") { + it.hasFieldEqualTo("needsClearTextHttp", false) + it.hasFieldEqualTo("firstPartyHostsWithHeaderTypes", emptyMap()) + } + .hasFieldEqualTo("clientToken", fakeConfiguration.clientToken) + .hasFieldEqualTo("env", fakeConfiguration.env) + .hasFieldEqualTo("variant", "") + .hasFieldEqualTo("crashReportsEnabled", true) + .hasFieldEqualTo( + "additionalConfig", + fakeConfiguration.additionalConfig?.filterValues { it != null }.orEmpty() + ) + assertThat(rumConfigCaptor.firstValue) + .hasFieldEqualTo("applicationId", fakeConfiguration.applicationId) + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } @Test @@ -221,75 +243,104 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(sdkConfigCaptor.firstValue) - .hasField("coreConfig") { - it.hasFieldEqualTo("needsClearTextHttp", false) - it.hasFieldEqualTo("firstPartyHostsWithHeaderTypes", emptyMap()) + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } - .hasFieldEqualTo("clientToken", fakeConfiguration.clientToken) - .hasFieldEqualTo("env", fakeConfiguration.env) - .hasFieldEqualTo("variant", "") - .hasFieldEqualTo("crashReportsEnabled", false) - .hasFieldEqualTo( - "additionalConfig", - fakeConfiguration.additionalConfig?.filterValues { it != null }.orEmpty() - ) - assertThat(rumConfigCaptor.firstValue) - .hasFieldEqualTo("applicationId", fakeConfiguration.applicationId) + assertThat(sdkConfigCaptor.firstValue) + .hasField("coreConfig") { + it.hasFieldEqualTo("needsClearTextHttp", false) + it.hasFieldEqualTo("firstPartyHostsWithHeaderTypes", emptyMap()) + } + .hasFieldEqualTo("clientToken", fakeConfiguration.clientToken) + .hasFieldEqualTo("env", fakeConfiguration.env) + .hasFieldEqualTo("variant", "") + .hasFieldEqualTo("crashReportsEnabled", false) + .hasFieldEqualTo( + "additionalConfig", + fakeConfiguration.additionalConfig?.filterValues { it != null }.orEmpty() + ) + assertThat(rumConfigCaptor.firstValue) + .hasFieldEqualTo("applicationId", fakeConfiguration.applicationId) + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } @Test fun `𝕄 initialize native SDK 𝕎 initialize() {nativeCrashReportEnabled=null}`() { // Given fakeConfiguration = fakeConfiguration.copy(nativeCrashReportEnabled = false, site = null) + val sdkConfigCaptor = argumentCaptor() val rumConfigCaptor = argumentCaptor() val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(sdkConfigCaptor.firstValue) - .hasField("coreConfig") { - it.hasFieldEqualTo("needsClearTextHttp", false) - it.hasFieldEqualTo("firstPartyHostsWithHeaderTypes", emptyMap()) + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } - .hasFieldEqualTo("clientToken", fakeConfiguration.clientToken) - .hasFieldEqualTo("env", fakeConfiguration.env) - .hasFieldEqualTo("variant", "") - .hasFieldEqualTo("crashReportsEnabled", false) - .hasFieldEqualTo( - "additionalConfig", - fakeConfiguration.additionalConfig?.filterValues { it != null }.orEmpty() - ) - assertThat(rumConfigCaptor.firstValue) - .hasFieldEqualTo("applicationId", fakeConfiguration.applicationId) + assertThat(sdkConfigCaptor.firstValue) + .hasField("coreConfig") { + it.hasFieldEqualTo("needsClearTextHttp", false) + it.hasFieldEqualTo("firstPartyHostsWithHeaderTypes", emptyMap()) + } + .hasFieldEqualTo("clientToken", fakeConfiguration.clientToken) + .hasFieldEqualTo("env", fakeConfiguration.env) + .hasFieldEqualTo("variant", "") + .hasFieldEqualTo("crashReportsEnabled", false) + .hasFieldEqualTo( + "additionalConfig", + fakeConfiguration.additionalConfig?.filterValues { it != null }.orEmpty() + ) + assertThat(rumConfigCaptor.firstValue) + .hasFieldEqualTo("applicationId", fakeConfiguration.applicationId) + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } // endregion @@ -305,37 +356,50 @@ internal class DdSdkTest { val traceConfigCaptor = argumentCaptor() val expectedRumSampleRate = fakeConfiguration.sampleRate?.toFloat() ?: 100f - // When - testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(sdkConfigCaptor.firstValue) - .hasField("coreConfig") { - it.hasFieldEqualTo("needsClearTextHttp", false) - it.hasFieldEqualTo("firstPartyHostsWithHeaderTypes", emptyMap()) - } - .hasFieldEqualTo("clientToken", fakeConfiguration.clientToken) - .hasFieldEqualTo("env", fakeConfiguration.env) - .hasFieldEqualTo("variant", "") - .hasFieldEqualTo( - "additionalConfig", - fakeConfiguration.additionalConfig?.filterValues { it != null }.orEmpty() - ) - assertThat(rumConfigCaptor.firstValue) - .hasFieldEqualTo("applicationId", fakeConfiguration.applicationId) - .hasField("featureConfiguration") { - it.hasFieldEqualTo("sampleRate", expectedRumSampleRate) + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + // When + testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } + assertThat(sdkConfigCaptor.firstValue) + .hasField("coreConfig") { + it.hasFieldEqualTo("needsClearTextHttp", false) + it.hasFieldEqualTo("firstPartyHostsWithHeaderTypes", emptyMap()) + } + .hasFieldEqualTo("clientToken", fakeConfiguration.clientToken) + .hasFieldEqualTo("env", fakeConfiguration.env) + .hasFieldEqualTo("variant", "") + .hasFieldEqualTo( + "additionalConfig", + fakeConfiguration.additionalConfig?.filterValues { it != null }.orEmpty() + ) + assertThat(rumConfigCaptor.firstValue) + .hasFieldEqualTo("applicationId", fakeConfiguration.applicationId) + .hasField("featureConfiguration") { + it.hasFieldEqualTo("sampleRate", expectedRumSampleRate) + } + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } // endregion @@ -351,37 +415,50 @@ internal class DdSdkTest { val traceConfigCaptor = argumentCaptor() val expectedTelemetrySampleRate = fakeConfiguration.telemetrySampleRate?.toFloat() ?: 20f - // When - testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(sdkConfigCaptor.firstValue) - .hasField("coreConfig") { - it.hasFieldEqualTo("needsClearTextHttp", false) - it.hasFieldEqualTo("firstPartyHostsWithHeaderTypes", emptyMap()) - } - .hasFieldEqualTo("clientToken", fakeConfiguration.clientToken) - .hasFieldEqualTo("env", fakeConfiguration.env) - .hasFieldEqualTo("variant", "") - .hasFieldEqualTo( - "additionalConfig", - fakeConfiguration.additionalConfig?.filterValues { it != null }.orEmpty() - ) - assertThat(rumConfigCaptor.firstValue) - .hasFieldEqualTo("applicationId", fakeConfiguration.applicationId) - .hasField("featureConfiguration") { - it.hasFieldEqualTo("telemetrySampleRate", expectedTelemetrySampleRate) + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + // When + testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } + assertThat(sdkConfigCaptor.firstValue) + .hasField("coreConfig") { + it.hasFieldEqualTo("needsClearTextHttp", false) + it.hasFieldEqualTo("firstPartyHostsWithHeaderTypes", emptyMap()) + } + .hasFieldEqualTo("clientToken", fakeConfiguration.clientToken) + .hasFieldEqualTo("env", fakeConfiguration.env) + .hasFieldEqualTo("variant", "") + .hasFieldEqualTo( + "additionalConfig", + fakeConfiguration.additionalConfig?.filterValues { it != null }.orEmpty() + ) + assertThat(rumConfigCaptor.firstValue) + .hasFieldEqualTo("applicationId", fakeConfiguration.applicationId) + .hasField("featureConfiguration") { + it.hasFieldEqualTo("telemetrySampleRate", expectedTelemetrySampleRate) + } + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } // endregion @@ -397,31 +474,44 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(sdkConfigCaptor.firstValue) - .hasField("coreConfig") { - it.hasFieldEqualTo("needsClearTextHttp", false) - it.hasFieldEqualTo("firstPartyHostsWithHeaderTypes", emptyMap()) + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } - .hasFieldEqualTo("clientToken", fakeConfiguration.clientToken) - .hasFieldEqualTo("env", fakeConfiguration.env) - .hasFieldEqualTo("variant", "") - .hasFieldEqualTo("additionalConfig", emptyMap()) - assertThat(rumConfigCaptor.firstValue) - .hasFieldEqualTo("applicationId", fakeConfiguration.applicationId) + assertThat(sdkConfigCaptor.firstValue) + .hasField("coreConfig") { + it.hasFieldEqualTo("needsClearTextHttp", false) + it.hasFieldEqualTo("firstPartyHostsWithHeaderTypes", emptyMap()) + } + .hasFieldEqualTo("clientToken", fakeConfiguration.clientToken) + .hasFieldEqualTo("env", fakeConfiguration.env) + .hasFieldEqualTo("variant", "") + .hasFieldEqualTo("additionalConfig", emptyMap()) + assertThat(rumConfigCaptor.firstValue) + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } @Test @@ -432,34 +522,47 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(sdkConfigCaptor.firstValue) - .hasField("coreConfig") { - it.hasFieldEqualTo("needsClearTextHttp", false) - it.hasFieldEqualTo("firstPartyHostsWithHeaderTypes", emptyMap()) + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + // When + testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } - .hasFieldEqualTo("clientToken", fakeConfiguration.clientToken) - .hasFieldEqualTo("env", fakeConfiguration.env) - .hasFieldEqualTo("variant", "") - .hasFieldEqualTo( - "additionalConfig", - fakeConfiguration.additionalConfig?.filterValues { it != null }.orEmpty() - ) - assertThat(rumConfigCaptor.firstValue) - .hasFieldEqualTo("applicationId", fakeConfiguration.applicationId) + assertThat(sdkConfigCaptor.firstValue) + .hasField("coreConfig") { + it.hasFieldEqualTo("needsClearTextHttp", false) + it.hasFieldEqualTo("firstPartyHostsWithHeaderTypes", emptyMap()) + } + .hasFieldEqualTo("clientToken", fakeConfiguration.clientToken) + .hasFieldEqualTo("env", fakeConfiguration.env) + .hasFieldEqualTo("variant", "") + .hasFieldEqualTo( + "additionalConfig", + fakeConfiguration.additionalConfig?.filterValues { it != null }.orEmpty() + ) + assertThat(rumConfigCaptor.firstValue) + .hasFieldEqualTo("applicationId", fakeConfiguration.applicationId) + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } // endregion @@ -477,35 +580,49 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(sdkConfigCaptor.firstValue) - .hasField("coreConfig") { - it.hasFieldEqualTo("needsClearTextHttp", false) - it.hasFieldEqualTo("firstPartyHostsWithHeaderTypes", emptyMap()) - it.hasFieldEqualTo("site", DatadogSite.US1) + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } - .hasFieldEqualTo("clientToken", fakeConfiguration.clientToken) - .hasFieldEqualTo("env", fakeConfiguration.env) - .hasFieldEqualTo("variant", "") - .hasFieldEqualTo( - "additionalConfig", - fakeConfiguration.additionalConfig?.filterValues { it != null }.orEmpty() - ) - assertThat(rumConfigCaptor.firstValue) - .hasFieldEqualTo("applicationId", fakeConfiguration.applicationId) + assertThat(sdkConfigCaptor.firstValue) + .hasField("coreConfig") { + it.hasFieldEqualTo("needsClearTextHttp", false) + it.hasFieldEqualTo("firstPartyHostsWithHeaderTypes", emptyMap()) + it.hasFieldEqualTo("site", DatadogSite.US1) + } + .hasFieldEqualTo("clientToken", fakeConfiguration.clientToken) + .hasFieldEqualTo("env", fakeConfiguration.env) + .hasFieldEqualTo("variant", "") + .hasFieldEqualTo( + "additionalConfig", + fakeConfiguration.additionalConfig?.filterValues { it != null }.orEmpty() + ) + assertThat(rumConfigCaptor.firstValue) + .hasFieldEqualTo("applicationId", fakeConfiguration.applicationId) + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } @Test @@ -520,35 +637,49 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(sdkConfigCaptor.firstValue) - .hasField("coreConfig") { - it.hasFieldEqualTo("needsClearTextHttp", false) - it.hasFieldEqualTo("firstPartyHostsWithHeaderTypes", emptyMap()) - it.hasFieldEqualTo("site", DatadogSite.US1) + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } - .hasFieldEqualTo("clientToken", fakeConfiguration.clientToken) - .hasFieldEqualTo("env", fakeConfiguration.env) - .hasFieldEqualTo("variant", "") - .hasFieldEqualTo( - "additionalConfig", - fakeConfiguration.additionalConfig?.filterValues { it != null }.orEmpty() - ) - assertThat(rumConfigCaptor.firstValue) - .hasFieldEqualTo("applicationId", fakeConfiguration.applicationId) + assertThat(sdkConfigCaptor.firstValue) + .hasField("coreConfig") { + it.hasFieldEqualTo("needsClearTextHttp", false) + it.hasFieldEqualTo("firstPartyHostsWithHeaderTypes", emptyMap()) + it.hasFieldEqualTo("site", DatadogSite.US1) + } + .hasFieldEqualTo("clientToken", fakeConfiguration.clientToken) + .hasFieldEqualTo("env", fakeConfiguration.env) + .hasFieldEqualTo("variant", "") + .hasFieldEqualTo( + "additionalConfig", + fakeConfiguration.additionalConfig?.filterValues { it != null }.orEmpty() + ) + assertThat(rumConfigCaptor.firstValue) + .hasFieldEqualTo("applicationId", fakeConfiguration.applicationId) + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } @Test @@ -563,35 +694,49 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(sdkConfigCaptor.firstValue) - .hasField("coreConfig") { - it.hasFieldEqualTo("needsClearTextHttp", false) - it.hasFieldEqualTo("firstPartyHostsWithHeaderTypes", emptyMap()) - it.hasFieldEqualTo("site", DatadogSite.US3) + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } - .hasFieldEqualTo("clientToken", fakeConfiguration.clientToken) - .hasFieldEqualTo("env", fakeConfiguration.env) - .hasFieldEqualTo("variant", "") - .hasFieldEqualTo( - "additionalConfig", - fakeConfiguration.additionalConfig?.filterValues { it != null }.orEmpty() - ) - assertThat(rumConfigCaptor.firstValue) - .hasFieldEqualTo("applicationId", fakeConfiguration.applicationId) + assertThat(sdkConfigCaptor.firstValue) + .hasField("coreConfig") { + it.hasFieldEqualTo("needsClearTextHttp", false) + it.hasFieldEqualTo("firstPartyHostsWithHeaderTypes", emptyMap()) + it.hasFieldEqualTo("site", DatadogSite.US3) + } + .hasFieldEqualTo("clientToken", fakeConfiguration.clientToken) + .hasFieldEqualTo("env", fakeConfiguration.env) + .hasFieldEqualTo("variant", "") + .hasFieldEqualTo( + "additionalConfig", + fakeConfiguration.additionalConfig?.filterValues { it != null }.orEmpty() + ) + assertThat(rumConfigCaptor.firstValue) + .hasFieldEqualTo("applicationId", fakeConfiguration.applicationId) + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } @Test @@ -606,35 +751,49 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(sdkConfigCaptor.firstValue) - .hasField("coreConfig") { - it.hasFieldEqualTo("needsClearTextHttp", false) - it.hasFieldEqualTo("firstPartyHostsWithHeaderTypes", emptyMap()) - it.hasFieldEqualTo("site", DatadogSite.US5) + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } - .hasFieldEqualTo("clientToken", fakeConfiguration.clientToken) - .hasFieldEqualTo("env", fakeConfiguration.env) - .hasFieldEqualTo("variant", "") - .hasFieldEqualTo( - "additionalConfig", - fakeConfiguration.additionalConfig?.filterValues { it != null }.orEmpty() - ) - assertThat(rumConfigCaptor.firstValue) - .hasFieldEqualTo("applicationId", fakeConfiguration.applicationId) + assertThat(sdkConfigCaptor.firstValue) + .hasField("coreConfig") { + it.hasFieldEqualTo("needsClearTextHttp", false) + it.hasFieldEqualTo("firstPartyHostsWithHeaderTypes", emptyMap()) + it.hasFieldEqualTo("site", DatadogSite.US5) + } + .hasFieldEqualTo("clientToken", fakeConfiguration.clientToken) + .hasFieldEqualTo("env", fakeConfiguration.env) + .hasFieldEqualTo("variant", "") + .hasFieldEqualTo( + "additionalConfig", + fakeConfiguration.additionalConfig?.filterValues { it != null }.orEmpty() + ) + assertThat(rumConfigCaptor.firstValue) + .hasFieldEqualTo("applicationId", fakeConfiguration.applicationId) + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } @Test @@ -649,35 +808,49 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(sdkConfigCaptor.firstValue) - .hasField("coreConfig") { - it.hasFieldEqualTo("needsClearTextHttp", false) - it.hasFieldEqualTo("firstPartyHostsWithHeaderTypes", emptyMap()) - it.hasFieldEqualTo("site", DatadogSite.US1_FED) + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } - .hasFieldEqualTo("clientToken", fakeConfiguration.clientToken) - .hasFieldEqualTo("env", fakeConfiguration.env) - .hasFieldEqualTo("variant", "") - .hasFieldEqualTo( - "additionalConfig", - fakeConfiguration.additionalConfig?.filterValues { it != null }.orEmpty() - ) - assertThat(rumConfigCaptor.firstValue) - .hasFieldEqualTo("applicationId", fakeConfiguration.applicationId) + assertThat(sdkConfigCaptor.firstValue) + .hasField("coreConfig") { + it.hasFieldEqualTo("needsClearTextHttp", false) + it.hasFieldEqualTo("firstPartyHostsWithHeaderTypes", emptyMap()) + it.hasFieldEqualTo("site", DatadogSite.US1_FED) + } + .hasFieldEqualTo("clientToken", fakeConfiguration.clientToken) + .hasFieldEqualTo("env", fakeConfiguration.env) + .hasFieldEqualTo("variant", "") + .hasFieldEqualTo( + "additionalConfig", + fakeConfiguration.additionalConfig?.filterValues { it != null }.orEmpty() + ) + assertThat(rumConfigCaptor.firstValue) + .hasFieldEqualTo("applicationId", fakeConfiguration.applicationId) + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } @Test @@ -692,35 +865,49 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(sdkConfigCaptor.firstValue) - .hasField("coreConfig") { - it.hasFieldEqualTo("needsClearTextHttp", false) - it.hasFieldEqualTo("firstPartyHostsWithHeaderTypes", emptyMap()) - it.hasFieldEqualTo("site", DatadogSite.EU1) + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } - .hasFieldEqualTo("clientToken", fakeConfiguration.clientToken) - .hasFieldEqualTo("env", fakeConfiguration.env) - .hasFieldEqualTo("variant", "") - .hasFieldEqualTo( - "additionalConfig", - fakeConfiguration.additionalConfig?.filterValues { it != null }.orEmpty() - ) - assertThat(rumConfigCaptor.firstValue) - .hasFieldEqualTo("applicationId", fakeConfiguration.applicationId) + assertThat(sdkConfigCaptor.firstValue) + .hasField("coreConfig") { + it.hasFieldEqualTo("needsClearTextHttp", false) + it.hasFieldEqualTo("firstPartyHostsWithHeaderTypes", emptyMap()) + it.hasFieldEqualTo("site", DatadogSite.EU1) + } + .hasFieldEqualTo("clientToken", fakeConfiguration.clientToken) + .hasFieldEqualTo("env", fakeConfiguration.env) + .hasFieldEqualTo("variant", "") + .hasFieldEqualTo( + "additionalConfig", + fakeConfiguration.additionalConfig?.filterValues { it != null }.orEmpty() + ) + assertThat(rumConfigCaptor.firstValue) + .hasFieldEqualTo("applicationId", fakeConfiguration.applicationId) + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } @Test @@ -735,35 +922,49 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(sdkConfigCaptor.firstValue) - .hasField("coreConfig") { - it.hasFieldEqualTo("needsClearTextHttp", false) - it.hasFieldEqualTo("firstPartyHostsWithHeaderTypes", emptyMap()) - it.hasFieldEqualTo("site", DatadogSite.AP1) + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } - .hasFieldEqualTo("clientToken", fakeConfiguration.clientToken) - .hasFieldEqualTo("env", fakeConfiguration.env) - .hasFieldEqualTo("variant", "") - .hasFieldEqualTo( - "additionalConfig", - fakeConfiguration.additionalConfig?.filterValues { it != null }.orEmpty() - ) - assertThat(rumConfigCaptor.firstValue) - .hasFieldEqualTo("applicationId", fakeConfiguration.applicationId) + assertThat(sdkConfigCaptor.firstValue) + .hasField("coreConfig") { + it.hasFieldEqualTo("needsClearTextHttp", false) + it.hasFieldEqualTo("firstPartyHostsWithHeaderTypes", emptyMap()) + it.hasFieldEqualTo("site", DatadogSite.AP1) + } + .hasFieldEqualTo("clientToken", fakeConfiguration.clientToken) + .hasFieldEqualTo("env", fakeConfiguration.env) + .hasFieldEqualTo("variant", "") + .hasFieldEqualTo( + "additionalConfig", + fakeConfiguration.additionalConfig?.filterValues { it != null }.orEmpty() + ) + assertThat(rumConfigCaptor.firstValue) + .hasFieldEqualTo("applicationId", fakeConfiguration.applicationId) + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } @Test @@ -778,35 +979,49 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(sdkConfigCaptor.firstValue) - .hasField("coreConfig") { - it.hasFieldEqualTo("needsClearTextHttp", false) - it.hasFieldEqualTo("firstPartyHostsWithHeaderTypes", emptyMap()) - it.hasFieldEqualTo("site", DatadogSite.AP2) + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } - .hasFieldEqualTo("clientToken", fakeConfiguration.clientToken) - .hasFieldEqualTo("env", fakeConfiguration.env) - .hasFieldEqualTo("variant", "") - .hasFieldEqualTo( - "additionalConfig", - fakeConfiguration.additionalConfig?.filterValues { it != null }.orEmpty() - ) - assertThat(rumConfigCaptor.firstValue) - .hasFieldEqualTo("applicationId", fakeConfiguration.applicationId) + assertThat(sdkConfigCaptor.firstValue) + .hasField("coreConfig") { + it.hasFieldEqualTo("needsClearTextHttp", false) + it.hasFieldEqualTo("firstPartyHostsWithHeaderTypes", emptyMap()) + it.hasFieldEqualTo("site", DatadogSite.AP2) + } + .hasFieldEqualTo("clientToken", fakeConfiguration.clientToken) + .hasFieldEqualTo("env", fakeConfiguration.env) + .hasFieldEqualTo("variant", "") + .hasFieldEqualTo( + "additionalConfig", + fakeConfiguration.additionalConfig?.filterValues { it != null }.orEmpty() + ) + assertThat(rumConfigCaptor.firstValue) + .hasFieldEqualTo("applicationId", fakeConfiguration.applicationId) + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } // endregion @@ -822,19 +1037,33 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - eq(TrackingConsent.PENDING) - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + eq(TrackingConsent.PENDING) + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } + } + } finally { + rumMock.close() + logsMock.close() + traceMock.close() } } @@ -850,19 +1079,33 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - eq(TrackingConsent.PENDING) - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + eq(TrackingConsent.PENDING) + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } + } + } finally { + rumMock.close() + logsMock.close() + traceMock.close() } } @@ -878,19 +1121,33 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - eq(TrackingConsent.GRANTED) - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + eq(TrackingConsent.GRANTED) + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } + } + } finally { + rumMock.close() + logsMock.close() + traceMock.close() } } @@ -906,19 +1163,33 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - eq(TrackingConsent.NOT_GRANTED) - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + eq(TrackingConsent.NOT_GRANTED) + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } + } + } finally { + rumMock.close() + logsMock.close() + traceMock.close() } } @@ -937,24 +1208,38 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(rumConfigCaptor.firstValue) - .hasField("featureConfiguration") { - it.hasFieldEqualTo("viewTrackingStrategy", NoOpViewTrackingStrategy) + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } + assertThat(rumConfigCaptor.firstValue) + .hasField("featureConfiguration") { + it.hasFieldEqualTo("viewTrackingStrategy", NoOpViewTrackingStrategy) + } + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } @Test @@ -970,24 +1255,38 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(rumConfigCaptor.firstValue) - .hasField("featureConfiguration") { - it.hasFieldEqualTo("viewTrackingStrategy", ActivityViewTrackingStrategy(false)) + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } + assertThat(rumConfigCaptor.firstValue) + .hasField("featureConfiguration") { + it.hasFieldEqualTo("viewTrackingStrategy", ActivityViewTrackingStrategy(false)) + } + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } @Test @@ -1003,24 +1302,38 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(rumConfigCaptor.firstValue) - .hasField("featureConfiguration") { - it.hasFieldEqualTo("userActionTracking", false) + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } + assertThat(rumConfigCaptor.firstValue) + .hasField("featureConfiguration") { + it.hasFieldEqualTo("userActionTracking", false) + } + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } @Test @@ -1036,24 +1349,38 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(rumConfigCaptor.firstValue) - .hasField("featureConfiguration") { - it.hasFieldEqualTo("trackFrustrations", true) + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } + assertThat(rumConfigCaptor.firstValue) + .hasField("featureConfiguration") { + it.hasFieldEqualTo("trackFrustrations", true) + } + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } @Test @@ -1064,29 +1391,44 @@ internal class DdSdkTest { val bridgeConfiguration = configuration.copy( trackFrustrations = false ) + val sdkConfigCaptor = argumentCaptor() val rumConfigCaptor = argumentCaptor() val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(rumConfigCaptor.firstValue) - .hasField("featureConfiguration") { - it.hasFieldEqualTo("trackFrustrations", false) + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } + assertThat(rumConfigCaptor.firstValue) + .hasField("featureConfiguration") { + it.hasFieldEqualTo("trackFrustrations", false) + } + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } @Test @@ -1102,24 +1444,39 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(rumConfigCaptor.firstValue) - .hasField("featureConfiguration") { - it.hasFieldEqualTo("userActionTracking", true) + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } + + assertThat(rumConfigCaptor.firstValue) + .hasField("featureConfiguration") { + it.hasFieldEqualTo("userActionTracking", true) + } + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } @Test @@ -1177,35 +1534,49 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(sdkConfigCaptor.firstValue) - .hasField("coreConfig") { - it.hasFieldEqualTo("needsClearTextHttp", false) - it.hasFieldEqualTo("firstPartyHostsWithHeaderTypes", emptyMap()) + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } - .hasFieldEqualTo("clientToken", bridgeConfiguration.clientToken) - .hasFieldEqualTo("env", bridgeConfiguration.env) - .hasFieldEqualTo("variant", "") - .hasFieldEqualTo("service", serviceName) - .hasFieldEqualTo( - "additionalConfig", - bridgeConfiguration.additionalConfig?.filterValues { it != null }.orEmpty() - ) - assertThat(rumConfigCaptor.firstValue) - .hasFieldEqualTo("applicationId", bridgeConfiguration.applicationId) + assertThat(sdkConfigCaptor.firstValue) + .hasField("coreConfig") { + it.hasFieldEqualTo("needsClearTextHttp", false) + it.hasFieldEqualTo("firstPartyHostsWithHeaderTypes", emptyMap()) + } + .hasFieldEqualTo("clientToken", bridgeConfiguration.clientToken) + .hasFieldEqualTo("env", bridgeConfiguration.env) + .hasFieldEqualTo("variant", "") + .hasFieldEqualTo("service", serviceName) + .hasFieldEqualTo( + "additionalConfig", + bridgeConfiguration.additionalConfig?.filterValues { it != null }.orEmpty() + ) + assertThat(rumConfigCaptor.firstValue) + .hasFieldEqualTo("applicationId", bridgeConfiguration.applicationId) + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } @Test @@ -1224,31 +1595,45 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(rumConfigCaptor.firstValue) - .hasField("featureConfiguration") { rumConfig -> - rumConfig.hasField("longTaskTrackingStrategy") { longTaskTrackingStrategy -> - longTaskTrackingStrategy - .isInstanceOf( - "com.datadog.android.rum.internal.instrumentation." + - "MainLooperLongTaskStrategy" - ) - .hasFieldEqualTo("thresholdMs", threshold.toLong()) - } + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } + assertThat(rumConfigCaptor.firstValue) + .hasField("featureConfiguration") { rumConfig -> + rumConfig.hasField("longTaskTrackingStrategy") { longTaskTrackingStrategy -> + longTaskTrackingStrategy + .isInstanceOf( + "com.datadog.android.rum.internal.instrumentation." + + "MainLooperLongTaskStrategy" + ) + .hasFieldEqualTo("thresholdMs", threshold.toLong()) + } + } + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } @Test @@ -1265,24 +1650,38 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(rumConfigCaptor.firstValue) - .hasField("featureConfiguration") { rumConfig -> - rumConfig.doesNotHaveField("longTaskTrackingStrategy") + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } + assertThat(rumConfigCaptor.firstValue) + .hasField("featureConfiguration") { rumConfig -> + rumConfig.doesNotHaveField("longTaskTrackingStrategy") + } + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } @Test @@ -1326,27 +1725,41 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(sdkConfigCaptor.firstValue) - .hasField("coreConfig") { coreConfig -> - coreConfig.hasFieldEqualTo( - "firstPartyHostsWithHeaderTypes", - tracingHosts + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } + assertThat(sdkConfigCaptor.firstValue) + .hasField("coreConfig") { coreConfig -> + coreConfig.hasFieldEqualTo( + "firstPartyHostsWithHeaderTypes", + tracingHosts + ) + } + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } @Test @@ -1385,27 +1798,41 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(sdkConfigCaptor.firstValue) - .hasField("coreConfig") { coreConfig -> - coreConfig.hasFieldEqualTo( - "firstPartyHostsWithHeaderTypes", - tracingHosts + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } + assertThat(sdkConfigCaptor.firstValue) + .hasField("coreConfig") { coreConfig -> + coreConfig.hasFieldEqualTo( + "firstPartyHostsWithHeaderTypes", + tracingHosts + ) + } + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } @Test @@ -1451,27 +1878,41 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(sdkConfigCaptor.firstValue) - .hasField("coreConfig") { coreConfig -> - coreConfig.hasFieldEqualTo( - "firstPartyHostsWithHeaderTypes", - tracingHosts + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } + assertThat(sdkConfigCaptor.firstValue) + .hasField("coreConfig") { coreConfig -> + coreConfig.hasFieldEqualTo( + "firstPartyHostsWithHeaderTypes", + tracingHosts + ) + } + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } @ParameterizedTest @@ -1490,27 +1931,41 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(sdkConfigCaptor.firstValue) - .hasField("coreConfig") { coreConfig -> - coreConfig.hasFieldEqualTo( - "uploadFrequency", - expectedUploadFrequency + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } + assertThat(sdkConfigCaptor.firstValue) + .hasField("coreConfig") { coreConfig -> + coreConfig.hasFieldEqualTo( + "uploadFrequency", + expectedUploadFrequency + ) + } + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } @ParameterizedTest @@ -1529,27 +1984,41 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(sdkConfigCaptor.firstValue) - .hasField("coreConfig") { coreConfig -> - coreConfig.hasFieldEqualTo( - "batchSize", - expectedBatchSize + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } + assertThat(sdkConfigCaptor.firstValue) + .hasField("coreConfig") { coreConfig -> + coreConfig.hasFieldEqualTo( + "batchSize", + expectedBatchSize + ) + } + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } @ParameterizedTest @@ -1568,27 +2037,41 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(sdkConfigCaptor.firstValue) - .hasField("coreConfig") { coreConfig -> - coreConfig.hasFieldEqualTo( - "batchProcessingLevel", - expectedBatchSize + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } + assertThat(sdkConfigCaptor.firstValue) + .hasField("coreConfig") { coreConfig -> + coreConfig.hasFieldEqualTo( + "batchProcessingLevel", + expectedBatchSize + ) + } + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } @Test @@ -1606,24 +2089,38 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(rumConfigCaptor.firstValue) - .hasField("featureConfiguration") { - it.hasFieldEqualTo("backgroundEventTracking", trackBackgroundEvents ?: false) + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } + assertThat(rumConfigCaptor.firstValue) + .hasField("featureConfiguration") { + it.hasFieldEqualTo("backgroundEventTracking", trackBackgroundEvents ?: false) + } + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } @Test @@ -1639,28 +2136,42 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(rumConfigCaptor.firstValue) - .hasField("featureConfiguration") { - it.hasFieldEqualTo("vitalsMonitorUpdateFrequency", VitalsUpdateFrequency.RARE) + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } + assertThat(rumConfigCaptor.firstValue) + .hasField("featureConfiguration") { + it.hasFieldEqualTo("vitalsMonitorUpdateFrequency", VitalsUpdateFrequency.RARE) + } - argumentCaptor { - verify(mockChoreographer).postFrameCallback(capture()) - assertThat(firstValue).isInstanceOf(FpsFrameCallback::class.java) + argumentCaptor { + verify(mockChoreographer).postFrameCallback(capture()) + assertThat(firstValue).isInstanceOf(FpsFrameCallback::class.java) + } + } finally { + rumMock.close() + logsMock.close() + traceMock.close() } } @@ -1679,25 +2190,37 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(rumConfigCaptor.firstValue) - .hasField("featureConfiguration") { - it.hasFieldEqualTo("vitalsMonitorUpdateFrequency", VitalsUpdateFrequency.NEVER) + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } // When + testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } - verifyNoInteractions(mockChoreographer) + assertThat(rumConfigCaptor.firstValue) + .hasField("featureConfiguration") { + it.hasFieldEqualTo("vitalsMonitorUpdateFrequency", VitalsUpdateFrequency.NEVER) + } + verifyNoInteractions(mockChoreographer) + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } @Test @@ -1719,41 +2242,56 @@ internal class DdSdkTest { val traceConfigCaptor = argumentCaptor() val frameDurationNs = threshold + frameDurationOverThreshold - // When - testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) - - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(rumConfigCaptor.firstValue) - .hasField("featureConfiguration") { - it.hasFieldEqualTo("vitalsMonitorUpdateFrequency", VitalsUpdateFrequency.AVERAGE) - } - argumentCaptor { - verify(mockChoreographer).postFrameCallback(capture()) - assertThat(firstValue).isInstanceOf(FpsFrameCallback::class.java) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // When - firstValue.doFrame(timestampNs) - firstValue.doFrame(timestampNs + frameDurationNs) + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } // When + testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) - // then - verify(mockRumMonitor._getInternal()!!).updatePerformanceMetric( - RumPerformanceMetric.JS_FRAME_TIME, - frameDurationNs.toDouble() - ) - verify(mockRumMonitor._getInternal()!!, never()).addLongTask( - frameDurationNs, - "javascript" - ) + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } + } + assertThat(rumConfigCaptor.firstValue) + .hasField("featureConfiguration") { + it.hasFieldEqualTo( + "vitalsMonitorUpdateFrequency", + VitalsUpdateFrequency.AVERAGE + ) + } + argumentCaptor { + verify(mockChoreographer).postFrameCallback(capture()) + assertThat(firstValue).isInstanceOf(FpsFrameCallback::class.java) + + // When + firstValue.doFrame(timestampNs) + firstValue.doFrame(timestampNs + frameDurationNs) + + // then + verify(mockRumMonitor._getInternal()!!).updatePerformanceMetric( + RumPerformanceMetric.JS_FRAME_TIME, + frameDurationNs.toDouble() + ) + verify(mockRumMonitor._getInternal()!!, never()).addLongTask( + frameDurationNs, + "javascript" + ) + } + } finally { + rumMock.close() + logsMock.close() + traceMock.close() } } @@ -1846,25 +2384,37 @@ internal class DdSdkTest { val traceConfigCaptor = argumentCaptor() val defaultTimeBasedIdentifier = TimeBasedInitialResourceIdentifier(100) - // When - testedBridgeSdk.initialize(configuration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } // When + testedBridgeSdk.initialize(configuration.toReadableJavaOnlyMap(), mockPromise) - assertThat(rumConfigCaptor.firstValue) - .hasField("featureConfiguration") { - it.hasFieldEqualTo("initialResourceIdentifier", defaultTimeBasedIdentifier) + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } + + assertThat(rumConfigCaptor.firstValue) + .hasField("featureConfiguration") { + it.hasFieldEqualTo("initialResourceIdentifier", defaultTimeBasedIdentifier) + } + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } @Test @@ -1884,25 +2434,37 @@ internal class DdSdkTest { thresholdInSeconds.seconds.inWholeMilliseconds ) - // When - testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } // When + testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) - assertThat(rumConfigCaptor.firstValue) - .hasField("featureConfiguration") { - it.hasFieldEqualTo("initialResourceIdentifier", timeBasedIdentifier) + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } + + assertThat(rumConfigCaptor.firstValue) + .hasField("featureConfiguration") { + it.hasFieldEqualTo("initialResourceIdentifier", timeBasedIdentifier) + } + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } // endregion @@ -1925,28 +2487,42 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(sdkConfigCaptor.firstValue) - .hasFieldEqualTo( - "additionalConfig", - mapOf( - DdSdkImplementation.DD_VERSION_SUFFIX to versionSuffix, - DdSdkImplementation.DD_VERSION to mockPackageInfo.versionName + versionSuffix + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } // When + testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() ) - ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } + } + assertThat(sdkConfigCaptor.firstValue) + .hasFieldEqualTo( + "additionalConfig", + mapOf( + DdSdkImplementation.DD_VERSION_SUFFIX to versionSuffix, + DdSdkImplementation.DD_VERSION to ( + mockPackageInfo.versionName + versionSuffix + ) + ) + ) + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } // endregion @@ -1985,47 +2561,59 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(rumConfigCaptor.firstValue) - .hasField("featureConfiguration") { - val configurationMapper = it - .getActualValue>( - "telemetryConfigurationMapper" - ) - val result = configurationMapper.map(telemetryConfigurationEvent)!! - assertThat(result.telemetry.configuration.trackNativeErrors!!).isEqualTo( - trackNativeErrors - ) - assertThat(result.telemetry.configuration.trackCrossPlatformLongTasks!!) - .isEqualTo(false) - assertThat(result.telemetry.configuration.trackLongTask!!) - .isEqualTo(false) - assertThat(result.telemetry.configuration.trackNativeLongTasks!!) - .isEqualTo(false) - - assertThat(result.telemetry.configuration.initializationType!!) - .isEqualTo(initializationType) - assertThat(result.telemetry.configuration.trackInteractions!!) - .isEqualTo(trackInteractions) - assertThat(result.telemetry.configuration.trackErrors!!).isEqualTo(trackErrors) - assertThat(result.telemetry.configuration.trackResources!!) - .isEqualTo(trackNetworkRequests) - assertThat(result.telemetry.configuration.trackNetworkRequests!!) - .isEqualTo(trackNetworkRequests) + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } // When + testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } + assertThat(rumConfigCaptor.firstValue) + .hasField("featureConfiguration") { + val configurationMapper = it + .getActualValue>( + "telemetryConfigurationMapper" + ) + val result = configurationMapper.map(telemetryConfigurationEvent)!! + assertThat(result.telemetry.configuration.trackNativeErrors!!).isEqualTo( + trackNativeErrors + ) + assertThat(result.telemetry.configuration.trackCrossPlatformLongTasks!!) + .isEqualTo(false) + assertThat(result.telemetry.configuration.trackLongTask!!) + .isEqualTo(false) + assertThat(result.telemetry.configuration.trackNativeLongTasks!!) + .isEqualTo(false) + + assertThat(result.telemetry.configuration.initializationType!!) + .isEqualTo(initializationType) + assertThat(result.telemetry.configuration.trackInteractions!!) + .isEqualTo(trackInteractions) + assertThat(result.telemetry.configuration.trackErrors!!).isEqualTo(trackErrors) + assertThat(result.telemetry.configuration.trackResources!!) + .isEqualTo(trackNetworkRequests) + assertThat(result.telemetry.configuration.trackNetworkRequests!!) + .isEqualTo(trackNetworkRequests) + } + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } // endregion @@ -2042,27 +2630,39 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(rumConfigCaptor.firstValue) - .hasField("featureConfiguration") { - val resourceMapper = it - .getActualValue>("resourceEventMapper") - val notDroppedEvent = resourceMapper.map(resourceEvent) - assertThat(notDroppedEvent).isNotNull + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } // When + testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } + assertThat(rumConfigCaptor.firstValue) + .hasField("featureConfiguration") { + val resourceMapper = it + .getActualValue>("resourceEventMapper") + val notDroppedEvent = resourceMapper.map(resourceEvent) + assertThat(notDroppedEvent).isNotNull + } + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } @Test @@ -2076,27 +2676,39 @@ internal class DdSdkTest { val traceConfigCaptor = argumentCaptor() resourceEvent.context?.additionalProperties?.put("_dd.resource.drop_resource", true) - // When - testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(rumConfigCaptor.firstValue) - .hasField("featureConfiguration") { - val resourceMapper = it - .getActualValue>("resourceEventMapper") - val droppedEvent = resourceMapper.map(resourceEvent) - assertThat(droppedEvent).isNull() + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } // When + testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } + assertThat(rumConfigCaptor.firstValue) + .hasField("featureConfiguration") { + val resourceMapper = it + .getActualValue>("resourceEventMapper") + val droppedEvent = resourceMapper.map(resourceEvent) + assertThat(droppedEvent).isNull() + } + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } // endregion @@ -2113,27 +2725,39 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(rumConfigCaptor.firstValue) - .hasField("featureConfiguration") { - val actionMapper = it - .getActualValue>("actionEventMapper") - val notDroppedEvent = actionMapper.map(actionEvent) - assertThat(notDroppedEvent).isNotNull + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } // When + testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } + assertThat(rumConfigCaptor.firstValue) + .hasField("featureConfiguration") { + val actionMapper = it + .getActualValue>("actionEventMapper") + val notDroppedEvent = actionMapper.map(actionEvent) + assertThat(notDroppedEvent).isNotNull + } + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } @Test @@ -2147,27 +2771,39 @@ internal class DdSdkTest { val traceConfigCaptor = argumentCaptor() actionEvent.context?.additionalProperties?.put("_dd.action.drop_action", true) - // When - testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(rumConfigCaptor.firstValue) - .hasField("featureConfiguration") { - val actionMapper = it - .getActualValue>("actionEventMapper") - val droppedEvent = actionMapper.map(actionEvent) - assertThat(droppedEvent).isNull() + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } // When + testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } + assertThat(rumConfigCaptor.firstValue) + .hasField("featureConfiguration") { + val actionMapper = it + .getActualValue>("actionEventMapper") + val droppedEvent = actionMapper.map(actionEvent) + assertThat(droppedEvent).isNull() + } + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } // endregion @@ -2580,24 +3216,36 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } // When + testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) - assertThat(rumConfigCaptor.firstValue) - .hasField("featureConfiguration") { - it.hasFieldEqualTo("customEndpointUrl", customRumEndpoint) + // Then + inOrder(mockDatadog) { + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } - assertThat(logsConfigCaptor.firstValue) - .hasFieldEqualTo("customEndpointUrl", customLogsEndpoint) - assertThat(traceConfigCaptor.firstValue) - .hasFieldEqualTo("customEndpointUrl", customTraceEndpoint) + + assertThat(rumConfigCaptor.firstValue) + .hasField("featureConfiguration") { + it.hasFieldEqualTo("customEndpointUrl", customRumEndpoint) + } + assertThat(logsConfigCaptor.firstValue) + .hasFieldEqualTo("customEndpointUrl", customLogsEndpoint) + assertThat(traceConfigCaptor.firstValue) + .hasFieldEqualTo("customEndpointUrl", customTraceEndpoint) + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } @Test diff --git a/packages/internal-testing-tools/android/src/main/kotlin/com/datadog/reactnative/internaltesting/DdInternalTestingImplementation.kt b/packages/internal-testing-tools/android/src/main/kotlin/com/datadog/reactnative/internaltesting/DdInternalTestingImplementation.kt index 18c6c7e91..5729ae8c5 100644 --- a/packages/internal-testing-tools/android/src/main/kotlin/com/datadog/reactnative/internaltesting/DdInternalTestingImplementation.kt +++ b/packages/internal-testing-tools/android/src/main/kotlin/com/datadog/reactnative/internaltesting/DdInternalTestingImplementation.kt @@ -58,6 +58,12 @@ class DdInternalTestingImplementation { promise.resolve(null) } + /** + * Get wrapped core instance. + */ + internal fun getWrappedCore(): StubSDKCore? { + return wrappedCore + } companion object { internal const val NAME = "DdInternalTesting" diff --git a/packages/internal-testing-tools/android/src/test/kotlin/com/datadog/reactnative/internaltesting/DdInternalTestingImplementationTest.kt b/packages/internal-testing-tools/android/src/test/kotlin/com/datadog/reactnative/internaltesting/DdInternalTestingImplementationTest.kt index c542ed6bc..4a6938f9b 100644 --- a/packages/internal-testing-tools/android/src/test/kotlin/com/datadog/reactnative/internaltesting/DdInternalTestingImplementationTest.kt +++ b/packages/internal-testing-tools/android/src/test/kotlin/com/datadog/reactnative/internaltesting/DdInternalTestingImplementationTest.kt @@ -80,7 +80,9 @@ internal class DdInternalTestingImplementationTest { // Simulating DdSdkImplementation initialization DatadogSDKWrapperStorage.notifyOnInitializedListeners(mockCore) - val wrappedCore = Datadog.getInstance() as StubSDKCore + val wrappedCore = testedInternalTesting.getWrappedCore() + requireNotNull(wrappedCore) + wrappedCore.registerFeature(mockFeature) requireNotNull(wrappedCore.getFeature(mockFeature.name)) .withWriteContext { _, eventBatchWriter -> From 51d3d3217c7411dc496bdf8a4647f9be1b92d861 Mon Sep 17 00:00:00 2001 From: Sergio Barrio Date: Fri, 5 Sep 2025 15:29:37 +0200 Subject: [PATCH 009/410] Remove type interdependencies between modules --- packages/core/src/{rum => }/DdAttributes.ts | 0 .../src/DdSdkReactNativeConfiguration.tsx | 2 +- .../src/__tests__/DdSdkReactNative.test.tsx | 3 +- packages/core/src/index.tsx | 3 +- packages/core/src/logs/DdLogs.ts | 5 +- .../core/src/logs/__tests__/DdLogs.test.ts | 4 +- .../src/logs/__tests__/eventMapper.test.ts | 2 +- packages/core/src/logs/eventMapper.ts | 3 +- packages/core/src/logs/types.ts | 21 +------- packages/core/src/rum/DdRum.ts | 4 +- packages/core/src/rum/__tests__/DdRum.test.ts | 3 +- .../src/rum/eventMappers/errorEventMapper.ts | 2 +- .../instrumentation/DdRumErrorTracking.tsx | 2 +- packages/core/src/rum/types.ts | 10 +--- packages/core/src/types.tsx | 49 ++++++++++++++++--- 15 files changed, 62 insertions(+), 51 deletions(-) rename packages/core/src/{rum => }/DdAttributes.ts (100%) diff --git a/packages/core/src/rum/DdAttributes.ts b/packages/core/src/DdAttributes.ts similarity index 100% rename from packages/core/src/rum/DdAttributes.ts rename to packages/core/src/DdAttributes.ts diff --git a/packages/core/src/DdSdkReactNativeConfiguration.tsx b/packages/core/src/DdSdkReactNativeConfiguration.tsx index 9be08fa28..44debb2d2 100644 --- a/packages/core/src/DdSdkReactNativeConfiguration.tsx +++ b/packages/core/src/DdSdkReactNativeConfiguration.tsx @@ -7,12 +7,12 @@ import type { ProxyConfiguration } from './ProxyConfiguration'; import type { SdkVerbosity } from './SdkVerbosity'; import { TrackingConsent } from './TrackingConsent'; -import type { LogEventMapper } from './logs/types'; import type { ActionEventMapper } from './rum/eventMappers/actionEventMapper'; import type { ErrorEventMapper } from './rum/eventMappers/errorEventMapper'; import type { ResourceEventMapper } from './rum/eventMappers/resourceEventMapper'; import type { FirstPartyHost } from './rum/types'; import { PropagatorType } from './rum/types'; +import type { LogEventMapper } from './types'; export enum VitalsUpdateFrequency { FREQUENT = 'FREQUENT', diff --git a/packages/core/src/__tests__/DdSdkReactNative.test.tsx b/packages/core/src/__tests__/DdSdkReactNative.test.tsx index 49c0bd1f2..18bf060ce 100644 --- a/packages/core/src/__tests__/DdSdkReactNative.test.tsx +++ b/packages/core/src/__tests__/DdSdkReactNative.test.tsx @@ -17,11 +17,12 @@ import { DdRum } from '../rum/DdRum'; import { DdRumErrorTracking } from '../rum/instrumentation/DdRumErrorTracking'; import { DdRumUserInteractionTracking } from '../rum/instrumentation/interactionTracking/DdRumUserInteractionTracking'; import { DdRumResourceTracking } from '../rum/instrumentation/resourceTracking/DdRumResourceTracking'; -import { ErrorSource, PropagatorType, RumActionType } from '../rum/types'; +import { PropagatorType, RumActionType } from '../rum/types'; import { AttributesSingleton } from '../sdk/AttributesSingleton/AttributesSingleton'; import { DdSdk } from '../sdk/DdSdk'; import { GlobalState } from '../sdk/GlobalState/GlobalState'; import { UserInfoSingleton } from '../sdk/UserInfoSingleton/UserInfoSingleton'; +import { ErrorSource } from '../types'; import type { DdSdkConfiguration } from '../types'; import { version as sdkVersion } from '../version'; diff --git a/packages/core/src/index.tsx b/packages/core/src/index.tsx index 9332354dc..062fecc90 100644 --- a/packages/core/src/index.tsx +++ b/packages/core/src/index.tsx @@ -37,11 +37,12 @@ import { DATADOG_GRAPH_QL_VARIABLES_HEADER } from './rum/instrumentation/resourceTracking/graphql/graphqlHeaders'; import type { FirstPartyHost } from './rum/types'; -import { ErrorSource, PropagatorType, RumActionType } from './rum/types'; +import { PropagatorType, RumActionType } from './rum/types'; import { DatadogProvider } from './sdk/DatadogProvider/DatadogProvider'; import { DdSdk } from './sdk/DdSdk'; import { FileBasedConfiguration } from './sdk/FileBasedConfiguration/FileBasedConfiguration'; import { DdTrace } from './trace/DdTrace'; +import { ErrorSource } from './types'; import { DefaultTimeProvider } from './utils/time-provider/DefaultTimeProvider'; import type { Timestamp } from './utils/time-provider/TimeProvider'; import { TimeProvider } from './utils/time-provider/TimeProvider'; diff --git a/packages/core/src/logs/DdLogs.ts b/packages/core/src/logs/DdLogs.ts index 9ac35fa73..ff4cc1a55 100644 --- a/packages/core/src/logs/DdLogs.ts +++ b/packages/core/src/logs/DdLogs.ts @@ -4,18 +4,17 @@ * Copyright 2016-Present Datadog, Inc. */ +import { DdAttributes } from '../DdAttributes'; import { DATADOG_MESSAGE_PREFIX, InternalLog } from '../InternalLog'; import { SdkVerbosity } from '../SdkVerbosity'; import type { DdNativeLogsType } from '../nativeModulesTypes'; -import { DdAttributes } from '../rum/DdAttributes'; -import type { ErrorSource } from '../rum/types'; +import type { ErrorSource, LogEventMapper } from '../types'; import { validateContext } from '../utils/argsUtils'; import { generateEventMapper } from './eventMapper'; import type { DdLogsType, LogArguments, - LogEventMapper, LogWithErrorArguments, NativeLogWithError, RawLogWithError diff --git a/packages/core/src/logs/__tests__/DdLogs.test.ts b/packages/core/src/logs/__tests__/DdLogs.test.ts index 6a530ec36..f7e848a0b 100644 --- a/packages/core/src/logs/__tests__/DdLogs.test.ts +++ b/packages/core/src/logs/__tests__/DdLogs.test.ts @@ -11,9 +11,9 @@ import { DdSdkReactNative } from '../../DdSdkReactNative'; import { InternalLog } from '../../InternalLog'; import { SdkVerbosity } from '../../SdkVerbosity'; import type { DdNativeLogsType } from '../../nativeModulesTypes'; -import { ErrorSource } from '../../rum/types'; +import { ErrorSource } from '../../types'; +import type { LogEventMapper } from '../../types'; import { DdLogs } from '../DdLogs'; -import type { LogEventMapper } from '../types'; jest.mock('../../InternalLog', () => { return { diff --git a/packages/core/src/logs/__tests__/eventMapper.test.ts b/packages/core/src/logs/__tests__/eventMapper.test.ts index 0999a6058..cd505f811 100644 --- a/packages/core/src/logs/__tests__/eventMapper.test.ts +++ b/packages/core/src/logs/__tests__/eventMapper.test.ts @@ -5,7 +5,7 @@ */ /* eslint-disable @typescript-eslint/ban-ts-comment */ -import { ErrorSource } from '../../rum/types'; +import { ErrorSource } from '../../types'; import { formatRawLogToLogEvent } from '../eventMapper'; describe('formatRawLogToLogEvent', () => { diff --git a/packages/core/src/logs/eventMapper.ts b/packages/core/src/logs/eventMapper.ts index 2bbd398cb..eb7b5f22c 100644 --- a/packages/core/src/logs/eventMapper.ts +++ b/packages/core/src/logs/eventMapper.ts @@ -7,10 +7,9 @@ import type { Attributes } from '../sdk/AttributesSingleton/types'; import { EventMapper } from '../sdk/EventMappers/EventMapper'; import type { UserInfo } from '../sdk/UserInfoSingleton/types'; +import type { LogEvent, LogEventMapper } from '../types'; import type { - LogEvent, - LogEventMapper, NativeLog, NativeLogWithError, RawLog, diff --git a/packages/core/src/logs/types.ts b/packages/core/src/logs/types.ts index 9c1b3cb09..18cdbc533 100644 --- a/packages/core/src/logs/types.ts +++ b/packages/core/src/logs/types.ts @@ -4,8 +4,7 @@ * Copyright 2016-Present Datadog, Inc. */ -import type { ErrorSource } from '../rum/types'; -import type { UserInfo } from '../sdk/UserInfoSingleton/types'; +import type { LogStatus, ErrorSource } from '../types'; /** * The entry point to use Datadog's Logs feature. @@ -75,24 +74,6 @@ export type NativeLogWithError = { fingerprint?: string; }; -export type LogStatus = 'debug' | 'info' | 'warn' | 'error'; - -export type LogEvent = { - message: string; - context: object; - errorKind?: string; - errorMessage?: string; - stacktrace?: string; - fingerprint?: string; - readonly source?: ErrorSource; - // readonly date: number; // TODO: RUMM-2446 & RUMM-2447 - readonly status: LogStatus; - readonly userInfo: UserInfo; - readonly attributes?: object; -}; - -export type LogEventMapper = (logEvent: LogEvent) => LogEvent | null; - export type LogArguments = [message: string, context?: object]; export type LogWithErrorArguments = [ diff --git a/packages/core/src/rum/DdRum.ts b/packages/core/src/rum/DdRum.ts index 8943ed5b0..f24805e35 100644 --- a/packages/core/src/rum/DdRum.ts +++ b/packages/core/src/rum/DdRum.ts @@ -5,18 +5,19 @@ */ import type { GestureResponderEvent } from 'react-native'; +import { DdAttributes } from '../DdAttributes'; import { InternalLog } from '../InternalLog'; import { SdkVerbosity } from '../SdkVerbosity'; import type { DdNativeRumType } from '../nativeModulesTypes'; import { bufferVoidNativeCall } from '../sdk/DatadogProvider/Buffer/bufferNativeCall'; import { DdSdk } from '../sdk/DdSdk'; import { GlobalState } from '../sdk/GlobalState/GlobalState'; +import type { ErrorSource } from '../types'; import { validateContext } from '../utils/argsUtils'; import { getErrorContext } from '../utils/errorUtils'; import { DefaultTimeProvider } from '../utils/time-provider/DefaultTimeProvider'; import type { TimeProvider } from '../utils/time-provider/TimeProvider'; -import { DdAttributes } from './DdAttributes'; import { generateActionEventMapper } from './eventMappers/actionEventMapper'; import type { ActionEventMapper } from './eventMappers/actionEventMapper'; import { generateErrorEventMapper } from './eventMappers/errorEventMapper'; @@ -35,7 +36,6 @@ import { setCachedSessionId } from './sessionId/sessionIdHelper'; import type { - ErrorSource, DdRumType, RumActionType, ResourceKind, diff --git a/packages/core/src/rum/__tests__/DdRum.test.ts b/packages/core/src/rum/__tests__/DdRum.test.ts index 527492891..7e5fc24de 100644 --- a/packages/core/src/rum/__tests__/DdRum.test.ts +++ b/packages/core/src/rum/__tests__/DdRum.test.ts @@ -12,6 +12,7 @@ import { SdkVerbosity } from '../../SdkVerbosity'; import { BufferSingleton } from '../../sdk/DatadogProvider/Buffer/BufferSingleton'; import { DdSdk } from '../../sdk/DdSdk'; import { GlobalState } from '../../sdk/GlobalState/GlobalState'; +import { ErrorSource } from '../../types'; import { DdRum } from '../DdRum'; import type { ActionEventMapper } from '../eventMappers/actionEventMapper'; import type { ErrorEventMapper } from '../eventMappers/errorEventMapper'; @@ -22,7 +23,7 @@ import { TracingIdFormat } from '../instrumentation/resourceTracking/distributed import { TracingIdentifierUtils } from '../instrumentation/resourceTracking/distributedTracing/__tests__/__utils__/TracingIdentifierUtils'; import { setCachedSessionId } from '../sessionId/sessionIdHelper'; import type { FirstPartyHost } from '../types'; -import { ErrorSource, PropagatorType, RumActionType } from '../types'; +import { PropagatorType, RumActionType } from '../types'; import * as TracingContextUtils from './__utils__/TracingHeadersUtils'; diff --git a/packages/core/src/rum/eventMappers/errorEventMapper.ts b/packages/core/src/rum/eventMappers/errorEventMapper.ts index 462754d2c..6630d59a4 100644 --- a/packages/core/src/rum/eventMappers/errorEventMapper.ts +++ b/packages/core/src/rum/eventMappers/errorEventMapper.ts @@ -6,7 +6,7 @@ import type { AdditionalEventDataForMapper } from '../../sdk/EventMappers/EventMapper'; import { EventMapper } from '../../sdk/EventMappers/EventMapper'; -import type { ErrorSource } from '../types'; +import type { ErrorSource } from '../../types'; type RawError = { message: string; diff --git a/packages/core/src/rum/instrumentation/DdRumErrorTracking.tsx b/packages/core/src/rum/instrumentation/DdRumErrorTracking.tsx index 5a45bdc65..3c3ec9f65 100644 --- a/packages/core/src/rum/instrumentation/DdRumErrorTracking.tsx +++ b/packages/core/src/rum/instrumentation/DdRumErrorTracking.tsx @@ -8,6 +8,7 @@ import type { ErrorHandlerCallback } from 'react-native'; import { InternalLog } from '../../InternalLog'; import { SdkVerbosity } from '../../SdkVerbosity'; +import { ErrorSource } from '../../types'; import { getErrorMessage, getErrorStackTrace, @@ -18,7 +19,6 @@ import { } from '../../utils/errorUtils'; import { executeWithDelay } from '../../utils/jsUtils'; import { DdRum } from '../DdRum'; -import { ErrorSource } from '../types'; /** * Provides RUM auto-instrumentation feature to track errors as RUM events. diff --git a/packages/core/src/rum/types.ts b/packages/core/src/rum/types.ts index 5834123a6..fc8d07c02 100644 --- a/packages/core/src/rum/types.ts +++ b/packages/core/src/rum/types.ts @@ -4,6 +4,8 @@ * Copyright 2016-Present Datadog, Inc. */ +import type { ErrorSource } from '../types'; + import type { DatadogTracingContext } from './instrumentation/resourceTracking/distributedTracing/DatadogTracingContext'; import type { DatadogTracingIdentifier } from './instrumentation/resourceTracking/distributedTracing/DatadogTracingIdentifier'; @@ -233,14 +235,6 @@ export type ResourceKind = | 'other' | 'native'; -export enum ErrorSource { - NETWORK = 'NETWORK', - SOURCE = 'SOURCE', - CONSOLE = 'CONSOLE', - WEBVIEW = 'WEBVIEW', - CUSTOM = 'CUSTOM' -} - /** * Type of instrumentation on the host. * - DATADOG: Datadog’s propagator (`x-datadog-*`) diff --git a/packages/core/src/types.tsx b/packages/core/src/types.tsx index 4db877469..e1c5096fb 100644 --- a/packages/core/src/types.tsx +++ b/packages/core/src/types.tsx @@ -5,6 +5,7 @@ */ import type { BatchProcessingLevel } from './DdSdkReactNativeConfiguration'; +import type { UserInfo as UserInfoSingleton } from './sdk/UserInfoSingleton/types'; declare global { // eslint-disable-next-line no-var, vars-on-top @@ -118,13 +119,6 @@ export type DdSdkType = { setTrackingConsent(trackingConsent: string): Promise; }; -export type UserInfo = { - id: string; - name?: string; - email?: string; - extraInfo?: object; -}; - /** * The entry point to use Datadog's Trace feature. */ @@ -153,3 +147,44 @@ export type DdTraceType = { timestampMs?: number ): Promise; }; + +// Shared types across modules + +// Core + +export type UserInfo = { + id: string; + name?: string; + email?: string; + extraInfo?: object; +}; + +// DdLogs + +export type LogStatus = 'debug' | 'info' | 'warn' | 'error'; + +export type LogEvent = { + message: string; + context: object; + errorKind?: string; + errorMessage?: string; + stacktrace?: string; + fingerprint?: string; + readonly source?: ErrorSource; + // readonly date: number; // TODO: RUMM-2446 & RUMM-2447 + readonly status: LogStatus; + readonly userInfo: UserInfoSingleton; + readonly attributes?: object; +}; + +export type LogEventMapper = (logEvent: LogEvent) => LogEvent | null; + +// DdRum + +export enum ErrorSource { + NETWORK = 'NETWORK', + SOURCE = 'SOURCE', + CONSOLE = 'CONSOLE', + WEBVIEW = 'WEBVIEW', + CUSTOM = 'CUSTOM' +} From 93aa6125b648862f7ecdcb17b2f268aa7fe99a75 Mon Sep 17 00:00:00 2001 From: Marco Saia Date: Tue, 9 Sep 2025 12:08:35 +0200 Subject: [PATCH 010/410] iOS: Always use SDK default core instance --- .../core/ios/Sources/DatadogSDKWrapper.swift | 137 ++---------------- .../ios/Sources/DdLogsImplementation.swift | 7 +- .../ios/Sources/DdSdkImplementation.swift | 15 +- .../Sources/DdSdkNativeInitialization.swift | 21 ++- packages/core/ios/Sources/DdTelemetry.swift | 49 +++++++ .../ios/Tests/DatadogSdkWrapperTests.swift | 17 ++- packages/core/ios/Tests/DdSdkTests.swift | 58 +++----- .../DdSessionReplayImplementation.swift | 26 +--- .../ios/Tests/DdSessionReplayTests.swift | 6 +- .../Sources/RCTDatadogWebViewTracking.swift | 18 ++- .../DatadogSDKReactNativeWebViewTests.swift | 14 +- 11 files changed, 146 insertions(+), 222 deletions(-) create mode 100644 packages/core/ios/Sources/DdTelemetry.swift diff --git a/packages/core/ios/Sources/DatadogSDKWrapper.swift b/packages/core/ios/Sources/DatadogSDKWrapper.swift index 842ee5d89..0b9e51573 100644 --- a/packages/core/ios/Sources/DatadogSDKWrapper.swift +++ b/packages/core/ios/Sources/DatadogSDKWrapper.swift @@ -14,7 +14,7 @@ import DatadogWebViewTracking import DatadogInternal import Foundation -public typealias OnCoreInitializedListener = (DatadogCoreProtocol) -> Void +public typealias OnSdkInitializedListener = () -> Void /// Wrapper around the Datadog SDK. Use DatadogSDKWrapper.shared to access the instance. public class DatadogSDKWrapper { @@ -22,25 +22,14 @@ public class DatadogSDKWrapper { public static var shared = DatadogSDKWrapper() // Initialization callbacks - internal var onCoreInitializedListeners: [OnCoreInitializedListener] = [] - internal var loggerConfiguration = DatadogLogs.Logger.Configuration() - // Core instance - private var coreInstance: DatadogCoreProtocol? = nil + internal var onSdkInitializedListeners: [OnSdkInitializedListener] = [] - private init() { } - - public func addOnCoreInitializedListener(listener:@escaping OnCoreInitializedListener) { - onCoreInitializedListeners.append(listener) - } + internal private(set) var loggerConfiguration = DatadogLogs.Logger.Configuration() - /// This is intended for internal testing only. - public func setCoreInstance(core: DatadogCoreProtocol?) { - self.coreInstance = core - } + private init() { } - /// This is not supposed to be used in the SDK itself, rather by other SDKs like Session Replay. - public func getCoreInstance() -> DatadogCoreProtocol? { - return coreInstance + public func addOnSdkInitializedListener(listener:@escaping OnSdkInitializedListener) { + onSdkInitializedListeners.append(listener) } // SDK Wrapper @@ -49,124 +38,22 @@ public class DatadogSDKWrapper { loggerConfiguration: DatadogLogs.Logger.Configuration, trackingConsent: TrackingConsent ) -> Void { - let core = Datadog.initialize(with: coreConfiguration, trackingConsent: trackingConsent) - setCoreInstance(core: core) - for listener in onCoreInitializedListeners { - listener(core) - } - - self.loggerConfiguration = loggerConfiguration - } - - internal func isInitialized() -> Bool { - return Datadog.isInitialized() - } - - internal func clearAllData() -> Void { - if let core = coreInstance { - Datadog.clearAllData(in: core) - } else { - Datadog.clearAllData() - } - } - - // Features - internal func enableRUM(with configuration: RUM.Configuration) { - if let core = coreInstance { - RUM.enable(with: configuration, in: core) - } else { - consolePrint("Core instance was not found when initializing RUM.", .critical) - } - } - - internal func enableLogs(with configuration: Logs.Configuration) { - if let core = coreInstance { - Logs.enable(with: configuration, in: core) - } else { - consolePrint("Core instance was not found when initializing Logs.", .critical) - } - } - - internal func enableTrace(with configuration: Trace.Configuration) { - if let core = coreInstance { - Trace.enable(with: configuration, in: core) - } else { - consolePrint("Core instance was not found when initializing Trace.", .critical) - } - } - - internal func enableCrashReporting() { - if let core = coreInstance { - CrashReporting.enable(in: core) - } else { - consolePrint("Core instance was not found when initializing CrashReporting.", .critical) - } - } - - internal func createLogger() -> LoggerProtocol { - let core = coreInstance ?? { - consolePrint("Core instance was not found when creating Logger.", .critical) - return CoreRegistry.default - }() + Datadog.initialize(with: coreConfiguration, trackingConsent: trackingConsent) - return DatadogLogs.Logger.create(with: loggerConfiguration, in: core) - } - - // Telemetry - internal func sendTelemetryLog(message: String, attributes: [String: any Encodable], config: [String: any Encodable]) { - if let core = coreInstance { - let id = (config["onlyOnce"] as? Bool) == true ? message : UUID().uuidString - core.telemetry.debug(id: id, message: message, attributes: attributes) - } else { - consolePrint("Core instance was not found when calling sendTelemetryLog.", .warn) + for listener in onSdkInitializedListeners { + listener() } - } - internal func telemetryDebug(id: String, message: String) { - return Datadog._internal.telemetry.debug(id: id, message: message) - } - - internal func telemetryError(id: String, message: String, kind: String?, stack: String?) { - return Datadog._internal.telemetry.error(id: id, message: message, kind: kind, stack: stack) - } - - internal func overrideTelemetryConfiguration( - initializationType: String? = nil, - reactNativeVersion: String? = nil, - reactVersion: String? = nil, - trackCrossPlatformLongTasks: Bool? = nil, - trackErrors: Bool? = nil, - trackInteractions: Bool? = nil, - trackLongTask: Bool? = nil, - trackNativeErrors: Bool? = nil, - trackNativeLongTasks: Bool? = nil, - trackNetworkRequests: Bool? = nil - ) { - coreInstance?.telemetry.configuration( - initializationType: initializationType, - reactNativeVersion: reactNativeVersion, - reactVersion: reactVersion, - trackCrossPlatformLongTasks: trackCrossPlatformLongTasks, - trackErrors: trackErrors, - trackLongTask: trackLongTask, - trackNativeErrors: trackNativeErrors, - trackNativeLongTasks: trackNativeLongTasks, - trackNetworkRequests: trackNetworkRequests, - trackUserInteractions: trackInteractions - ) + self.loggerConfiguration = loggerConfiguration } // Webview private var webviewMessageEmitter: InternalExtension.AbstractMessageEmitter? internal func enableWebviewTracking() { - if let core = coreInstance { - webviewMessageEmitter = WebViewTracking._internal.messageEmitter(in: core) - } else { - consolePrint("Core instance was not found when initializing Webview tracking.", .critical) - } + webviewMessageEmitter = WebViewTracking._internal.messageEmitter(in: CoreRegistry.default) } - + internal func sendWebviewMessage(body: NSString) throws { try self.webviewMessageEmitter?.send(body: body) } diff --git a/packages/core/ios/Sources/DdLogsImplementation.swift b/packages/core/ios/Sources/DdLogsImplementation.swift index 264a3d0b3..fe3fde092 100644 --- a/packages/core/ios/Sources/DdLogsImplementation.swift +++ b/packages/core/ios/Sources/DdLogsImplementation.swift @@ -5,7 +5,9 @@ */ import Foundation +import DatadogInternal import DatadogLogs +import DatadogCore @objc public class DdLogsImplementation: NSObject { @@ -20,7 +22,10 @@ public class DdLogsImplementation: NSObject { @objc public override convenience init() { - self.init({ DatadogSDKWrapper.shared.createLogger() }, { DatadogSDKWrapper.shared.isInitialized() }) + self.init( + { DatadogLogs.Logger.create(with: DatadogSDKWrapper.shared.loggerConfiguration) }, + { Datadog.isInitialized() } + ) } @objc diff --git a/packages/core/ios/Sources/DdSdkImplementation.swift b/packages/core/ios/Sources/DdSdkImplementation.swift index 6d060dc31..2f66f868c 100644 --- a/packages/core/ios/Sources/DdSdkImplementation.swift +++ b/packages/core/ios/Sources/DdSdkImplementation.swift @@ -133,40 +133,41 @@ public class DdSdkImplementation: NSObject { public func sendTelemetryLog(message: NSString, attributes: NSDictionary, config: NSDictionary, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { let castedAttributes = castAttributesToSwift(attributes) let castedConfig = castAttributesToSwift(config) - DatadogSDKWrapper.shared.sendTelemetryLog(message: message as String, attributes: castedAttributes, config: castedConfig) + DdTelemetry.sendTelemetryLog(message: message as String, attributes: castedAttributes, config: castedConfig) resolve(nil) } @objc public func telemetryDebug(message: NSString, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { - DatadogSDKWrapper.shared.telemetryDebug(id: "datadog_react_native:\(message)", message: message as String) + DdTelemetry.telemetryDebug(id: "datadog_react_native:\(message)", message: message as String) resolve(nil) } @objc public func telemetryError(message: NSString, stack: NSString, kind: NSString, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { - DatadogSDKWrapper.shared.telemetryError(id: "datadog_react_native:\(String(describing: kind)):\(message)", message: message as String, kind: kind as String, stack: stack as String) + DdTelemetry.telemetryError(id: "datadog_react_native:\(String(describing: kind)):\(message)", message: message as String, kind: kind as String, stack: stack as String) resolve(nil) } - + @objc public func consumeWebviewEvent(message: NSString, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { do{ try DatadogSDKWrapper.shared.sendWebviewMessage(body: message) } catch { - DatadogSDKWrapper.shared.telemetryError(id: "datadog_react_native:\(error.localizedDescription)", message: "The message being sent was:\(message)" as String, kind: "WebViewEventBridgeError" as String, stack: String(describing: error) as String) + DdTelemetry.telemetryError(id: "datadog_react_native:\(error.localizedDescription)", message: "The message being sent was:\(message)" as String, kind: "WebViewEventBridgeError" as String, stack: String(describing: error) as String) } + resolve(nil) } @objc public func clearAllData(resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { - DatadogSDKWrapper.shared.clearAllData() + Datadog.clearAllData() resolve(nil) } func overrideReactNativeTelemetry(rnConfiguration: DdSdkConfiguration) -> Void { - DatadogSDKWrapper.shared.overrideTelemetryConfiguration( + DdTelemetry.overrideTelemetryConfiguration( initializationType: rnConfiguration.configurationForTelemetry?.initializationType as? String, reactNativeVersion: rnConfiguration.configurationForTelemetry?.reactNativeVersion as? String, reactVersion: rnConfiguration.configurationForTelemetry?.reactVersion as? String, diff --git a/packages/core/ios/Sources/DdSdkNativeInitialization.swift b/packages/core/ios/Sources/DdSdkNativeInitialization.swift index d68d89d06..7b0f1be19 100644 --- a/packages/core/ios/Sources/DdSdkNativeInitialization.swift +++ b/packages/core/ios/Sources/DdSdkNativeInitialization.swift @@ -30,12 +30,13 @@ public class DdSdkNativeInitialization: NSObject { } internal func initialize(sdkConfiguration: DdSdkConfiguration) { - // TODO: see if this `if` is still needed - if DatadogSDKWrapper.shared.isInitialized() { - // Initializing the SDK twice results in Global.rum and - // Global.sharedTracer to be set to no-op instances + if Datadog.isInitialized(instanceName: CoreRegistry.defaultInstanceName) { + // Initializing the SDK twice results in Global.rum and Global.sharedTracer to be set to no-op instances consolePrint("Datadog SDK is already initialized, skipping initialization.", .debug) - DatadogSDKWrapper.shared.telemetryDebug(id: "datadog_react_native: RN SDK was already initialized in native", message: "RN SDK was already initialized in native") + DdTelemetry.telemetryDebug( + id: "datadog_react_native: RN SDK was already initialized in native", + message: "RN SDK was already initialized in native" + ) RUMMonitor.shared().currentSessionID { sessionId in guard let id = sessionId else { return } @@ -78,19 +79,17 @@ public class DdSdkNativeInitialization: NSObject { func enableFeatures(sdkConfiguration: DdSdkConfiguration) { let rumConfig = buildRUMConfiguration(configuration: sdkConfiguration) - DatadogSDKWrapper.shared.enableRUM(with: rumConfig) + RUM.enable(with: rumConfig) let logsConfig = buildLogsConfiguration(configuration: sdkConfiguration) - DatadogSDKWrapper.shared.enableLogs(with: logsConfig) + Logs.enable(with: logsConfig) let traceConfig = buildTraceConfiguration(configuration: sdkConfiguration) - DatadogSDKWrapper.shared.enableTrace(with: traceConfig) + Trace.enable(with: traceConfig) if sdkConfiguration.nativeCrashReportEnabled ?? false { - DatadogSDKWrapper.shared.enableCrashReporting() + CrashReporting.enable() } - - DatadogSDKWrapper.shared.enableWebviewTracking() } func buildSDKConfiguration(configuration: DdSdkConfiguration, defaultAppVersion: String = getDefaultAppVersion()) -> Datadog.Configuration { diff --git a/packages/core/ios/Sources/DdTelemetry.swift b/packages/core/ios/Sources/DdTelemetry.swift new file mode 100644 index 000000000..cb1896db8 --- /dev/null +++ b/packages/core/ios/Sources/DdTelemetry.swift @@ -0,0 +1,49 @@ + +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2025 Datadog, Inc. + */ +import DatadogCore +import DatadogInternal + +public class DdTelemetry { + public static func sendTelemetryLog(message: String, attributes: [String: any Encodable], config: [String: any Encodable]) { + let id = (config["onlyOnce"] as? Bool) == true ? message : UUID().uuidString + CoreRegistry.default.telemetry.debug(id: id, message: message, attributes: attributes) + } + + public static func telemetryDebug(id: String, message: String) { + return Datadog._internal.telemetry.debug(id: id, message: message) + } + + public static func telemetryError(id: String, message: String, kind: String?, stack: String?) { + return Datadog._internal.telemetry.error(id: id, message: message, kind: kind, stack: stack) + } + + public static func overrideTelemetryConfiguration( + initializationType: String? = nil, + reactNativeVersion: String? = nil, + reactVersion: String? = nil, + trackCrossPlatformLongTasks: Bool? = nil, + trackErrors: Bool? = nil, + trackInteractions: Bool? = nil, + trackLongTask: Bool? = nil, + trackNativeErrors: Bool? = nil, + trackNativeLongTasks: Bool? = nil, + trackNetworkRequests: Bool? = nil + ) { + CoreRegistry.default.telemetry.configuration( + initializationType: initializationType, + reactNativeVersion: reactNativeVersion, + reactVersion: reactVersion, + trackCrossPlatformLongTasks: trackCrossPlatformLongTasks, + trackErrors: trackErrors, + trackLongTask: trackLongTask, + trackNativeErrors: trackNativeErrors, + trackNativeLongTasks: trackNativeLongTasks, + trackNetworkRequests: trackNetworkRequests, + trackUserInteractions: trackInteractions + ) + } +} diff --git a/packages/core/ios/Tests/DatadogSdkWrapperTests.swift b/packages/core/ios/Tests/DatadogSdkWrapperTests.swift index 4811b4c1d..a445b2bbe 100644 --- a/packages/core/ios/Tests/DatadogSdkWrapperTests.swift +++ b/packages/core/ios/Tests/DatadogSdkWrapperTests.swift @@ -8,22 +8,23 @@ import XCTest @testable import DatadogSDKReactNative import DatadogTrace import DatadogInternal - +import DatadogRUM +import DatadogLogs internal class DatadogSdkWrapperTests: XCTestCase { override func setUp() { super.setUp() - DatadogSDKWrapper.shared.setCoreInstance(core: nil) - DatadogSDKWrapper.shared.onCoreInitializedListeners = [] + DatadogSDKWrapper.shared.onSdkInitializedListeners = [] } - func testItSetsCoreUsedForFeatures() { + func testOverrideCoreRegistryDefault() { let coreMock = MockDatadogCore() - DatadogSDKWrapper.shared.setCoreInstance(core: coreMock) + CoreRegistry.register(default: coreMock) + defer { CoreRegistry.unregisterDefault() } - DatadogSDKWrapper.shared.enableTrace(with: .init()) - DatadogSDKWrapper.shared.enableRUM(with: .init(applicationID: "app-id")) - DatadogSDKWrapper.shared.enableLogs(with: .init()) + Trace.enable(with: .init()) + RUM.enable(with: .init(applicationID: "app-id")) + Logs.enable(with: .init()) XCTAssertNotNil(coreMock.features["tracing"]) XCTAssertNotNil(coreMock.features["rum"]) diff --git a/packages/core/ios/Tests/DdSdkTests.swift b/packages/core/ios/Tests/DdSdkTests.swift index edbbba4dd..bcc3252a8 100644 --- a/packages/core/ios/Tests/DdSdkTests.swift +++ b/packages/core/ios/Tests/DdSdkTests.swift @@ -34,8 +34,7 @@ class DdSdkTests: XCTestCase { private func mockReject(args _: String?, arg _: String?, err _: Error?) {} override func tearDown() { - DatadogSDKWrapper.shared.setCoreInstance(core: nil) - DatadogSDKWrapper.shared.onCoreInitializedListeners = [] + DatadogSDKWrapper.shared.onSdkInitializedListeners = [] Datadog.internalFlushAndDeinitialize() } @@ -84,11 +83,10 @@ class DdSdkTests: XCTestCase { let bridge = DispatchQueueMock() let mockJSRefreshRateMonitor = MockJSRefreshRateMonitor() let mockListener = MockOnCoreInitializedListener() - DatadogSDKWrapper.shared.addOnCoreInitializedListener(listener: mockListener.listener) + DatadogSDKWrapper.shared.addOnSdkInitializedListener(listener: mockListener.listener) - let expectation = self.expectation(description: "Core is set when promise resolves") + let expectation = self.expectation(description: "Listener is called when promise resolves") func mockPromiseResolve(_: Any?) { - XCTAssertNotNil(mockListener.core) expectation.fulfill() } @@ -276,9 +274,9 @@ class DdSdkTests: XCTestCase { } func testSDKInitializationWithOnInitializedCallback() { - var coreFromCallback: DatadogCoreProtocol? = nil - DatadogSDKWrapper.shared.addOnCoreInitializedListener(listener: { core in - coreFromCallback = core + var isInitialized = false + DatadogSDKWrapper.shared.addOnSdkInitializedListener(listener: { + isInitialized = Datadog.isInitialized() }) DdSdkImplementation( @@ -293,14 +291,16 @@ class DdSdkTests: XCTestCase { reject: mockReject ) - XCTAssertNotNil(coreFromCallback) + XCTAssertTrue(isInitialized) } func testEnableAllFeatures() { let core = MockDatadogCore() + CoreRegistry.register(default: core) + defer { CoreRegistry.unregisterDefault() } + let configuration: DdSdkConfiguration = .mockAny() - DatadogSDKWrapper.shared.setCoreInstance(core: core) DdSdkNativeInitialization().enableFeatures( sdkConfiguration: configuration ) @@ -479,9 +479,11 @@ class DdSdkTests: XCTestCase { func testBuildConfigurationWithCrashReport() { let core = MockDatadogCore() + CoreRegistry.register(default: core) + defer { CoreRegistry.unregisterDefault() } + let configuration: DdSdkConfiguration = .mockAny(nativeCrashReportEnabled: true) - DatadogSDKWrapper.shared.setCoreInstance(core: core) DdSdkNativeInitialization().enableFeatures( sdkConfiguration: configuration ) @@ -1233,6 +1235,9 @@ class DdSdkTests: XCTestCase { func testConfigurationTelemetryOverride() throws { let core = MockDatadogCore() + CoreRegistry.register(default: core) + defer { CoreRegistry.unregisterDefault() } + let configuration: DdSdkConfiguration = .mockAny( nativeCrashReportEnabled: false, nativeLongTaskThresholdMs: 0.0, @@ -1244,7 +1249,6 @@ class DdSdkTests: XCTestCase { ] ) - DatadogSDKWrapper.shared.setCoreInstance(core: core) DdSdkImplementation().overrideReactNativeTelemetry(rnConfiguration: configuration) XCTAssertEqual(core.configuration?.initializationType, "LEGACY") @@ -1313,11 +1317,12 @@ class DdSdkTests: XCTestCase { XCTAssertTrue(bridge.isSameQueue(queue: mockJSRefreshRateMonitor.jsQueue!)) } - func testCallsOnCoreInitializedListeners() throws { + func testCallsOnSdkInitializedListeners() throws { let bridge = DispatchQueueMock() let mockJSRefreshRateMonitor = MockJSRefreshRateMonitor() let mockListener = MockOnCoreInitializedListener() - DatadogSDKWrapper.shared.addOnCoreInitializedListener(listener: mockListener.listener) + + DatadogSDKWrapper.shared.addOnSdkInitializedListener(listener: mockListener.listener) DdSdkImplementation( mainDispatchQueue: DispatchQueueMock(), @@ -1331,23 +1336,7 @@ class DdSdkTests: XCTestCase { reject: mockReject ) - XCTAssertNotNil(mockListener.core) - } - - func testConsumeWebviewEvent() throws { - let configuration: DdSdkConfiguration = .mockAny() - let core = MockDatadogCore() - - DatadogSDKWrapper.shared.setCoreInstance(core: core) - DdSdkNativeInitialization().enableFeatures( - sdkConfiguration: configuration - ) - - DdSdkImplementation().consumeWebviewEvent( - message: "{\"eventType\":\"rum\",\"event\":{\"blabla\":\"custom message\"}}", - resolve: mockResolve, reject: mockReject) - - XCTAssertNotNil(core.baggages["browser-rum-event"]) + XCTAssertTrue(mockListener.called) } func testInitialResourceThreshold() { @@ -1601,9 +1590,8 @@ extension DdSdkImplementation { } class MockOnCoreInitializedListener { - var core: DatadogCoreProtocol? - - func listener(core: DatadogCoreProtocol) { - self.core = core + var called = false + func listener() { + self.called = true } } diff --git a/packages/react-native-session-replay/ios/Sources/DdSessionReplayImplementation.swift b/packages/react-native-session-replay/ios/Sources/DdSessionReplayImplementation.swift index e576cb0ae..3fe9413c2 100644 --- a/packages/react-native-session-replay/ios/Sources/DdSessionReplayImplementation.swift +++ b/packages/react-native-session-replay/ios/Sources/DdSessionReplayImplementation.swift @@ -6,6 +6,7 @@ import Foundation @_spi(Internal) import DatadogSessionReplay +import DatadogCore import DatadogInternal import DatadogSDKReactNative import React @@ -64,38 +65,21 @@ public class DdSessionReplayImplementation: NSObject { sessionReplayConfiguration.setAdditionalNodeRecorders([ RCTTextViewRecorder(uiManager: uiManager, fabricWrapper: fabricWrapper) ]) - - if let core = DatadogSDKWrapper.shared.getCoreInstance() { - sessionReplay.enable( - with: sessionReplayConfiguration, - in: core - ) - } else { - consolePrint("Core instance was not found when initializing Session Replay.", .critical) - } + + sessionReplay.enable(with: sessionReplayConfiguration, in: CoreRegistry.default) resolve(nil) } @objc public func startRecording(resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) -> Void { - if let core = DatadogSDKWrapper.shared.getCoreInstance() { - sessionReplay.startRecording(in: core) - } else { - consolePrint("Core instance was not found when calling startRecording in Session Replay.", .critical) - } - + sessionReplay.startRecording(in: CoreRegistry.default) resolve(nil) } @objc public func stopRecording(resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) -> Void { - if let core = DatadogSDKWrapper.shared.getCoreInstance() { - sessionReplay.stopRecording(in: core) - } else { - consolePrint("Core instance was not found when calling stopRecording in Session Replay.", .critical) - } - + sessionReplay.stopRecording(in: CoreRegistry.default) resolve(nil) } diff --git a/packages/react-native-session-replay/ios/Tests/DdSessionReplayTests.swift b/packages/react-native-session-replay/ios/Tests/DdSessionReplayTests.swift index aa9102850..067a11df0 100644 --- a/packages/react-native-session-replay/ios/Tests/DdSessionReplayTests.swift +++ b/packages/react-native-session-replay/ios/Tests/DdSessionReplayTests.swift @@ -35,7 +35,11 @@ internal class DdSessionReplayTests: XCTestCase { override func setUp() { super.setUp() let mockDatadogCore = MockDatadogCore() - DatadogSDKWrapper.shared.setCoreInstance(core: mockDatadogCore) + CoreRegistry.register(default: mockDatadogCore) + } + + override func tearDown() { + CoreRegistry.unregisterDefault() } func testEnablesSessionReplayWithZeroReplaySampleRate() { diff --git a/packages/react-native-webview/ios/Sources/RCTDatadogWebViewTracking.swift b/packages/react-native-webview/ios/Sources/RCTDatadogWebViewTracking.swift index 97fd835b5..45f11e452 100644 --- a/packages/react-native-webview/ios/Sources/RCTDatadogWebViewTracking.swift +++ b/packages/react-native-webview/ios/Sources/RCTDatadogWebViewTracking.swift @@ -8,26 +8,27 @@ import WebKit import DatadogWebViewTracking import DatadogSDKReactNative import DatadogCore +import DatadogInternal @objc public class RCTDatadogWebViewTracking: NSObject { var webView: RCTDatadogWebView? = nil var allowedHosts: Set = Set() - var coreListener: OnCoreInitializedListener? + var onSdkInitializedListener: OnSdkInitializedListener? public override init() { super.init() - self.coreListener = { [weak self] (core: DatadogCoreProtocol) in + self.onSdkInitializedListener = { [weak self] in guard let strongSelf = self, let webView = strongSelf.webView else { return } strongSelf.enableWebViewTracking( webView: webView, allowedHosts: strongSelf.allowedHosts, - core: core + core: CoreRegistry.default ) } } - + /** Enables tracking on the given WebView. @@ -42,15 +43,16 @@ import DatadogCore guard !webView.isTrackingEnabled else { return } - if let core = DatadogSDKWrapper.shared.getCoreInstance() { - enableWebViewTracking(webView: webView, allowedHosts: allowedHosts, core: core) - } else if let coreListener = self.coreListener { - DatadogSDKWrapper.shared.addOnCoreInitializedListener(listener: coreListener) + if CoreRegistry.isRegistered(instanceName: CoreRegistry.defaultInstanceName) { + enableWebViewTracking(webView: webView, allowedHosts: allowedHosts, core: CoreRegistry.default) + } else if let onSdkInitializedListener = self.onSdkInitializedListener { + DatadogSDKWrapper.shared.addOnSdkInitializedListener(listener: onSdkInitializedListener) } else { // TODO: Report initialization problem } } + private func enableWebViewTracking( webView: RCTDatadogWebView, allowedHosts: Set, diff --git a/packages/react-native-webview/ios/Tests/DatadogSDKReactNativeWebViewTests.swift b/packages/react-native-webview/ios/Tests/DatadogSDKReactNativeWebViewTests.swift index 7ea36bda8..20f1b84fa 100644 --- a/packages/react-native-webview/ios/Tests/DatadogSDKReactNativeWebViewTests.swift +++ b/packages/react-native-webview/ios/Tests/DatadogSDKReactNativeWebViewTests.swift @@ -17,7 +17,11 @@ internal class DatadogSDKReactNativeWebViewTests: XCTestCase { override func setUp() { super.setUp() let mockDatadogCore = MockDatadogCore() - DatadogSDKWrapper.shared.setCoreInstance(core: mockDatadogCore) + CoreRegistry.register(default: mockDatadogCore) + } + + override func tearDown() { + CoreRegistry.unregisterDefault() } func testDatadogWebViewManagerReturnsDatadogWebView() { @@ -41,9 +45,10 @@ internal class DatadogSDKReactNativeWebViewTests: XCTestCase { XCTAssertFalse(view.isTrackingEnabled) } - func testDatadogWebViewTrackingIsDisabledIfCoreIsNotReady() { + func testDatadogWebViewTrackingIsDisabledIfSdkIsNotInitialized() { // Given - DatadogSDKWrapper.shared.setCoreInstance(core: nil) + CoreRegistry.unregisterDefault() + let viewManager = RCTDatadogWebViewManager() let allowedHosts = NSArray(objects: "example1.com", "example2.com") @@ -82,7 +87,7 @@ internal class DatadogSDKReactNativeWebViewTests: XCTestCase { view.addSubview(WKWebView()) - DatadogSDKWrapper.shared.setCoreInstance(core: nil) + CoreRegistry.unregisterDefault() // Given let selector = NSSelectorFromString("setupDatadogWebView:view:") @@ -92,7 +97,6 @@ internal class DatadogSDKReactNativeWebViewTests: XCTestCase { XCTAssertFalse(view.isTrackingEnabled) // When - DatadogSDKWrapper.shared.setCoreInstance(core: MockDatadogCore()) DatadogSDKWrapper.shared.callInitialize() let expectation = self.expectation(description: "WebView tracking is enabled through the listener.") From eb0fb912a3a9b833738a2d4b84161bdbead86f9c Mon Sep 17 00:00:00 2001 From: Sergio Barrio Date: Mon, 8 Sep 2025 15:27:17 +0200 Subject: [PATCH 011/410] Bump native SDK dependencies to 3.0.0 --- benchmarks/android/app/build.gradle | 2 +- benchmarks/ios/Podfile.lock | 174 ++++++++-------- bump-native-dd-sdk.sh | 2 +- example-new-architecture/ios/Podfile.lock | 152 +++++++------- example/ios/Podfile.lock | 188 +++++++++--------- packages/core/DatadogSDKReactNative.podspec | 12 +- packages/core/android/build.gradle | 10 +- ...DatadogSDKReactNativeSessionReplay.podspec | 2 +- .../android/build.gradle | 4 +- .../DatadogSDKReactNativeWebView.podspec | 4 +- .../react-native-webview/android/build.gradle | 2 +- 11 files changed, 276 insertions(+), 276 deletions(-) diff --git a/benchmarks/android/app/build.gradle b/benchmarks/android/app/build.gradle index 86d26791f..7dc4b3707 100644 --- a/benchmarks/android/app/build.gradle +++ b/benchmarks/android/app/build.gradle @@ -129,5 +129,5 @@ dependencies { // Benchmark tools from dd-sdk-android are used for vitals recording // Remember to bump thid alongside the main dd-sdk-android dependencies - implementation("com.datadoghq:dd-sdk-android-benchmark-internal:2.25.0") + implementation("com.datadoghq:dd-sdk-android-benchmark-internal:3.0.0") } diff --git a/benchmarks/ios/Podfile.lock b/benchmarks/ios/Podfile.lock index ebeb51d71..d27610b2c 100644 --- a/benchmarks/ios/Podfile.lock +++ b/benchmarks/ios/Podfile.lock @@ -1,22 +1,22 @@ PODS: - boost (1.84.0) - - DatadogCore (2.30.0): - - DatadogInternal (= 2.30.0) - - DatadogCrashReporting (2.30.0): - - DatadogInternal (= 2.30.0) + - DatadogCore (3.0.0): + - DatadogInternal (= 3.0.0) + - DatadogCrashReporting (3.0.0): + - DatadogInternal (= 3.0.0) - PLCrashReporter (~> 1.12.0) - - DatadogInternal (2.30.0) - - DatadogLogs (2.30.0): - - DatadogInternal (= 2.30.0) - - DatadogRUM (2.30.0): - - DatadogInternal (= 2.30.0) + - DatadogInternal (3.0.0) + - DatadogLogs (3.0.0): + - DatadogInternal (= 3.0.0) + - DatadogRUM (3.0.0): + - DatadogInternal (= 3.0.0) - DatadogSDKReactNative (2.12.1): - - DatadogCore (= 2.30.0) - - DatadogCrashReporting (= 2.30.0) - - DatadogLogs (= 2.30.0) - - DatadogRUM (= 2.30.0) - - DatadogTrace (= 2.30.0) - - DatadogWebViewTracking (= 2.30.0) + - DatadogCore (= 3.0.0) + - DatadogCrashReporting (= 3.0.0) + - DatadogLogs (= 3.0.0) + - DatadogRUM (= 3.0.0) + - DatadogTrace (= 3.0.0) + - DatadogWebViewTracking (= 3.0.0) - DoubleConversion - glog - hermes-engine @@ -39,7 +39,7 @@ PODS: - Yoga - DatadogSDKReactNativeSessionReplay (2.12.1): - DatadogSDKReactNative - - DatadogSessionReplay (= 2.30.0) + - DatadogSessionReplay (= 3.0.0) - DoubleConversion - glog - hermes-engine @@ -61,9 +61,9 @@ PODS: - ReactCommon/turbomodule/core - Yoga - DatadogSDKReactNativeWebView (2.12.1): - - DatadogInternal (= 2.30.0) + - DatadogInternal (= 3.0.0) - DatadogSDKReactNative - - DatadogWebViewTracking (= 2.30.0) + - DatadogWebViewTracking (= 3.0.0) - DoubleConversion - glog - hermes-engine @@ -84,13 +84,13 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - DatadogSessionReplay (2.30.0): - - DatadogInternal (= 2.30.0) - - DatadogTrace (2.30.0): - - DatadogInternal (= 2.30.0) + - DatadogSessionReplay (3.0.0): + - DatadogInternal (= 3.0.0) + - DatadogTrace (3.0.0): + - DatadogInternal (= 3.0.0) - OpenTelemetrySwiftApi (= 1.13.1) - - DatadogWebViewTracking (2.30.0): - - DatadogInternal (= 2.30.0) + - DatadogWebViewTracking (3.0.0): + - DatadogInternal (= 3.0.0) - DoubleConversion (1.1.6) - fast_float (6.1.4) - FBLazyVector (0.78.2) @@ -2069,17 +2069,17 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: boost: 7e761d76ca2ce687f7cc98e698152abd03a18f90 - DatadogCore: 5c01290a3b60b27bf49aa958f2e339c738364d9e - DatadogCrashReporting: 11286d48ab61baeb2b41b945c7c0d4ef23db317d - DatadogInternal: 7aeb48e254178a0c462c3953dc0a8a8d64499a93 - DatadogLogs: 4324739de62a6059e07d70bf6ceceed78764edeb - DatadogRUM: f36949a38285f3b240a7be577d425f8518e087d4 - DatadogSDKReactNative: 6bb6b9db6669f38edafa1354eda6d24b0285b385 - DatadogSDKReactNativeSessionReplay: aaf36cb0f1dded23a57ece8782419b1ee2b6ba4b - DatadogSDKReactNativeWebView: c631bcd2d7fa712e291341128e1b8dafce0926b3 - DatadogSessionReplay: 682c4d56b88cdb4d94e20c6db13f9630a725b178 - DatadogTrace: bfea32b6ed2870829629a9296cf526221493cc3e - DatadogWebViewTracking: 78c20d8e5f1ade506f4aadaec5690c1a63283fe2 + DatadogCore: 0c39ba4ef3dee02071f235f2fb56af7e29f4ceb0 + DatadogCrashReporting: 604e65e524cb2fc22cda39fd9c9ea5fd3bddf24e + DatadogInternal: 442673a7fb5329299b489b91ed705aa2085380f7 + DatadogLogs: 0146a0e3140ba62fd42729593c0910d692aea5fd + DatadogRUM: f97fbd0290ecec5bc952fb03a6a1ae068649243f + DatadogSDKReactNative: 241bf982c16ceff03d94a58e6d005e55c47b99c6 + DatadogSDKReactNativeSessionReplay: bba36092686e3183e97c1a0c7f4ca8142582ae28 + DatadogSDKReactNativeWebView: 6da060df20e235abac533e582d9fc2b3a5070840 + DatadogSessionReplay: 0357b911c6ab42c77c93b29761ecc20d9282e971 + DatadogTrace: d714e7c456e612b7f171483d08086a59aa1c4213 + DatadogWebViewTracking: fb9591c09fca82b143f3843a7164a7e782175c53 DoubleConversion: cb417026b2400c8f53ae97020b2be961b59470cb fast_float: 06eeec4fe712a76acc9376682e4808b05ce978b6 FBLazyVector: e32d34492c519a2194ec9d7f5e7a79d11b73f91c @@ -2088,70 +2088,70 @@ SPEC CHECKSUMS: hermes-engine: 2771b98fb813fdc6f92edd7c9c0035ecabf9fee7 OpenTelemetrySwiftApi: aaee576ed961e0c348af78df58b61300e95bd104 PLCrashReporter: db59ef96fa3d25f3650040d02ec2798cffee75f2 - RCT-Folly: 36fe2295e44b10d831836cc0d1daec5f8abcf809 + RCT-Folly: e78785aa9ba2ed998ea4151e314036f6c49e6d82 RCTDeprecation: be794de7dc6ed8f9f7fbf525f86e7651b8b68746 RCTRequired: a83787b092ec554c2eb6019ff3f5b8d125472b3b RCTTypeSafety: 48ad3c858926b1c46f46a81a58822b476e178e2c React: 3b5754191f1b65f1dbc52fbea7959c3d2d9e39c9 React-callinvoker: 6beeaf4c7db11b6cc953fac45f2c76e3fb125013 - React-Core: 88e817c42de035378cc71e009193b9a044d3f595 - React-CoreModules: dcf764d71efb4f75d38fcae8d4513b6729f49360 - React-cxxreact: 8cdcc937c5fbc406fe843a381102fd69440ca78a + React-Core: 8a10ac9de53373a3ecb5dfcbcf56df1d3dad0861 + React-CoreModules: af6999b35c7c01b0e12b59d27f3e054e13da43b1 + React-cxxreact: 833f00155ce8c2fda17f6d286f8eaeff2ececc69 React-debug: 440175830c448e7e53e61ebb8d8468c3256b645e - React-defaultsnativemodule: 4824bcd7b96ee2d75c28b1ca21f58976867f5535 - React-domnativemodule: a421118b475618961cf282e8ea85347cc9bb453c - React-Fabric: 6ac7de06009eb96b609a770b17abba6e460b5f45 - React-FabricComponents: e3bc2680a5a9a4917ff0c8d7f390688c30ef753c - React-FabricImage: 8bad558dec7478077974caa96acc79692d6b71f5 + React-defaultsnativemodule: a970effe18fe50bdbbb7115c3297f873b666d0d4 + React-domnativemodule: 45f886342a724e61531b18fba1859bb6782e5d62 + React-Fabric: 69f1881f2177a8512304a64157943548ab6df0cf + React-FabricComponents: f54111c8e2439fc273ab07483e3a7054ca1e75af + React-FabricImage: 9ad2619dfe8c386d79e8aaa87da6e8f018ab9592 React-featureflags: b9cf9b35baca1c7f20c06a104ffc325a02752faa - React-featureflagsnativemodule: dc93d81da9f41f7132e24455ec8b4b60802fd5b0 - React-graphics: aaa5a38bea15d7b895b210d95d554af45a07002a - React-hermes: 08ad9fb832d1b9faef391be17309aa6a69fad23b - React-idlecallbacksnativemodule: aacea33ef6c511a9781f9286cc7cdf93f39bba14 - React-ImageManager: c596c3b658c9c14607f9183ed0f635c8dd77987c - React-jserrorhandler: 987609b2f16b7d79d63fcd621bf0110dd7400b35 - React-jsi: afa286d7e0c102c2478dc420d4f8935e13c973fc - React-jsiexecutor: 08f5b512b4db9e2f147416d60a0a797576b9cfef - React-jsinspector: 5a94bcae66e3637711c4d96a00038ab9ec935bf5 - React-jsinspectortracing: a12589a0adbb2703cbc4380dabe9a58800810923 - React-jsitracing: 0b1a403d7757cec66b7dd8b308d04db85eef75f3 - React-logger: 304814ae37503c8eb54359851cc55bd4f936b39c - React-Mapbuffer: b588d1ca18d2ce626f868f04ab12d8b1f004f12c - React-microtasksnativemodule: 11831d070aa47755bb5739069eb04ec621fec548 - react-native-config: 3367df9c1f25bb96197007ec531c7087ed4554c3 - react-native-safe-area-context: 9b169299f9dc95f1d7fe1dd266fde53bd899cd0c - react-native-slider: 27263d134d55db948a4706f1e47d0ec88fb354dd - react-native-webview: be9957759cb73cb64f2ed5359e32a85f1f5bdff8 - React-NativeModulesApple: 79a4404ac301b40bec3b367879c5e9a9ce81683c - React-perflogger: 0ea25c109dba33d47dec36b2634bf7ea67c1a555 - React-performancetimeline: f74480de6efbcd8541c34317c0baedb433f27296 + React-featureflagsnativemodule: 7f1bc76d1d2c5bede5e753b8d188dbde7c59b12f + React-graphics: 069e0d0b31ed1e80feb023ad4f7e97f00e84f7b9 + React-hermes: 63df5ac5a944889c8758a6213b39ed825863adb7 + React-idlecallbacksnativemodule: 4c700bd7c0012adf904929075a79418b828b5ffc + React-ImageManager: 5d1ba8a7bae44ebba43fc93da64937c713d42941 + React-jserrorhandler: 0defd58f8bb797cdd0a820f733bf42d8bee708ce + React-jsi: 99d6207ec802ad73473a0dad3c9ad48cd98463f6 + React-jsiexecutor: 8c8097b4ba7e7f480582d6e6238b01be5dcc01c0 + React-jsinspector: ea148ec45bc7ff830e443383ea715f9780c15934 + React-jsinspectortracing: 46bb2841982f01e7b63eaab98140fa1de5b2a1db + React-jsitracing: c1063fc2233960d1c8322291e74bca51d25c10d7 + React-logger: 763728cf4eebc9c5dc9bfc3649e22295784f69f3 + React-Mapbuffer: 63278529b5cf531a7eaf8fc71244fabb062ca90c + React-microtasksnativemodule: 6a39463c32ce831c4c2aa8469273114d894b6be9 + react-native-config: 644074ab88db883fcfaa584f03520ec29589d7df + react-native-safe-area-context: afcc2e2b3e78ae8ef90d81e658aacee34ebc27ea + react-native-slider: 310d3f89edd6ca8344a974bfe83a29a3fbb60e5a + react-native-webview: 80ef603d1df42e24fdde765686fbb9b8a6ecd554 + React-NativeModulesApple: fd0545efbb7f936f78edd15a6564a72d2c34bb32 + React-perflogger: 5f8fa36a8e168fb355efe72099efe77213bc2ac6 + React-performancetimeline: 8c0ecfa1ae459cc5678a65f95ac3bf85644d6feb React-RCTActionSheet: 2ef95837e89b9b154f13cd8401f9054fc3076aff - React-RCTAnimation: 33d960d7f58a81779eea6dea47ad0364c67e1517 - React-RCTAppDelegate: 85c13403fd6f6b6cc630428d52bd8bd76a670dc9 - React-RCTBlob: 74c986a02d951931d2f6ed0e07ed5a7eb385bfc0 - React-RCTFabric: 384a8fea4f22fc0f21299d771971862883ba630a - React-RCTFBReactNativeSpec: eb1c3ec5149f76133593a516ff9d5efe32ebcecd - React-RCTImage: 2c58b5ddeb3c65e52f942bbe13ff9c59bd649b09 - React-RCTLinking: b6b14f8a3e62c02fc627ac4f3fb0c7bd941f907c - React-RCTNetwork: 1d050f2466c1541b339587d46f78d5eee218d626 - React-RCTSettings: 8148f6be0ccc0cfe6e313417ebf8a479caaa2146 - React-RCTText: 64114531ad1359e4e02a4a8af60df606dbbabc25 - React-RCTVibration: f4859417a7dd859b6bf18b1aba897e52beb72ef6 + React-RCTAnimation: 46abefd5acfda7e6629f9e153646deecc70babd2 + React-RCTAppDelegate: 7e58e0299e304cceee3f7019fa77bc6990f66b22 + React-RCTBlob: f68c63a801ef1d27e83c4011e3b083cc86a200d7 + React-RCTFabric: c59f41d0c4edbaac8baa232731ca09925ae4dda7 + React-RCTFBReactNativeSpec: 3240b9b8d792aa4be0fb85c9898fc183125ba8de + React-RCTImage: 34e0bba1507e55f1c614bd759eb91d9be48c8c5b + React-RCTLinking: a0b6c9f4871c18b0b81ea952f43e752718bd5f1d + React-RCTNetwork: bdafd661ac2b20d23b779e45bf7ac3e4c8bd1b60 + React-RCTSettings: 98aa5163796f43789314787b584a84eba47787a9 + React-RCTText: 424a274fc9015b29de89cf3cbcdf4dd85dd69f83 + React-RCTVibration: 92d9875a955b0adb34b4b773528fdbbbc5addd6c React-rendererconsistency: 5ac4164ec18cfdd76ed5f864dbfdc56a5a948bc9 - React-rendererdebug: 3dc1d97bbee0c0c13191e501a96ed9325bbd920e + React-rendererdebug: 710dbd7990e355852c786aa6bc7753f6028f357a React-rncore: 0bace3b991d8843bb5b57c5f2301ec6e9c94718b - React-RuntimeApple: 1e1e0a0c6086bc8c3b07e8f1a2f6ca99b50419a0 - React-RuntimeCore: d39322c59bef2a4b343fda663d20649f29f57fcc + React-RuntimeApple: 701ec44a8b5d863ee9b6a2b2447b6a26bb6805a1 + React-RuntimeCore: a82767065b9a936b05e209dc6987bc1ea9eb5d2d React-runtimeexecutor: 876dfc1d8daa819dfd039c40f78f277c5a3e66a6 - React-RuntimeHermes: 44f5f2baf039f249b31ea4f3e224484fd1731e0e - React-runtimescheduler: 3b3c5b50743bb8743ca49b9e5a70c2c385f156e1 + React-RuntimeHermes: e7a051fd91cab8849df56ac917022ef6064ad621 + React-runtimescheduler: c544141f2124ee3d5f3d5bf0d69f4029a61a68b0 React-timing: 1ee3572c398f5579c9df5bf76aacddf5683ff74e - React-utils: 0cfb7c7fb37d4e5f31cc18ffc7426be0ae6bf907 - ReactAppDependencyProvider: b48473fe434569ff8f6cb6ed4421217ebcbda878 - ReactCodegen: 653a0d8532d8c7dab50c391392044d98e20c9f79 - ReactCommon: 547db015202a80a5b3e7e041586ea54c4a087180 - RNCPicker: ffbd7b9fc7c1341929e61dbef6219f7860f57418 - RNScreens: 0f01bbed9bd8045a8d58e4b46993c28c7f498f3c + React-utils: 18703928768cb37e70cf2efff09def12d74a399e + ReactAppDependencyProvider: 4893bde33952f997a323eb1a1ee87a72764018ff + ReactCodegen: da30aff1cea9b5993dcbc33bf1ef47a463c55194 + ReactCommon: 865ebe76504a95e115b6229dd00a31e56d2d4bfe + RNCPicker: cfb51a08c6e10357d9a65832e791825b0747b483 + RNScreens: 790123c4a28783d80a342ce42e8c7381bed62db1 SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 Yoga: e14bad835e12b6c7e2260fc320bd00e0f4b45add diff --git a/bump-native-dd-sdk.sh b/bump-native-dd-sdk.sh index 8545802db..67d99a0e7 100755 --- a/bump-native-dd-sdk.sh +++ b/bump-native-dd-sdk.sh @@ -23,7 +23,7 @@ podspec_files=( "packages/react-native-webview/DatadogSDKReactNativeWebView.podspec" ) -ios_pattern="('Datadog[^']+', '~> )[0-9.]+'" +ios_pattern="('Datadog[^']+', ')[0-9.]+'" android_pattern='(com\.datadoghq:dd-sdk-android-[^:"]+):[0-9.]+' if [[ "$sdk" == "ios" ]]; then diff --git a/example-new-architecture/ios/Podfile.lock b/example-new-architecture/ios/Podfile.lock index 18b046b1d..281513566 100644 --- a/example-new-architecture/ios/Podfile.lock +++ b/example-new-architecture/ios/Podfile.lock @@ -1,22 +1,22 @@ PODS: - boost (1.84.0) - - DatadogCore (2.30.0): - - DatadogInternal (= 2.30.0) - - DatadogCrashReporting (2.30.0): - - DatadogInternal (= 2.30.0) + - DatadogCore (3.0.0): + - DatadogInternal (= 3.0.0) + - DatadogCrashReporting (3.0.0): + - DatadogInternal (= 3.0.0) - PLCrashReporter (~> 1.12.0) - - DatadogInternal (2.30.0) - - DatadogLogs (2.30.0): - - DatadogInternal (= 2.30.0) - - DatadogRUM (2.30.0): - - DatadogInternal (= 2.30.0) + - DatadogInternal (3.0.0) + - DatadogLogs (3.0.0): + - DatadogInternal (= 3.0.0) + - DatadogRUM (3.0.0): + - DatadogInternal (= 3.0.0) - DatadogSDKReactNative (2.12.1): - - DatadogCore (= 2.30.0) - - DatadogCrashReporting (= 2.30.0) - - DatadogLogs (= 2.30.0) - - DatadogRUM (= 2.30.0) - - DatadogTrace (= 2.30.0) - - DatadogWebViewTracking (= 2.30.0) + - DatadogCore (= 3.0.0) + - DatadogCrashReporting (= 3.0.0) + - DatadogLogs (= 3.0.0) + - DatadogRUM (= 3.0.0) + - DatadogTrace (= 3.0.0) + - DatadogWebViewTracking (= 3.0.0) - DoubleConversion - glog - hermes-engine @@ -38,12 +38,12 @@ PODS: - ReactCommon/turbomodule/core - Yoga - DatadogSDKReactNative/Tests (2.12.1): - - DatadogCore (= 2.30.0) - - DatadogCrashReporting (= 2.30.0) - - DatadogLogs (= 2.30.0) - - DatadogRUM (= 2.30.0) - - DatadogTrace (= 2.30.0) - - DatadogWebViewTracking (= 2.30.0) + - DatadogCore (= 3.0.0) + - DatadogCrashReporting (= 3.0.0) + - DatadogLogs (= 3.0.0) + - DatadogRUM (= 3.0.0) + - DatadogTrace (= 3.0.0) + - DatadogWebViewTracking (= 3.0.0) - DoubleConversion - glog - hermes-engine @@ -64,11 +64,11 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - DatadogTrace (2.30.0): - - DatadogInternal (= 2.30.0) + - DatadogTrace (3.0.0): + - DatadogInternal (= 3.0.0) - OpenTelemetrySwiftApi (= 1.13.1) - - DatadogWebViewTracking (2.30.0): - - DatadogInternal (= 2.30.0) + - DatadogWebViewTracking (3.0.0): + - DatadogInternal (= 3.0.0) - DoubleConversion (1.1.6) - fast_float (6.1.4) - FBLazyVector (0.76.9) @@ -1850,14 +1850,14 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: boost: 1dca942403ed9342f98334bf4c3621f011aa7946 - DatadogCore: 5c01290a3b60b27bf49aa958f2e339c738364d9e - DatadogCrashReporting: 11286d48ab61baeb2b41b945c7c0d4ef23db317d - DatadogInternal: 7aeb48e254178a0c462c3953dc0a8a8d64499a93 - DatadogLogs: 4324739de62a6059e07d70bf6ceceed78764edeb - DatadogRUM: f36949a38285f3b240a7be577d425f8518e087d4 - DatadogSDKReactNative: b2ce73815472d5612bc6a7aa58a0b331294ce429 - DatadogTrace: bfea32b6ed2870829629a9296cf526221493cc3e - DatadogWebViewTracking: 78c20d8e5f1ade506f4aadaec5690c1a63283fe2 + DatadogCore: 0c39ba4ef3dee02071f235f2fb56af7e29f4ceb0 + DatadogCrashReporting: 604e65e524cb2fc22cda39fd9c9ea5fd3bddf24e + DatadogInternal: 442673a7fb5329299b489b91ed705aa2085380f7 + DatadogLogs: 0146a0e3140ba62fd42729593c0910d692aea5fd + DatadogRUM: f97fbd0290ecec5bc952fb03a6a1ae068649243f + DatadogSDKReactNative: 0a80aa75958d595a99be54d2838db53eda7a08af + DatadogTrace: d714e7c456e612b7f171483d08086a59aa1c4213 + DatadogWebViewTracking: fb9591c09fca82b143f3843a7164a7e782175c53 DoubleConversion: f16ae600a246532c4020132d54af21d0ddb2a385 fast_float: 06eeec4fe712a76acc9376682e4808b05ce978b6 FBLazyVector: 7605ea4810e0e10ae4815292433c09bf4324ba45 @@ -1866,62 +1866,62 @@ SPEC CHECKSUMS: hermes-engine: 9e868dc7be781364296d6ee2f56d0c1a9ef0bb11 OpenTelemetrySwiftApi: aaee576ed961e0c348af78df58b61300e95bd104 PLCrashReporter: db59ef96fa3d25f3650040d02ec2798cffee75f2 - RCT-Folly: 7b4f73a92ad9571b9dbdb05bb30fad927fa971e1 + RCT-Folly: ea9d9256ba7f9322ef911169a9f696e5857b9e17 RCTDeprecation: ebe712bb05077934b16c6bf25228bdec34b64f83 RCTRequired: ca91e5dd26b64f577b528044c962baf171c6b716 RCTTypeSafety: e7678bd60850ca5a41df9b8dc7154638cb66871f React: 4641770499c39f45d4e7cde1eba30e081f9d8a3d React-callinvoker: 4bef67b5c7f3f68db5929ab6a4d44b8a002998ea - React-Core: 0a06707a0b34982efc4a556aff5dae4b22863455 - React-CoreModules: 907334e94314189c2e5eed4877f3efe7b26d85b0 - React-cxxreact: 3a1d5e8f4faa5e09be26614e9c8bbcae8d11b73d + React-Core: a68cea3e762814e60ecc3fa521c7f14c36c99245 + React-CoreModules: d81b1eaf8066add66299bab9d23c9f00c9484c7c + React-cxxreact: 984f8b1feeca37181d4e95301fcd6f5f6501c6ab React-debug: 817160c07dc8d24d020fbd1eac7b3558ffc08964 - React-defaultsnativemodule: 814830ccbc3fb08d67d0190e63b179ee4098c67b - React-domnativemodule: 270acf94bd0960b026bc3bfb327e703665d27fb4 - React-Fabric: 64586dc191fc1c170372a638b8e722e4f1d0a09b - React-FabricComponents: b0ebd032387468ea700574c581b139f57a7497fb - React-FabricImage: 81f0e0794caf25ad1224fa406d288fbc1986607f + React-defaultsnativemodule: 18a684542f82ce1897552a1c4b847be414c9566e + React-domnativemodule: 90bdd4ec3ab38c47cfc3461c1e9283a8507d613f + React-Fabric: f6dade7007533daeb785ba5925039d83f343be4b + React-FabricComponents: b0655cc3e1b5ae12a4a1119aa7d8308f0ad33520 + React-FabricImage: 9b157c4c01ac2bf433f834f0e1e5fe234113a576 React-featureflags: f2792b067a351d86fdc7bec23db3b9a2f2c8d26c - React-featureflagsnativemodule: 0d7091ae344d6160c0557048e127897654a5c00f - React-graphics: cbebe910e4a15b65b0bff94a4d3ed278894d6386 - React-hermes: ec18c10f5a69d49fb9b5e17ae95494e9ea13d4d3 - React-idlecallbacksnativemodule: 6b84add48971da9c40403bd1860d4896462590f2 - React-ImageManager: f2a4c01c2ccb2193e60a20c135da74c7ca4d36f2 - React-jserrorhandler: 61d205b5a7cbc57fed3371dd7eed48c97f49fc64 - React-jsi: 95f7676103137861b79b0f319467627bcfa629ee - React-jsiexecutor: 41e0fe87cda9ea3970ffb872ef10f1ff8dbd1932 - React-jsinspector: 15578208796723e5c6f39069b6e8bf36863ef6e2 - React-jsitracing: 3758cdb155ea7711f0e77952572ea62d90c69f0b - React-logger: dbca7bdfd4aa5ef69431362bde6b36d49403cb20 - React-Mapbuffer: 6efad4a606c1fae7e4a93385ee096681ef0300dc - React-microtasksnativemodule: a645237a841d733861c70b69908ab4a1707b52ad + React-featureflagsnativemodule: 742a8325b3c821d2a1ca13a6d2a0fc72d04555e0 + React-graphics: 68969e4e49d73f89da7abef4116c9b5f466aa121 + React-hermes: ac0bcba26a5d288ebc99b500e1097da2d0297ddf + React-idlecallbacksnativemodule: d61d9c9816131bf70d3d80cd04889fc625ee523f + React-ImageManager: e906eec93a9eb6102a06576b89d48d80a4683020 + React-jserrorhandler: ac5dde01104ff444e043cad8f574ca02756e20d6 + React-jsi: 496fa2b9d63b726aeb07d0ac800064617d71211d + React-jsiexecutor: dd22ab48371b80f37a0a30d0e8915b6d0f43a893 + React-jsinspector: 4629ac376f5765e684d19064f2093e55c97fd086 + React-jsitracing: 7a1c9cd484248870cf660733cd3b8114d54c035f + React-logger: c4052eb941cca9a097ef01b59543a656dc088559 + React-Mapbuffer: 33546a3ebefbccb8770c33a1f8a5554fa96a54de + React-microtasksnativemodule: d80ff86c8902872d397d9622f1a97aadcc12cead React-nativeconfig: 8efdb1ef1e9158c77098a93085438f7e7b463678 - React-NativeModulesApple: 958d4f6c5c2ace4c0f427cf7ef82e28ae6538a22 - React-perflogger: 9b4f13c0afe56bc7b4a0e93ec74b1150421ee22d - React-performancetimeline: 359db1cb889aa0282fafc5838331b0987c4915a9 + React-NativeModulesApple: cebca2e5320a3d66e123cade23bd90a167ffce5e + React-perflogger: 72e653eb3aba9122f9e57cf012d22d2486f33358 + React-performancetimeline: cd6a9374a72001165995d2ab632f672df04076dc React-RCTActionSheet: aacf2375084dea6e7c221f4a727e579f732ff342 - React-RCTAnimation: d8c82deebebe3aaf7a843affac1b57cb2dc073d4 - React-RCTAppDelegate: 1774aa421a29a41a704ecaf789811ef73c4634b6 - React-RCTBlob: 70a58c11a6a3500d1a12f2e51ca4f6c99babcff8 - React-RCTFabric: 731cda82aed592aacce2d32ead69d78cde5d9274 - React-RCTImage: 5e9d655ba6a790c31e3176016f9b47fd0978fbf0 - React-RCTLinking: 2a48338252805091f7521eaf92687206401bdf2a - React-RCTNetwork: 0c1282b377257f6b1c81934f72d8a1d0c010e4c3 - React-RCTSettings: f757b679a74e5962be64ea08d7865a7debd67b40 - React-RCTText: e7d20c490b407d3b4a2daa48db4bcd8ec1032af2 - React-RCTVibration: 8228e37144ca3122a91f1de16ba8e0707159cfec + React-RCTAnimation: 395ab53fd064dff81507c15efb781c8684d9a585 + React-RCTAppDelegate: 345a6f1b82abc578437df0ce7e9c48740eca827c + React-RCTBlob: 13311e554c1a367de063c10ee7c5e6573b2dd1d6 + React-RCTFabric: 007b1a98201cc49b5bc6e1417d7fe3f6fc6e2b78 + React-RCTImage: 1b1f914bcc12187c49ba5d949dac38c2eb9f5cc8 + React-RCTLinking: 4ac7c42beb65e36fba0376f3498f3cd8dd0be7fa + React-RCTNetwork: 938902773add4381e84426a7aa17a2414f5f94f7 + React-RCTSettings: e848f1ba17a7a18479cf5a31d28145f567da8223 + React-RCTText: 7e98fafdde7d29e888b80f0b35544e0cb07913cf + React-RCTVibration: cd7d80affd97dc7afa62f9acd491419558b64b78 React-rendererconsistency: b4917053ecbaa91469c67a4319701c9dc0d40be6 - React-rendererdebug: 81becbc8852b38d9b1b68672aa504556481330d5 + React-rendererdebug: aa181c36dd6cf5b35511d1ed875d6638fd38f0ec React-rncore: 120d21715c9b4ba8f798bffe986cb769b988dd74 - React-RuntimeApple: 52ed0e9e84a7c2607a901149fb13599a3c057655 - React-RuntimeCore: ca6189d2e53d86db826e2673fe8af6571b8be157 + React-RuntimeApple: d033becbbd1eba6f9f6e3af6f1893030ce203edd + React-RuntimeCore: 38af280bb678e66ba000a3c3d42920b2a138eebb React-runtimeexecutor: 877596f82f5632d073e121cba2d2084b76a76899 - React-RuntimeHermes: 3b752dc5d8a1661c9d1687391d6d96acfa385549 - React-runtimescheduler: 8321bb09175ace2a4f0b3e3834637eb85bf42ebe + React-RuntimeHermes: 37aad735ff21ca6de2d8450a96de1afe9f86c385 + React-runtimescheduler: 8ec34cc885281a34696ea16c4fd86892d631f38d React-timing: 331cbf9f2668c67faddfd2e46bb7f41cbd9320b9 - React-utils: 54df9ada708578c8ad40d92895d6fed03e0e8a9e - ReactCodegen: 21a52ccddc6479448fc91903a437dd23ddc7366c - ReactCommon: bfd3600989d79bc3acbe7704161b171a1480b9fd + React-utils: ed818f19ab445000d6b5c4efa9d462449326cc9f + ReactCodegen: f853a20cc9125c5521c8766b4b49375fec20648b + ReactCommon: 300d8d9c5cb1a6cd79a67cf5d8f91e4d477195f9 SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 Yoga: feb4910aba9742cfedc059e2b2902e22ffe9954a diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 20e6b9477..00643c0f1 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -1,34 +1,34 @@ PODS: - boost (1.84.0) - - DatadogCore (2.30.0): - - DatadogInternal (= 2.30.0) - - DatadogCrashReporting (2.30.0): - - DatadogInternal (= 2.30.0) + - DatadogCore (3.0.0): + - DatadogInternal (= 3.0.0) + - DatadogCrashReporting (3.0.0): + - DatadogInternal (= 3.0.0) - PLCrashReporter (~> 1.12.0) - - DatadogInternal (2.30.0) - - DatadogLogs (2.30.0): - - DatadogInternal (= 2.30.0) - - DatadogRUM (2.30.0): - - DatadogInternal (= 2.30.0) + - DatadogInternal (3.0.0) + - DatadogLogs (3.0.0): + - DatadogInternal (= 3.0.0) + - DatadogRUM (3.0.0): + - DatadogInternal (= 3.0.0) - DatadogSDKReactNative (2.12.1): - - DatadogCore (= 2.30.0) - - DatadogCrashReporting (= 2.30.0) - - DatadogLogs (= 2.30.0) - - DatadogRUM (= 2.30.0) - - DatadogTrace (= 2.30.0) - - DatadogWebViewTracking (= 2.30.0) + - DatadogCore (= 3.0.0) + - DatadogCrashReporting (= 3.0.0) + - DatadogLogs (= 3.0.0) + - DatadogRUM (= 3.0.0) + - DatadogTrace (= 3.0.0) + - DatadogWebViewTracking (= 3.0.0) - React-Core - DatadogSDKReactNative/Tests (2.12.1): - - DatadogCore (= 2.30.0) - - DatadogCrashReporting (= 2.30.0) - - DatadogLogs (= 2.30.0) - - DatadogRUM (= 2.30.0) - - DatadogTrace (= 2.30.0) - - DatadogWebViewTracking (= 2.30.0) + - DatadogCore (= 3.0.0) + - DatadogCrashReporting (= 3.0.0) + - DatadogLogs (= 3.0.0) + - DatadogRUM (= 3.0.0) + - DatadogTrace (= 3.0.0) + - DatadogWebViewTracking (= 3.0.0) - React-Core - DatadogSDKReactNativeSessionReplay (2.12.1): - DatadogSDKReactNative - - DatadogSessionReplay (= 2.30.0) + - DatadogSessionReplay (= 3.0.0) - DoubleConversion - glog - hermes-engine @@ -51,7 +51,7 @@ PODS: - Yoga - DatadogSDKReactNativeSessionReplay/Tests (2.12.1): - DatadogSDKReactNative - - DatadogSessionReplay (= 2.30.0) + - DatadogSessionReplay (= 3.0.0) - DoubleConversion - glog - hermes-engine @@ -74,24 +74,24 @@ PODS: - ReactCommon/turbomodule/core - Yoga - DatadogSDKReactNativeWebView (2.12.1): - - DatadogInternal (= 2.30.0) + - DatadogInternal (= 3.0.0) - DatadogSDKReactNative - - DatadogWebViewTracking (= 2.30.0) + - DatadogWebViewTracking (= 3.0.0) - React-Core - DatadogSDKReactNativeWebView/Tests (2.12.1): - - DatadogInternal (= 2.30.0) + - DatadogInternal (= 3.0.0) - DatadogSDKReactNative - - DatadogWebViewTracking (= 2.30.0) + - DatadogWebViewTracking (= 3.0.0) - React-Core - react-native-webview - React-RCTText - - DatadogSessionReplay (2.30.0): - - DatadogInternal (= 2.30.0) - - DatadogTrace (2.30.0): - - DatadogInternal (= 2.30.0) + - DatadogSessionReplay (3.0.0): + - DatadogInternal (= 3.0.0) + - DatadogTrace (3.0.0): + - DatadogInternal (= 3.0.0) - OpenTelemetrySwiftApi (= 1.13.1) - - DatadogWebViewTracking (2.30.0): - - DatadogInternal (= 2.30.0) + - DatadogWebViewTracking (3.0.0): + - DatadogInternal (= 3.0.0) - DoubleConversion (1.1.6) - fast_float (6.1.4) - FBLazyVector (0.76.9) @@ -1988,17 +1988,17 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: boost: 1dca942403ed9342f98334bf4c3621f011aa7946 - DatadogCore: 5c01290a3b60b27bf49aa958f2e339c738364d9e - DatadogCrashReporting: 11286d48ab61baeb2b41b945c7c0d4ef23db317d - DatadogInternal: 7aeb48e254178a0c462c3953dc0a8a8d64499a93 - DatadogLogs: 4324739de62a6059e07d70bf6ceceed78764edeb - DatadogRUM: f36949a38285f3b240a7be577d425f8518e087d4 - DatadogSDKReactNative: 4dac5e6ce44317b7f3e3173c23a7196df698019b - DatadogSDKReactNativeSessionReplay: 1044b7351420643b5b111390eaa85807e06d4a42 - DatadogSDKReactNativeWebView: a0b416ee07b785ced7b75eaeaaaf683405bf8e64 - DatadogSessionReplay: 682c4d56b88cdb4d94e20c6db13f9630a725b178 - DatadogTrace: bfea32b6ed2870829629a9296cf526221493cc3e - DatadogWebViewTracking: 78c20d8e5f1ade506f4aadaec5690c1a63283fe2 + DatadogCore: 0c39ba4ef3dee02071f235f2fb56af7e29f4ceb0 + DatadogCrashReporting: 604e65e524cb2fc22cda39fd9c9ea5fd3bddf24e + DatadogInternal: 442673a7fb5329299b489b91ed705aa2085380f7 + DatadogLogs: 0146a0e3140ba62fd42729593c0910d692aea5fd + DatadogRUM: f97fbd0290ecec5bc952fb03a6a1ae068649243f + DatadogSDKReactNative: e430b3f4d7adb0fac07b61e2fb65ceacdaf82b3e + DatadogSDKReactNativeSessionReplay: 4b2a3d166a79581f18522795b40141c34cf3685d + DatadogSDKReactNativeWebView: 35dc2b9736e1aaa82b366bf6b8a8a959a9b088c5 + DatadogSessionReplay: 0357b911c6ab42c77c93b29761ecc20d9282e971 + DatadogTrace: d714e7c456e612b7f171483d08086a59aa1c4213 + DatadogWebViewTracking: fb9591c09fca82b143f3843a7164a7e782175c53 DoubleConversion: f16ae600a246532c4020132d54af21d0ddb2a385 fast_float: 06eeec4fe712a76acc9376682e4808b05ce978b6 FBLazyVector: 7605ea4810e0e10ae4815292433c09bf4324ba45 @@ -2008,69 +2008,69 @@ SPEC CHECKSUMS: HMSegmentedControl: 34c1f54d822d8308e7b24f5d901ec674dfa31352 OpenTelemetrySwiftApi: aaee576ed961e0c348af78df58b61300e95bd104 PLCrashReporter: db59ef96fa3d25f3650040d02ec2798cffee75f2 - RCT-Folly: 7b4f73a92ad9571b9dbdb05bb30fad927fa971e1 + RCT-Folly: ea9d9256ba7f9322ef911169a9f696e5857b9e17 RCTDeprecation: ebe712bb05077934b16c6bf25228bdec34b64f83 RCTRequired: ca91e5dd26b64f577b528044c962baf171c6b716 RCTTypeSafety: e7678bd60850ca5a41df9b8dc7154638cb66871f React: 4641770499c39f45d4e7cde1eba30e081f9d8a3d React-callinvoker: 4bef67b5c7f3f68db5929ab6a4d44b8a002998ea - React-Core: 0a06707a0b34982efc4a556aff5dae4b22863455 - React-CoreModules: 907334e94314189c2e5eed4877f3efe7b26d85b0 - React-cxxreact: 3a1d5e8f4faa5e09be26614e9c8bbcae8d11b73d + React-Core: a68cea3e762814e60ecc3fa521c7f14c36c99245 + React-CoreModules: d81b1eaf8066add66299bab9d23c9f00c9484c7c + React-cxxreact: 984f8b1feeca37181d4e95301fcd6f5f6501c6ab React-debug: 817160c07dc8d24d020fbd1eac7b3558ffc08964 - React-defaultsnativemodule: a965cb39fb0a79276ab611793d39f52e59a9a851 - React-domnativemodule: d647f94e503c62c44f54291334b1aa22a30fa08b - React-Fabric: 64586dc191fc1c170372a638b8e722e4f1d0a09b - React-FabricComponents: b0ebd032387468ea700574c581b139f57a7497fb - React-FabricImage: 81f0e0794caf25ad1224fa406d288fbc1986607f + React-defaultsnativemodule: 21f216e8db975897eb32b5f13247f5bbfaa97f41 + React-domnativemodule: 19270ad4b8d33312838d257f24731a0026809d49 + React-Fabric: f6dade7007533daeb785ba5925039d83f343be4b + React-FabricComponents: b0655cc3e1b5ae12a4a1119aa7d8308f0ad33520 + React-FabricImage: 9b157c4c01ac2bf433f834f0e1e5fe234113a576 React-featureflags: f2792b067a351d86fdc7bec23db3b9a2f2c8d26c - React-featureflagsnativemodule: 95a02d895475de8ace78fedd76143866838bb720 - React-graphics: cbebe910e4a15b65b0bff94a4d3ed278894d6386 - React-hermes: ec18c10f5a69d49fb9b5e17ae95494e9ea13d4d3 - React-idlecallbacksnativemodule: 0c1ae840cc5587197cd926a3cb76828ad059d116 - React-ImageManager: f2a4c01c2ccb2193e60a20c135da74c7ca4d36f2 - React-jserrorhandler: 61d205b5a7cbc57fed3371dd7eed48c97f49fc64 - React-jsi: 95f7676103137861b79b0f319467627bcfa629ee - React-jsiexecutor: 41e0fe87cda9ea3970ffb872ef10f1ff8dbd1932 - React-jsinspector: 15578208796723e5c6f39069b6e8bf36863ef6e2 - React-jsitracing: 3758cdb155ea7711f0e77952572ea62d90c69f0b - React-logger: dbca7bdfd4aa5ef69431362bde6b36d49403cb20 - React-Mapbuffer: 6efad4a606c1fae7e4a93385ee096681ef0300dc - React-microtasksnativemodule: 8732b71aa66045da4bb341ddee1bb539f71e5f38 - react-native-crash-tester: 3ffaa64141427ca362079cb53559fe9a532487ae - react-native-safe-area-context: 04803a01f39f31cc6605a5531280b477b48f8a88 - react-native-webview: 1e12de2fad74c17b4f8b1b53ebd1e3baa0148d71 + React-featureflagsnativemodule: 3a8731d8fd9f755be57e00d9fa8a7f92aa77e87d + React-graphics: 68969e4e49d73f89da7abef4116c9b5f466aa121 + React-hermes: ac0bcba26a5d288ebc99b500e1097da2d0297ddf + React-idlecallbacksnativemodule: 9a2c5b5c174c0c476f039bedc1b9497a8272133e + React-ImageManager: e906eec93a9eb6102a06576b89d48d80a4683020 + React-jserrorhandler: ac5dde01104ff444e043cad8f574ca02756e20d6 + React-jsi: 496fa2b9d63b726aeb07d0ac800064617d71211d + React-jsiexecutor: dd22ab48371b80f37a0a30d0e8915b6d0f43a893 + React-jsinspector: 4629ac376f5765e684d19064f2093e55c97fd086 + React-jsitracing: 7a1c9cd484248870cf660733cd3b8114d54c035f + React-logger: c4052eb941cca9a097ef01b59543a656dc088559 + React-Mapbuffer: 33546a3ebefbccb8770c33a1f8a5554fa96a54de + React-microtasksnativemodule: 5c3d795318c22ab8df55100e50b151384a4a60b3 + react-native-crash-tester: 48bde9d6f5256c61ef2e0c52dfc74256b26e55eb + react-native-safe-area-context: e134b241010ebe2aacdcea013565963d13826faa + react-native-webview: 2ea635bc43fd8a4b89de61133e8cc0607084e9f8 React-nativeconfig: 8efdb1ef1e9158c77098a93085438f7e7b463678 - React-NativeModulesApple: 958d4f6c5c2ace4c0f427cf7ef82e28ae6538a22 - React-perflogger: 9b4f13c0afe56bc7b4a0e93ec74b1150421ee22d - React-performancetimeline: 359db1cb889aa0282fafc5838331b0987c4915a9 + React-NativeModulesApple: cebca2e5320a3d66e123cade23bd90a167ffce5e + React-perflogger: 72e653eb3aba9122f9e57cf012d22d2486f33358 + React-performancetimeline: cd6a9374a72001165995d2ab632f672df04076dc React-RCTActionSheet: aacf2375084dea6e7c221f4a727e579f732ff342 - React-RCTAnimation: d8c82deebebe3aaf7a843affac1b57cb2dc073d4 - React-RCTAppDelegate: 6c0377d9c4058773ea7073bb34bb9ebd6ddf5a84 - React-RCTBlob: 70a58c11a6a3500d1a12f2e51ca4f6c99babcff8 - React-RCTFabric: 7eb6dd2c8fda98cb860a572e3f4e4eb60d62c89e - React-RCTImage: 5e9d655ba6a790c31e3176016f9b47fd0978fbf0 - React-RCTLinking: 2a48338252805091f7521eaf92687206401bdf2a - React-RCTNetwork: 0c1282b377257f6b1c81934f72d8a1d0c010e4c3 - React-RCTSettings: f757b679a74e5962be64ea08d7865a7debd67b40 - React-RCTText: e7d20c490b407d3b4a2daa48db4bcd8ec1032af2 - React-RCTVibration: 8228e37144ca3122a91f1de16ba8e0707159cfec + React-RCTAnimation: 395ab53fd064dff81507c15efb781c8684d9a585 + React-RCTAppDelegate: 1e5b43833e3e36e9fa34eec20be98174bc0e14a2 + React-RCTBlob: 13311e554c1a367de063c10ee7c5e6573b2dd1d6 + React-RCTFabric: bd906861a4e971e21d8df496c2d8f3ca6956f840 + React-RCTImage: 1b1f914bcc12187c49ba5d949dac38c2eb9f5cc8 + React-RCTLinking: 4ac7c42beb65e36fba0376f3498f3cd8dd0be7fa + React-RCTNetwork: 938902773add4381e84426a7aa17a2414f5f94f7 + React-RCTSettings: e848f1ba17a7a18479cf5a31d28145f567da8223 + React-RCTText: 7e98fafdde7d29e888b80f0b35544e0cb07913cf + React-RCTVibration: cd7d80affd97dc7afa62f9acd491419558b64b78 React-rendererconsistency: b4917053ecbaa91469c67a4319701c9dc0d40be6 - React-rendererdebug: 81becbc8852b38d9b1b68672aa504556481330d5 + React-rendererdebug: aa181c36dd6cf5b35511d1ed875d6638fd38f0ec React-rncore: 120d21715c9b4ba8f798bffe986cb769b988dd74 - React-RuntimeApple: 52ed0e9e84a7c2607a901149fb13599a3c057655 - React-RuntimeCore: ca6189d2e53d86db826e2673fe8af6571b8be157 + React-RuntimeApple: d033becbbd1eba6f9f6e3af6f1893030ce203edd + React-RuntimeCore: 38af280bb678e66ba000a3c3d42920b2a138eebb React-runtimeexecutor: 877596f82f5632d073e121cba2d2084b76a76899 - React-RuntimeHermes: 3b752dc5d8a1661c9d1687391d6d96acfa385549 - React-runtimescheduler: 8321bb09175ace2a4f0b3e3834637eb85bf42ebe + React-RuntimeHermes: 37aad735ff21ca6de2d8450a96de1afe9f86c385 + React-runtimescheduler: 8ec34cc885281a34696ea16c4fd86892d631f38d React-timing: 331cbf9f2668c67faddfd2e46bb7f41cbd9320b9 - React-utils: 54df9ada708578c8ad40d92895d6fed03e0e8a9e - ReactCodegen: 21a52ccddc6479448fc91903a437dd23ddc7366c - ReactCommon: bfd3600989d79bc3acbe7704161b171a1480b9fd - ReactNativeNavigation: 50c1eef68b821e7265eff3a391d27ed18fdce459 - RNCAsyncStorage: 23e56519cc41d3bade3c8d4479f7760cb1c11996 - RNGestureHandler: 950dfa674dbf481460ca389c65b9036ac4ab8ada - RNScreens: 606ab1cf68162f7ba0d049a31f2a84089a6fffb4 + React-utils: ed818f19ab445000d6b5c4efa9d462449326cc9f + ReactCodegen: f853a20cc9125c5521c8766b4b49375fec20648b + ReactCommon: 300d8d9c5cb1a6cd79a67cf5d8f91e4d477195f9 + ReactNativeNavigation: 445f86273eb245d15b14023ee4ef9d6e4f891ad6 + RNCAsyncStorage: b44e8a4e798c3e1f56bffccd0f591f674fb9198f + RNGestureHandler: cb711d56ee3b03a5adea1d38324d4459ab55653f + RNScreens: f75b26fd4777848c216e27b0a09e1bf9c9f4760a SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 Yoga: feb4910aba9742cfedc059e2b2902e22ffe9954a diff --git a/packages/core/DatadogSDKReactNative.podspec b/packages/core/DatadogSDKReactNative.podspec index 189994028..a2054d6ca 100644 --- a/packages/core/DatadogSDKReactNative.podspec +++ b/packages/core/DatadogSDKReactNative.podspec @@ -19,14 +19,14 @@ Pod::Spec.new do |s| s.dependency "React-Core" # /!\ Remember to keep the versions in sync with DatadogSDKReactNativeSessionReplay.podspec - s.dependency 'DatadogCore', '2.30.0' - s.dependency 'DatadogLogs', '2.30.0' - s.dependency 'DatadogTrace', '2.30.0' - s.dependency 'DatadogRUM', '2.30.0' - s.dependency 'DatadogCrashReporting', '2.30.0' + s.dependency 'DatadogCore', '3.0.0' + s.dependency 'DatadogLogs', '3.0.0' + s.dependency 'DatadogTrace', '3.0.0' + s.dependency 'DatadogRUM', '3.0.0' + s.dependency 'DatadogCrashReporting', '3.0.0' # DatadogWebViewTracking is not available for tvOS - s.ios.dependency 'DatadogWebViewTracking', '2.30.0' + s.ios.dependency 'DatadogWebViewTracking', '3.0.0' s.test_spec 'Tests' do |test_spec| test_spec.source_files = 'ios/Tests/**/*.{swift,json}' diff --git a/packages/core/android/build.gradle b/packages/core/android/build.gradle index b82587b33..17b26cdbd 100644 --- a/packages/core/android/build.gradle +++ b/packages/core/android/build.gradle @@ -197,16 +197,16 @@ dependencies { // This breaks builds if the React Native target is below 0.76.0. as it relies on Gradle 8.5.0. // To avoid this, we enforce 1.0.0-beta01 on RN < 0.76.0 if (reactNativeMinorVersion < 76) { - implementation("com.datadoghq:dd-sdk-android-rum:2.25.0") { + implementation("com.datadoghq:dd-sdk-android-rum:3.0.0") { exclude group: "androidx.metrics", module: "metrics-performance" } implementation "androidx.metrics:metrics-performance:1.0.0-beta01" } else { - implementation "com.datadoghq:dd-sdk-android-rum:2.25.0" + implementation "com.datadoghq:dd-sdk-android-rum:3.0.0" } - implementation "com.datadoghq:dd-sdk-android-logs:2.25.0" - implementation "com.datadoghq:dd-sdk-android-trace:2.25.0" - implementation "com.datadoghq:dd-sdk-android-webview:2.25.0" + implementation "com.datadoghq:dd-sdk-android-logs:3.0.0" + implementation "com.datadoghq:dd-sdk-android-trace:3.0.0" + implementation "com.datadoghq:dd-sdk-android-webview:3.0.0" implementation "com.google.code.gson:gson:2.10.0" testImplementation "org.junit.platform:junit-platform-launcher:1.6.2" testImplementation "org.junit.jupiter:junit-jupiter-api:5.6.2" diff --git a/packages/react-native-session-replay/DatadogSDKReactNativeSessionReplay.podspec b/packages/react-native-session-replay/DatadogSDKReactNativeSessionReplay.podspec index b3fdcec09..77450eb93 100644 --- a/packages/react-native-session-replay/DatadogSDKReactNativeSessionReplay.podspec +++ b/packages/react-native-session-replay/DatadogSDKReactNativeSessionReplay.podspec @@ -19,7 +19,7 @@ Pod::Spec.new do |s| s.dependency "React-Core" # /!\ Remember to keep the version in sync with DatadogSDKReactNative.podspec - s.dependency 'DatadogSessionReplay', '2.30.0' + s.dependency 'DatadogSessionReplay', '3.0.0' s.dependency 'DatadogSDKReactNative' s.test_spec 'Tests' do |test_spec| diff --git a/packages/react-native-session-replay/android/build.gradle b/packages/react-native-session-replay/android/build.gradle index 2756709d7..365b089b1 100644 --- a/packages/react-native-session-replay/android/build.gradle +++ b/packages/react-native-session-replay/android/build.gradle @@ -211,8 +211,8 @@ dependencies { api "com.facebook.react:react-android:$reactNativeVersion" } implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" - implementation "com.datadoghq:dd-sdk-android-session-replay:2.25.0" - implementation "com.datadoghq:dd-sdk-android-internal:2.25.0" + implementation "com.datadoghq:dd-sdk-android-session-replay:3.0.0" + implementation "com.datadoghq:dd-sdk-android-internal:3.0.0" implementation project(path: ':datadog_mobile-react-native') testImplementation "org.junit.platform:junit-platform-launcher:1.6.2" diff --git a/packages/react-native-webview/DatadogSDKReactNativeWebView.podspec b/packages/react-native-webview/DatadogSDKReactNativeWebView.podspec index 647126555..26e160bbc 100644 --- a/packages/react-native-webview/DatadogSDKReactNativeWebView.podspec +++ b/packages/react-native-webview/DatadogSDKReactNativeWebView.podspec @@ -23,8 +23,8 @@ Pod::Spec.new do |s| end # /!\ Remember to keep the version in sync with DatadogSDKReactNative.podspec - s.dependency 'DatadogWebViewTracking', '2.30.0' - s.dependency 'DatadogInternal', '2.30.0' + s.dependency 'DatadogWebViewTracking', '3.0.0' + s.dependency 'DatadogInternal', '3.0.0' s.dependency 'DatadogSDKReactNative' s.test_spec 'Tests' do |test_spec| diff --git a/packages/react-native-webview/android/build.gradle b/packages/react-native-webview/android/build.gradle index 9f2aba9c9..d7c930ceb 100644 --- a/packages/react-native-webview/android/build.gradle +++ b/packages/react-native-webview/android/build.gradle @@ -189,7 +189,7 @@ dependencies { implementation "com.facebook.react:react-android:$reactNativeVersion" } - implementation "com.datadoghq:dd-sdk-android-webview:2.25.0" + implementation "com.datadoghq:dd-sdk-android-webview:3.0.0" implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation project(path: ':datadog_mobile-react-native') From 36cd5b1616e25240521b53ccea1827c821ec9937 Mon Sep 17 00:00:00 2001 From: Sergio Barrio Date: Fri, 12 Sep 2025 14:43:25 +0200 Subject: [PATCH 012/410] Remove setUser --- packages/codepush/__mocks__/react-native.ts | 3 - packages/core/__mocks__/react-native.ts | 3 - .../datadog/reactnative/DatadogSDKWrapper.kt | 10 -- .../com/datadog/reactnative/DatadogWrapper.kt | 17 -- .../reactnative/DdSdkImplementation.kt | 17 +- .../kotlin/com/datadog/reactnative/DdSdk.kt | 14 +- .../kotlin/com/datadog/reactnative/DdSdk.kt | 11 -- .../com/datadog/reactnative/DdSdkTest.kt | 158 ------------------ packages/core/ios/Sources/DdSdk.mm | 11 -- .../ios/Sources/DdSdkImplementation.swift | 15 +- packages/core/ios/Tests/DdSdkTests.swift | 132 --------------- packages/core/ios/Tests/MockRUMMonitor.swift | 16 ++ packages/core/jest/mock.js | 3 - packages/core/src/DdSdkReactNative.tsx | 25 +-- .../src/__tests__/DdSdkReactNative.test.tsx | 16 -- packages/core/src/logs/eventMapper.ts | 6 +- .../core/src/sdk/EventMappers/EventMapper.ts | 2 +- .../UserInfoSingleton/UserInfoSingleton.ts | 4 +- .../__tests__/UserInfoSingleton.test.ts | 6 +- .../core/src/sdk/UserInfoSingleton/types.ts | 5 +- packages/core/src/specs/NativeDdSdk.ts | 7 - packages/core/src/types.tsx | 10 +- .../__mocks__/react-native.ts | 3 - 23 files changed, 42 insertions(+), 452 deletions(-) diff --git a/packages/codepush/__mocks__/react-native.ts b/packages/codepush/__mocks__/react-native.ts index b35df4e31..046ced2f6 100644 --- a/packages/codepush/__mocks__/react-native.ts +++ b/packages/codepush/__mocks__/react-native.ts @@ -18,9 +18,6 @@ actualRN.NativeModules.DdSdk = { initialize: jest.fn().mockImplementation( () => new Promise(resolve => resolve()) ) as jest.MockedFunction, - setUser: jest.fn().mockImplementation( - () => new Promise(resolve => resolve()) - ) as jest.MockedFunction, setAttributes: jest.fn().mockImplementation( () => new Promise(resolve => resolve()) ) as jest.MockedFunction, diff --git a/packages/core/__mocks__/react-native.ts b/packages/core/__mocks__/react-native.ts index 9507ec33f..260fe68a7 100644 --- a/packages/core/__mocks__/react-native.ts +++ b/packages/core/__mocks__/react-native.ts @@ -18,9 +18,6 @@ actualRN.NativeModules.DdSdk = { initialize: jest.fn().mockImplementation( () => new Promise(resolve => resolve()) ) as jest.MockedFunction, - setUser: jest.fn().mockImplementation( - () => new Promise(resolve => resolve()) - ) as jest.MockedFunction, setUserInfo: jest.fn().mockImplementation( () => new Promise(resolve => resolve()) ) as jest.MockedFunction, diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogSDKWrapper.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogSDKWrapper.kt index 2b6e8800a..141b995da 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogSDKWrapper.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogSDKWrapper.kt @@ -70,16 +70,6 @@ internal class DatadogSDKWrapper : DatadogWrapper { DatadogSDKWrapperStorage.notifyOnInitializedListeners(core as InternalSdkCore) } - @Deprecated("Use setUserInfo instead; the user ID is now required.") - override fun setUser( - id: String?, - name: String?, - email: String?, - extraInfo: Map - ) { - Datadog.setUserInfo(id, name, email, extraInfo) - } - override fun setUserInfo( id: String, name: String?, diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogWrapper.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogWrapper.kt index 19b25e587..3ae3e6266 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogWrapper.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogWrapper.kt @@ -62,23 +62,6 @@ interface DatadogWrapper { consent: TrackingConsent ) - /** - * Sets the user information. - * - * @param id (nullable) a unique user identifier (relevant to your business domain) - * @param name (nullable) the user name or alias - * @param email (nullable) the user email - * @param extraInfo additional information. An extra information can be - * nested up to 8 levels deep. Keys using more than 8 levels will be sanitized by SDK. - */ - @Deprecated("Use setUserInfo instead; the user ID is now required.") - fun setUser( - id: String?, - name: String?, - email: String?, - extraInfo: Map - ) - /** * Sets the user information. * diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt index c59ab54be..54769fb8b 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt @@ -78,21 +78,6 @@ class DdSdkImplementation( promise.resolve(null) } - /** - * Set the user information. - * @param user The user object (use builtin attributes: 'id', 'email', 'name', and/or any custom - * attribute). - */ - @Deprecated("Use setUserInfo instead; the user ID is now required.") - fun setUser(user: ReadableMap, promise: Promise) { - val extraInfo = user.toHashMap().toMutableMap() - val id = extraInfo.remove("id")?.toString() - val name = extraInfo.remove("name")?.toString() - val email = extraInfo.remove("email")?.toString() - datadog.setUser(id, name, email, extraInfo) - promise.resolve(null) - } - /** * Set the user information. * @param userInfo The user object (use builtin attributes: 'id', 'email', 'name', and any custom @@ -110,7 +95,7 @@ class DdSdkImplementation( if (id != null) { datadog.setUserInfo(id, name, email, extraInfo) } else { - datadog.setUser(null, name, email, extraInfo) + // TO DO - Log warning? } promise.resolve(null) diff --git a/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdSdk.kt b/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdSdk.kt index 5bc470947..cfafffffe 100644 --- a/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdSdk.kt +++ b/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdSdk.kt @@ -51,19 +51,7 @@ class DdSdk( /** * Set the user information. - * @param user The user object (use builtin attributes: 'id', 'email', 'name', and/or any custom - * attribute). - */ - @Deprecated("Use setUserInfo instead; the user ID is now required.") - @ReactMethod - override fun setUser(user: ReadableMap, promise: Promise) { - implementation.setUser(user, promise) - } - - /** - * Set the user information. - * @param user The user object (use builtin attributes: 'id', 'email', 'name', and any custom - * attribute inside 'extraInfo'). + * @param user The user object (use builtin attributes: 'id', 'email', 'name', and any custom * attribute inside 'extraInfo'). */ @ReactMethod override fun setUserInfo(user: ReadableMap, promise: Promise) { diff --git a/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt b/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt index 17acd6d20..97acb2ebf 100644 --- a/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt +++ b/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt @@ -75,17 +75,6 @@ class DdSdk( implementation.setAttributes(attributes, promise) } - /** - * Set the user information. - * @param user The user object (use builtin attributes: 'id', 'email', 'name', and/or any custom - * attribute). - */ - @Deprecated("Use setUserInfo instead; the user ID is now required.") - @ReactMethod - fun setUser(user: ReadableMap, promise: Promise) { - implementation.setUser(user, promise) - } - /** * Set the user information. * @param user The user object (use builtin attributes: 'id', 'email', 'name', and any custom * attribute inside 'extraInfo'). diff --git a/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt b/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt index f71507ade..9b0014b7e 100644 --- a/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt +++ b/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt @@ -2810,164 +2810,6 @@ internal class DdSdkTest { // region misc - @Test - fun `𝕄 set native user info 𝕎 setUser()`( - @MapForgery( - key = AdvancedForgery(string = [StringForgery(StringForgeryType.NUMERICAL)]), - value = AdvancedForgery(string = [StringForgery(StringForgeryType.ASCII)]) - ) extraInfo: Map - ) { - // When - testedBridgeSdk.setUser(extraInfo.toReadableMap(), mockPromise) - - // Then - argumentCaptor> { - verify(mockDatadog) - .setUser( - isNull(), - isNull(), - isNull(), - capture() - ) - - assertThat(firstValue) - .containsAllEntriesOf(extraInfo) - .hasSize(extraInfo.size) - } - } - - @Test - fun `𝕄 set native user info 𝕎 setUser() {with id}`( - @StringForgery id: String, - @MapForgery( - key = AdvancedForgery(string = [StringForgery(StringForgeryType.NUMERICAL)]), - value = AdvancedForgery(string = [StringForgery(StringForgeryType.ASCII)]) - ) extraInfo: Map - ) { - // Given - val user = extraInfo.toMutableMap().also { - it.put("id", id) - } - - // When - testedBridgeSdk.setUser(user.toReadableMap(), mockPromise) - - // Then - argumentCaptor> { - verify(mockDatadog) - .setUser( - eq(id), - isNull(), - isNull(), - capture() - ) - - assertThat(firstValue) - .containsAllEntriesOf(extraInfo) - .hasSize(extraInfo.size) - } - } - - @Test - fun `𝕄 set native user info 𝕎 setUser() {with name}`( - @StringForgery name: String, - @MapForgery( - key = AdvancedForgery(string = [StringForgery(StringForgeryType.NUMERICAL)]), - value = AdvancedForgery(string = [StringForgery(StringForgeryType.ASCII)]) - ) extraInfo: Map - ) { - // Given - val user = extraInfo.toMutableMap().also { - it.put("name", name) - } - - // When - testedBridgeSdk.setUser(user.toReadableMap(), mockPromise) - - // Then - argumentCaptor> { - verify(mockDatadog) - .setUser( - isNull(), - eq(name), - isNull(), - capture() - ) - - assertThat(firstValue) - .containsAllEntriesOf(extraInfo) - .hasSize(extraInfo.size) - } - } - - @Test - fun `𝕄 set native user info 𝕎 setUser() {with email}`( - @StringForgery(regex = "\\w+@\\w+\\.[a-z]{3}") email: String, - @MapForgery( - key = AdvancedForgery(string = [StringForgery(StringForgeryType.NUMERICAL)]), - value = AdvancedForgery(string = [StringForgery(StringForgeryType.ASCII)]) - ) extraInfo: Map - ) { - // Given - val user = extraInfo.toMutableMap().also { - it.put("email", email) - } - - // When - testedBridgeSdk.setUser(user.toReadableMap(), mockPromise) - - // Then - argumentCaptor> { - verify(mockDatadog) - .setUser( - isNull(), - isNull(), - eq(email), - capture() - ) - - assertThat(firstValue) - .containsAllEntriesOf(extraInfo) - .hasSize(extraInfo.size) - } - } - - @Test - fun `𝕄 set native user info 𝕎 setUser() {with id, name and email}`( - @StringForgery id: String, - @StringForgery name: String, - @StringForgery(regex = "\\w+@\\w+\\.[a-z]{3}") email: String, - @MapForgery( - key = AdvancedForgery(string = [StringForgery(StringForgeryType.NUMERICAL)]), - value = AdvancedForgery(string = [StringForgery(StringForgeryType.ASCII)]) - ) extraInfo: Map - ) { - // Given - val user = extraInfo.toMutableMap().also { - it.put("id", id) - it.put("name", name) - it.put("email", email) - } - - // When - testedBridgeSdk.setUser(user.toReadableMap(), mockPromise) - - // Then - argumentCaptor> { - verify(mockDatadog) - .setUser( - eq(id), - eq(name), - eq(email), - capture() - ) - - assertThat(firstValue) - .containsAllEntriesOf(extraInfo) - .hasSize(extraInfo.size) - } - } - @Test fun `𝕄 set native user info 𝕎 setUserInfo() {with id}`( @StringForgery id: String diff --git a/packages/core/ios/Sources/DdSdk.mm b/packages/core/ios/Sources/DdSdk.mm index 663da0a79..3ead770ac 100644 --- a/packages/core/ios/Sources/DdSdk.mm +++ b/packages/core/ios/Sources/DdSdk.mm @@ -37,13 +37,6 @@ + (void)initFromNative { [self setAttributes:attributes resolve:resolve reject:reject]; } -RCT_REMAP_METHOD(setUser, withUser:(NSDictionary*)user - withResolver:(RCTPromiseResolveBlock)resolve - withRejecter:(RCTPromiseRejectBlock)reject) -{ - [self setUser:user resolve:resolve reject:reject]; -} - RCT_REMAP_METHOD(setUserInfo, withUserInfo:(NSDictionary*)userInfo withResolver:(RCTPromiseResolveBlock)resolve withRejecter:(RCTPromiseRejectBlock)reject) @@ -144,10 +137,6 @@ - (void)setTrackingConsent:(NSString *)trackingConsent resolve:(RCTPromiseResolv [self.ddSdkImplementation setTrackingConsentWithTrackingConsent:trackingConsent resolve:resolve reject:reject]; } -- (void)setUser:(NSDictionary *)user resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { - [self.ddSdkImplementation setUserWithUser:user resolve:resolve reject:reject]; -} - - (void)setUserInfo:(NSDictionary *)userInfo resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { [self.ddSdkImplementation setUserInfoWithUserInfo:userInfo resolve:resolve reject:reject]; } diff --git a/packages/core/ios/Sources/DdSdkImplementation.swift b/packages/core/ios/Sources/DdSdkImplementation.swift index 2f66f868c..5a4bbf9a5 100644 --- a/packages/core/ios/Sources/DdSdkImplementation.swift +++ b/packages/core/ios/Sources/DdSdkImplementation.swift @@ -81,18 +81,6 @@ public class DdSdkImplementation: NSObject { resolve(nil) } - @objc - public func setUser(user: NSDictionary, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { - var castedUser = castAttributesToSwift(user) - let id = castedUser.removeValue(forKey: "id") as? String - let name = castedUser.removeValue(forKey: "name") as? String - let email = castedUser.removeValue(forKey: "email") as? String - let extraInfo: [String: Encodable] = castedUser // everything what's left is an `extraInfo` - - Datadog.setUserInfo(id: id, name: name, email: email, extraInfo: extraInfo) - resolve(nil) - } - @objc public func setUserInfo(userInfo: NSDictionary, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { let castedUserInfo = castAttributesToSwift(userInfo) @@ -109,8 +97,9 @@ public class DdSdkImplementation: NSObject { if let validId = id { Datadog.setUserInfo(id: validId, name: name, email: email, extraInfo: extraInfo) } else { - Datadog.setUserInfo(name: name, email: email, extraInfo: extraInfo) + // TO DO - log warning message? } + resolve(nil) } diff --git a/packages/core/ios/Tests/DdSdkTests.swift b/packages/core/ios/Tests/DdSdkTests.swift index bcc3252a8..6be5e0994 100644 --- a/packages/core/ios/Tests/DdSdkTests.swift +++ b/packages/core/ios/Tests/DdSdkTests.swift @@ -535,85 +535,6 @@ class DdSdkTests: XCTestCase { XCTAssertEqual(ddConfig.trackFrustrations, false) } - func testSetUser() throws { - let bridge = DdSdkImplementation( - mainDispatchQueue: DispatchQueueMock(), - jsDispatchQueue: DispatchQueueMock(), - jsRefreshRateMonitor: JSRefreshRateMonitor(), - RUMMonitorProvider: { MockRUMMonitor() }, - RUMMonitorInternalProvider: { nil } - ) - bridge.initialize( - configuration: .mockAny(), - resolve: mockResolve, - reject: mockReject - ) - - bridge.setUser( - user: NSDictionary( - dictionary: [ - "id": "id_123", - "name": "John Doe", - "email": "john@doe.com", - "extra-info-1": 123, - "extra-info-2": "abc", - "extra-info-3": true, - ] - ), - resolve: mockResolve, - reject: mockReject - ) - - let ddContext = try XCTUnwrap(CoreRegistry.default as? DatadogCore).contextProvider.read() - let userInfo = try XCTUnwrap(ddContext.userInfo) - - XCTAssertEqual(userInfo.id, "id_123") - XCTAssertEqual(userInfo.name, "John Doe") - XCTAssertEqual(userInfo.email, "john@doe.com") - XCTAssertEqual(userInfo.extraInfo["extra-info-1"] as? Int64, 123) - XCTAssertEqual(userInfo.extraInfo["extra-info-2"] as? String, "abc") - XCTAssertEqual(userInfo.extraInfo["extra-info-3"] as? Bool, true) - } - - func testSetUserOptionalId() throws { - let bridge = DdSdkImplementation( - mainDispatchQueue: DispatchQueueMock(), - jsDispatchQueue: DispatchQueueMock(), - jsRefreshRateMonitor: JSRefreshRateMonitor(), - RUMMonitorProvider: { MockRUMMonitor() }, - RUMMonitorInternalProvider: { nil } - ) - bridge.initialize( - configuration: .mockAny(), - resolve: mockResolve, - reject: mockReject - ) - - bridge.setUser( - user: NSDictionary( - dictionary: [ - "name": "John Doe", - "email": "john@doe.com", - "extra-info-1": 123, - "extra-info-2": "abc", - "extra-info-3": true, - ] - ), - resolve: mockResolve, - reject: mockReject - ) - - let ddContext = try XCTUnwrap(CoreRegistry.default as? DatadogCore).contextProvider.read() - let userInfo = try XCTUnwrap(ddContext.userInfo) - - XCTAssertEqual(userInfo.id, nil) - XCTAssertEqual(userInfo.name, "John Doe") - XCTAssertEqual(userInfo.email, "john@doe.com") - XCTAssertEqual(userInfo.extraInfo["extra-info-1"] as? Int64, 123) - XCTAssertEqual(userInfo.extraInfo["extra-info-2"] as? String, "abc") - XCTAssertEqual(userInfo.extraInfo["extra-info-3"] as? Bool, true) - } - func testSetUserInfo() throws { let bridge = DdSdkImplementation( mainDispatchQueue: DispatchQueueMock(), @@ -668,59 +589,6 @@ class DdSdkTests: XCTestCase { } } - func testSetUserInfoOptionalId() throws { - let bridge = DdSdkImplementation( - mainDispatchQueue: DispatchQueueMock(), - jsDispatchQueue: DispatchQueueMock(), - jsRefreshRateMonitor: JSRefreshRateMonitor(), - RUMMonitorProvider: { MockRUMMonitor() }, - RUMMonitorInternalProvider: { nil } - ) - bridge.initialize( - configuration: .mockAny(), - resolve: mockResolve, - reject: mockReject - ) - - bridge.setUserInfo( - userInfo: NSDictionary( - dictionary: [ - "name": "John Doe", - "email": "john@doe.com", - "extraInfo": [ - "extra-info-1": 123, - "extra-info-2": "abc", - "extra-info-3": true, - "extra-info-4": [ - "nested-extra-info-1": 456 - ], - ], - ] - ), - resolve: mockResolve, - reject: mockReject - ) - - let ddContext = try XCTUnwrap(CoreRegistry.default as? DatadogCore).contextProvider.read() - let userInfo = try XCTUnwrap(ddContext.userInfo) - - XCTAssertEqual(userInfo.id, nil) - XCTAssertEqual(userInfo.name, "John Doe") - XCTAssertEqual(userInfo.email, "john@doe.com") - XCTAssertEqual(userInfo.extraInfo["extra-info-1"] as? Int64, 123) - XCTAssertEqual(userInfo.extraInfo["extra-info-2"] as? String, "abc") - XCTAssertEqual(userInfo.extraInfo["extra-info-3"] as? Bool, true) - - if let extraInfo4Encodable = userInfo.extraInfo["extra-info-4"] - as? DatadogSDKReactNative.AnyEncodable, - let extraInfo4Dict = extraInfo4Encodable.value as? [String: Int] - { - XCTAssertEqual(extraInfo4Dict, ["nested-extra-info-1": 456]) - } else { - XCTFail("extra-info-4 is not of expected type or value") - } - } - func testAddUserExtraInfo() throws { let bridge = DdSdkImplementation( mainDispatchQueue: DispatchQueueMock(), diff --git a/packages/core/ios/Tests/MockRUMMonitor.swift b/packages/core/ios/Tests/MockRUMMonitor.swift index fe17e7748..f0fa03364 100644 --- a/packages/core/ios/Tests/MockRUMMonitor.swift +++ b/packages/core/ios/Tests/MockRUMMonitor.swift @@ -10,6 +10,22 @@ @testable import DatadogSDKReactNative internal class MockRUMMonitor: RUMMonitorProtocol { + func addViewAttribute(forKey key: DatadogInternal.AttributeKey, value: any DatadogInternal.AttributeValue) { + // not implemented + } + + func addViewAttributes(_ attributes: [DatadogInternal.AttributeKey : any DatadogInternal.AttributeValue]) { + // not implemented + } + + func removeViewAttribute(forKey key: DatadogInternal.AttributeKey) { + // not implemented + } + + func removeViewAttributes(forKeys keys: [DatadogInternal.AttributeKey]) { + // not implemented + } + func currentSessionID(completion: @escaping (String?) -> Void) { // not implemented } diff --git a/packages/core/jest/mock.js b/packages/core/jest/mock.js index 9c08f5335..a8161295a 100644 --- a/packages/core/jest/mock.js +++ b/packages/core/jest/mock.js @@ -27,9 +27,6 @@ module.exports = { .fn() .mockImplementation(() => new Promise(resolve => resolve())), isInitialized: jest.fn().mockImplementation(() => true), - setUser: jest - .fn() - .mockImplementation(() => new Promise(resolve => resolve())), setUserInfo: jest .fn() .mockImplementation(() => new Promise(resolve => resolve())), diff --git a/packages/core/src/DdSdkReactNative.tsx b/packages/core/src/DdSdkReactNative.tsx index aa7e2ec36..668ae09f3 100644 --- a/packages/core/src/DdSdkReactNative.tsx +++ b/packages/core/src/DdSdkReactNative.tsx @@ -38,7 +38,6 @@ import { DdSdk } from './sdk/DdSdk'; import { FileBasedConfiguration } from './sdk/FileBasedConfiguration/FileBasedConfiguration'; import { GlobalState } from './sdk/GlobalState/GlobalState'; import { UserInfoSingleton } from './sdk/UserInfoSingleton/UserInfoSingleton'; -import type { UserInfo } from './sdk/UserInfoSingleton/types'; import { DdSdkConfiguration } from './types'; import { adaptLongTaskThreshold } from './utils/longTasksUtils'; import { version as sdkVersion } from './version'; @@ -192,22 +191,6 @@ export class DdSdkReactNative { AttributesSingleton.getInstance().setAttributes(attributes); }; - /** - * Set the user information. - * @deprecated UserInfo id property is now mandatory (please user setUserInfo instead) - * @param user: The user object (use builtin attributes: 'id', 'email', 'name', and/or any custom attribute). - * @returns a Promise. - */ - // eslint-disable-next-line @typescript-eslint/ban-types - static setUser = async (user: UserInfo): Promise => { - InternalLog.log( - `Setting user ${JSON.stringify(user)}`, - SdkVerbosity.DEBUG - ); - await DdSdk.setUser(user); - UserInfoSingleton.getInstance().setUserInfo(user); - }; - /** * Sets the user information. * @param id: A mandatory unique user identifier (relevant to your business domain). @@ -245,6 +228,14 @@ export class DdSdkReactNative { ); const userInfo = UserInfoSingleton.getInstance().getUserInfo(); + if (!userInfo) { + InternalLog.log( + 'Skipped adding User Extra Info: User Info is currently undefined. A user ID must be set before adding extra info. Please call setUserInfo() first.', + SdkVerbosity.WARN + ); + + return; + } const updatedUserInfo = { ...userInfo, extraInfo: { diff --git a/packages/core/src/__tests__/DdSdkReactNative.test.tsx b/packages/core/src/__tests__/DdSdkReactNative.test.tsx index 18bf060ce..f9405aa51 100644 --- a/packages/core/src/__tests__/DdSdkReactNative.test.tsx +++ b/packages/core/src/__tests__/DdSdkReactNative.test.tsx @@ -63,7 +63,6 @@ beforeEach(async () => { DdSdkReactNative['wasAutoInstrumented'] = false; NativeModules.DdSdk.initialize.mockClear(); NativeModules.DdSdk.setAttributes.mockClear(); - NativeModules.DdSdk.setUser.mockClear(); NativeModules.DdSdk.setTrackingConsent.mockClear(); NativeModules.DdSdk.onRUMSessionStarted.mockClear(); @@ -1064,21 +1063,6 @@ describe('DdSdkReactNative', () => { }); }); - describe('setUser', () => { - it('calls SDK method when setUser, and sets the user in UserProvider', async () => { - // GIVEN - const user = { id: 'id', foo: 'bar' }; - - // WHEN - await DdSdkReactNative.setUser(user); - - // THEN - expect(DdSdk.setUser).toHaveBeenCalledTimes(1); - expect(DdSdk.setUser).toHaveBeenCalledWith(user); - expect(UserInfoSingleton.getInstance().getUserInfo()).toEqual(user); - }); - }); - describe('setUserInfo', () => { it('calls SDK method when setUserInfo, and sets the user in UserProvider', async () => { // GIVEN diff --git a/packages/core/src/logs/eventMapper.ts b/packages/core/src/logs/eventMapper.ts index eb7b5f22c..939e882a5 100644 --- a/packages/core/src/logs/eventMapper.ts +++ b/packages/core/src/logs/eventMapper.ts @@ -31,13 +31,15 @@ export const formatRawLogToNativeEvent = ( export const formatRawLogToLogEvent = ( rawLog: RawLog | RawLogWithError, additionalInformation: { - userInfo: UserInfo; + userInfo?: UserInfo; attributes: Attributes; } ): LogEvent => { + const userInfo = additionalInformation?.userInfo; + return { ...rawLog, - userInfo: additionalInformation.userInfo, + ...(userInfo !== undefined ? { userInfo } : {}), attributes: additionalInformation.attributes }; }; diff --git a/packages/core/src/sdk/EventMappers/EventMapper.ts b/packages/core/src/sdk/EventMappers/EventMapper.ts index beafcf420..9ca252d72 100644 --- a/packages/core/src/sdk/EventMappers/EventMapper.ts +++ b/packages/core/src/sdk/EventMappers/EventMapper.ts @@ -15,7 +15,7 @@ import type { UserInfo } from '../UserInfoSingleton/types'; import { deepClone } from './utils/deepClone'; export type AdditionalEventDataForMapper = { - userInfo: UserInfo; + userInfo?: UserInfo; attributes: Attributes; }; diff --git a/packages/core/src/sdk/UserInfoSingleton/UserInfoSingleton.ts b/packages/core/src/sdk/UserInfoSingleton/UserInfoSingleton.ts index c3862aabc..26392d794 100644 --- a/packages/core/src/sdk/UserInfoSingleton/UserInfoSingleton.ts +++ b/packages/core/src/sdk/UserInfoSingleton/UserInfoSingleton.ts @@ -7,13 +7,13 @@ import type { UserInfo } from './types'; class UserInfoProvider { - private userInfo: UserInfo = {}; + private userInfo: UserInfo | undefined = undefined; setUserInfo = (userInfo: UserInfo) => { this.userInfo = userInfo; }; - getUserInfo = (): UserInfo => { + getUserInfo = (): UserInfo | undefined => { return this.userInfo; }; } diff --git a/packages/core/src/sdk/UserInfoSingleton/__tests__/UserInfoSingleton.test.ts b/packages/core/src/sdk/UserInfoSingleton/__tests__/UserInfoSingleton.test.ts index 78a722c01..1f7ae84e7 100644 --- a/packages/core/src/sdk/UserInfoSingleton/__tests__/UserInfoSingleton.test.ts +++ b/packages/core/src/sdk/UserInfoSingleton/__tests__/UserInfoSingleton.test.ts @@ -9,6 +9,7 @@ import { UserInfoSingleton } from '../UserInfoSingleton'; describe('UserInfoSingleton', () => { it('sets, returns and resets the user info', () => { UserInfoSingleton.getInstance().setUserInfo({ + id: 'test', email: 'user@mail.com', extraInfo: { loggedIn: true @@ -16,6 +17,7 @@ describe('UserInfoSingleton', () => { }); expect(UserInfoSingleton.getInstance().getUserInfo()).toEqual({ + id: 'test', email: 'user@mail.com', extraInfo: { loggedIn: true @@ -24,6 +26,8 @@ describe('UserInfoSingleton', () => { UserInfoSingleton.reset(); - expect(UserInfoSingleton.getInstance().getUserInfo()).toEqual({}); + expect(UserInfoSingleton.getInstance().getUserInfo()).toEqual( + undefined + ); }); }); diff --git a/packages/core/src/sdk/UserInfoSingleton/types.ts b/packages/core/src/sdk/UserInfoSingleton/types.ts index 97a03ae7f..dd14eb150 100644 --- a/packages/core/src/sdk/UserInfoSingleton/types.ts +++ b/packages/core/src/sdk/UserInfoSingleton/types.ts @@ -5,11 +5,8 @@ */ export type UserInfo = { - readonly id?: string /** @deprecated To be made mandatory when removing DdSdkReactnative.setUser */; + readonly id: string; readonly name?: string; readonly email?: string; readonly extraInfo?: Record; - readonly [ - key: string - ]: unknown /** @deprecated To be removed alongside DdSdkReactnative.setUser */; }; diff --git a/packages/core/src/specs/NativeDdSdk.ts b/packages/core/src/specs/NativeDdSdk.ts index 6f1ce82a5..bbf2572ee 100644 --- a/packages/core/src/specs/NativeDdSdk.ts +++ b/packages/core/src/specs/NativeDdSdk.ts @@ -31,13 +31,6 @@ export interface Spec extends TurboModule { */ setAttributes(attributes: Object): Promise; - /** - * Set the user information. - * @deprecated: Use setUserInfo instead - * @param user: The user object (use builtin attributes: 'id', 'email', 'name', and/or any custom attribute). - */ - setUser(user: Object): Promise; - /** * Set the user information. * @param user: The user object (use builtin attributes: 'id', 'email', 'name', and any custom attribute under extraInfo). diff --git a/packages/core/src/types.tsx b/packages/core/src/types.tsx index e1c5096fb..fe1a5895c 100644 --- a/packages/core/src/types.tsx +++ b/packages/core/src/types.tsx @@ -5,7 +5,6 @@ */ import type { BatchProcessingLevel } from './DdSdkReactNativeConfiguration'; -import type { UserInfo as UserInfoSingleton } from './sdk/UserInfoSingleton/types'; declare global { // eslint-disable-next-line no-var, vars-on-top @@ -90,13 +89,6 @@ export type DdSdkType = { */ setAttributes(attributes: object): Promise; - /** - * Sets the user information. - * @deprecated UserInfo id property is now mandatory (please user setUserInfo instead) - * @param user: The user object (use builtin attributes: 'id', 'email', 'name', and/or any custom attribute). - */ - setUser(user: object): Promise; - /** * Sets the user information. * @param id: A unique user identifier (relevant to your business domain) @@ -173,7 +165,7 @@ export type LogEvent = { readonly source?: ErrorSource; // readonly date: number; // TODO: RUMM-2446 & RUMM-2447 readonly status: LogStatus; - readonly userInfo: UserInfoSingleton; + readonly userInfo?: UserInfo; readonly attributes?: object; }; diff --git a/packages/react-native-apollo-client/__mocks__/react-native.ts b/packages/react-native-apollo-client/__mocks__/react-native.ts index b35df4e31..046ced2f6 100644 --- a/packages/react-native-apollo-client/__mocks__/react-native.ts +++ b/packages/react-native-apollo-client/__mocks__/react-native.ts @@ -18,9 +18,6 @@ actualRN.NativeModules.DdSdk = { initialize: jest.fn().mockImplementation( () => new Promise(resolve => resolve()) ) as jest.MockedFunction, - setUser: jest.fn().mockImplementation( - () => new Promise(resolve => resolve()) - ) as jest.MockedFunction, setAttributes: jest.fn().mockImplementation( () => new Promise(resolve => resolve()) ) as jest.MockedFunction, From b8d2a2eac9d062977b1a86fc1295a22e427bf2f5 Mon Sep 17 00:00:00 2001 From: Sergio Barrio Date: Fri, 12 Sep 2025 17:09:36 +0200 Subject: [PATCH 013/410] Update Tracer imports for Android to remove opentracing dependencies --- .../reactnative/DdTraceImplementation.kt | 39 ++++++++++--------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdTraceImplementation.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdTraceImplementation.kt index 224b6c994..c4226e69c 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdTraceImplementation.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdTraceImplementation.kt @@ -6,30 +6,28 @@ package com.datadog.reactnative -import com.datadog.android.trace.AndroidTracer -import com.datadog.android.trace.Trace -import com.datadog.android.trace.TraceConfiguration +import com.datadog.android.Datadog +import com.datadog.android.trace.DatadogTracing import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReadableMap -import io.opentracing.Scope -import io.opentracing.Span -import io.opentracing.Tracer -import io.opentracing.util.GlobalTracer +import com.datadog.android.trace.api.scope.DatadogScope +import com.datadog.android.trace.api.span.DatadogSpan +import com.datadog.android.trace.api.tracer.DatadogTracer +import com.datadog.android.trace.GlobalDatadogTracer import java.util.concurrent.TimeUnit /** * The entry point to use Datadog's Trace feature. */ class DdTraceImplementation( - private val tracerProvider: () -> Tracer = { - val tracer = AndroidTracer.Builder().build() - GlobalTracer.registerIfAbsent(tracer) - - GlobalTracer.get() + private val tracerProvider: () -> DatadogTracer = { + val tracer = DatadogTracing.newTracerBuilder(Datadog.getInstance()).build() + GlobalDatadogTracer.registerIfAbsent(tracer) + GlobalDatadogTracer.get() } ) { - private val spanMap: MutableMap = mutableMapOf() - private val scopeMap: MutableMap = mutableMapOf() + private val spanMap: MutableMap = mutableMapOf() + private val scopeMap: MutableMap = mutableMapOf() // lazy here is on purpose. The thing is that this class will be instantiated even // before Sdk.initialize is called, but Tracer can be created only after SDK is initialized. @@ -47,15 +45,18 @@ class DdTraceImplementation( .start() // This is required for traces to be able to be bundled with logs. - val scope = tracer.scopeManager().activate(span) - + val scope = tracer.activateSpan(span) val spanContext = span.context() span.setTags(context.toHashMap()) span.setTags(GlobalState.globalAttributes) - val spanId = spanContext.toSpanId() + val spanId = spanContext.spanId.toString() + spanMap[spanId] = span - scopeMap[spanId] = scope + if (scope != null) { + scopeMap[spanId] = scope + } + promise.resolve(spanId) } @@ -82,7 +83,7 @@ class DdTraceImplementation( promise.resolve(null) } - private fun Span.setTags(tags: Map) { + private fun DatadogSpan.setTags(tags: Map) { for ((key, value) in tags) { when (value) { is Boolean -> setTag(key, value) From 805419f9627191f0e22b8a102424300a7641cfa0 Mon Sep 17 00:00:00 2001 From: Sergio Barrio Date: Thu, 18 Sep 2025 10:13:53 +0200 Subject: [PATCH 014/410] Fix android tests --- .../com/datadog/reactnative/DdTraceTest.kt | 84 ++++++++++--------- .../com/datadog/tools/unit/MockRumMonitor.kt | 10 +-- 2 files changed, 47 insertions(+), 47 deletions(-) diff --git a/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdTraceTest.kt b/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdTraceTest.kt index 16d459a57..8c22f88e1 100644 --- a/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdTraceTest.kt +++ b/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdTraceTest.kt @@ -6,6 +6,12 @@ package com.datadog.reactnative +import com.datadog.android.trace.api.scope.DatadogScope +import com.datadog.android.trace.api.span.DatadogSpan +import com.datadog.android.trace.api.span.DatadogSpanBuilder +import com.datadog.android.trace.api.span.DatadogSpanContext +import com.datadog.android.trace.api.trace.DatadogTraceId +import com.datadog.android.trace.api.tracer.DatadogTracer import com.datadog.tools.unit.toReadableMap import com.facebook.react.bridge.Promise import fr.xgouchet.elmyr.annotation.AdvancedForgery @@ -15,11 +21,6 @@ import fr.xgouchet.elmyr.annotation.MapForgery import fr.xgouchet.elmyr.annotation.StringForgery import fr.xgouchet.elmyr.annotation.StringForgeryType import fr.xgouchet.elmyr.junit5.ForgeExtension -import io.opentracing.Scope -import io.opentracing.ScopeManager -import io.opentracing.Span -import io.opentracing.SpanContext -import io.opentracing.Tracer import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Assumptions.assumeTrue @@ -51,22 +52,19 @@ internal class DdTraceTest { lateinit var testedTrace: DdTraceImplementation @Mock - lateinit var mockTracer: Tracer + lateinit var mockTracer: DatadogTracer @Mock - lateinit var mockSpanBuilder: Tracer.SpanBuilder + lateinit var mockSpanBuilder: DatadogSpanBuilder @Mock - lateinit var mockSpanContext: SpanContext + lateinit var mockSpanContext: DatadogSpanContext @Mock - lateinit var mockScopeManager: ScopeManager + lateinit var mockSpan: DatadogSpan @Mock - lateinit var mockSpan: Span - - @Mock - lateinit var mockScope: Scope + lateinit var mockScope: DatadogScope @StringForgery lateinit var fakeOperation: String @@ -74,11 +72,11 @@ internal class DdTraceTest { @DoubleForgery(1000000000000.0, 2000000000000.0) var fakeTimestamp: Double = 0.0 - @StringForgery(type = StringForgeryType.HEXADECIMAL) - lateinit var fakeSpanId: String + @LongForgery(100L, 2000L) + var fakeSpanId: Long = 0 - @StringForgery(type = StringForgeryType.HEXADECIMAL) - lateinit var fakeTraceId: String + @Mock + lateinit var fakeTraceId: DatadogTraceId @MapForgery( key = AdvancedForgery(string = [StringForgery()]), @@ -102,7 +100,6 @@ internal class DdTraceTest { @BeforeEach fun `set up`() { whenever(mockTracer.buildSpan(fakeOperation)) doReturn mockSpanBuilder - whenever(mockTracer.scopeManager()) doReturn mockScopeManager whenever( mockSpanBuilder.withStartTimestamp( fakeTimestamp.toLong() * 1000 @@ -110,9 +107,9 @@ internal class DdTraceTest { ) doReturn mockSpanBuilder whenever(mockSpanBuilder.start()) doReturn mockSpan whenever(mockSpan.context()) doReturn mockSpanContext - whenever(mockSpanContext.toSpanId()) doReturn fakeSpanId - whenever(mockSpanContext.toTraceId()) doReturn fakeTraceId - whenever(mockScopeManager.activate(mockSpan)) doReturn mockScope + whenever(mockSpanContext.spanId) doReturn fakeSpanId + whenever(mockSpanContext.traceId) doReturn fakeTraceId + whenever(mockTracer.activateSpan(mockSpan)) doReturn mockScope testedTrace = DdTraceImplementation(tracerProvider = { mockTracer }) } @@ -133,7 +130,7 @@ internal class DdTraceTest { ) // Then - assertThat(lastResolvedValue).isEqualTo(fakeSpanId) + assertThat(lastResolvedValue.toString()).isEqualTo(fakeSpanId.toString()) } @Test @@ -154,18 +151,20 @@ internal class DdTraceTest { fakeTimestamp, mockPromise ) - val id = lastResolvedValue - testedTrace.finishSpan(id as String, fakeContext.toReadableMap(), endTimestamp, mockPromise) + val id = lastResolvedValue.toString() + testedTrace.finishSpan(id, fakeContext.toReadableMap(), endTimestamp, mockPromise) // Then - assertThat(id).isEqualTo(fakeSpanId) + assertThat(id).isEqualTo(fakeSpanId.toString()) verify(mockSpan).finish(endTimestamp.toLong() * 1000) } @Test fun `M do nothing W startSpan() + finishSpan() with unknown id`( @LongForgery(100L, 2000L) duration: Long, - @StringForgery(type = StringForgeryType.HEXADECIMAL) otherSpanId: String + @StringForgery(type = StringForgeryType.HEXADECIMAL) + @LongForgery(100L, 2000L) + otherSpanId: Long ) { // Given assumeTrue(otherSpanId != fakeSpanId) @@ -178,11 +177,16 @@ internal class DdTraceTest { fakeTimestamp, mockPromise ) - val id = lastResolvedValue - testedTrace.finishSpan(otherSpanId, fakeContext.toReadableMap(), endTimestamp, mockPromise) + val id = lastResolvedValue.toString() + testedTrace.finishSpan( + otherSpanId.toString(), + fakeContext.toReadableMap(), + endTimestamp, + mockPromise + ) // Then - assertThat(id).isEqualTo(fakeSpanId) + assertThat(id).isEqualTo(fakeSpanId.toString()) verify(mockSpan, never()).finish(any()) } @@ -200,7 +204,7 @@ internal class DdTraceTest { fakeTimestamp, mockPromise ) - val id = lastResolvedValue + val id = lastResolvedValue.toString() testedTrace.finishSpan( id as String, emptyMap().toReadableMap(), @@ -209,7 +213,7 @@ internal class DdTraceTest { ) // Then - assertThat(id).isEqualTo(fakeSpanId) + assertThat(id).isEqualTo(fakeSpanId.toString()) verify(mockSpan).context() verify(mockSpan).finish(endTimestamp.toLong() * 1000) fakeContext.forEach { @@ -232,11 +236,11 @@ internal class DdTraceTest { fakeTimestamp, mockPromise ) - val id = lastResolvedValue - testedTrace.finishSpan(id as String, fakeContext.toReadableMap(), endTimestamp, mockPromise) + val id = lastResolvedValue.toString() + testedTrace.finishSpan(id, fakeContext.toReadableMap(), endTimestamp, mockPromise) // Then - assertThat(id).isEqualTo(fakeSpanId) + assertThat(id).isEqualTo(fakeSpanId.toString()) verify(mockSpan).context() verify(mockSpan).finish(endTimestamp.toLong() * 1000) fakeContext.forEach { @@ -262,16 +266,16 @@ internal class DdTraceTest { fakeTimestamp, mockPromise ) - val id = lastResolvedValue + val id = lastResolvedValue.toString() testedTrace.finishSpan( - id as String, + id, emptyMap().toReadableMap(), endTimestamp, mockPromise ) // Then - assertThat(id).isEqualTo(fakeSpanId) + assertThat(id).isEqualTo(fakeSpanId.toString()) verify(mockSpan).context() verify(mockSpan).finish(endTimestamp.toLong() * 1000) fakeContext.forEach { @@ -298,14 +302,14 @@ internal class DdTraceTest { fakeTimestamp, mockPromise ) - val id = lastResolvedValue + val id = lastResolvedValue.toString() fakeGlobalState.forEach { (k, v) -> GlobalState.addAttribute(k, v) } - testedTrace.finishSpan(id as String, fakeContext.toReadableMap(), endTimestamp, mockPromise) + testedTrace.finishSpan(id, fakeContext.toReadableMap(), endTimestamp, mockPromise) // Then - assertThat(id).isEqualTo(fakeSpanId) + assertThat(id).isEqualTo(fakeSpanId.toString()) verify(mockSpan).context() verify(mockSpan).finish(endTimestamp.toLong() * 1000) expectedAttributes.forEach { diff --git a/packages/core/android/src/test/kotlin/com/datadog/tools/unit/MockRumMonitor.kt b/packages/core/android/src/test/kotlin/com/datadog/tools/unit/MockRumMonitor.kt index a2e79d630..7c5585edd 100644 --- a/packages/core/android/src/test/kotlin/com/datadog/tools/unit/MockRumMonitor.kt +++ b/packages/core/android/src/test/kotlin/com/datadog/tools/unit/MockRumMonitor.kt @@ -29,6 +29,8 @@ class MockRumMonitor : RumMonitor { override fun addAttribute(key: String, value: Any?) {} + override fun addViewAttributes(attributes: Map) {} + override fun addError( message: String, source: RumErrorSource, @@ -61,6 +63,7 @@ class MockRumMonitor : RumMonitor { override fun getCurrentSessionId(callback: (String?) -> Unit) {} override fun removeAttribute(key: String) {} + override fun removeViewAttributes(attributes: Collection) {} override fun startAction( type: RumActionType, @@ -75,13 +78,6 @@ class MockRumMonitor : RumMonitor { attributes: Map ) {} - override fun startResource( - key: String, - method: String, - url: String, - attributes: Map - ) {} - override fun startView( key: Any, name: String, From beeb2d6e738c03ebf2409c683c7bc91bd4aaaa35 Mon Sep 17 00:00:00 2001 From: Sergio Barrio Date: Thu, 18 Sep 2025 10:50:27 +0200 Subject: [PATCH 015/410] Fix iOS tests --- packages/core/ios/Tests/DdSdkTests.swift | 2 +- packages/core/ios/Tests/RUMMocks.swift | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/core/ios/Tests/DdSdkTests.swift b/packages/core/ios/Tests/DdSdkTests.swift index 6be5e0994..555ce4549 100644 --- a/packages/core/ios/Tests/DdSdkTests.swift +++ b/packages/core/ios/Tests/DdSdkTests.swift @@ -738,7 +738,7 @@ class DdSdkTests: XCTestCase { XCTAssertEqual(actualFirstPartyHosts, expectedFirstPartyHosts) XCTAssertEqual(actualTracingSamplingRate, 66) - XCTAssertEqual(actualTraceContextInjection, .all) + XCTAssertEqual(actualTraceContextInjection, .sampled) } func testBuildTelemetrySampleRate() { diff --git a/packages/core/ios/Tests/RUMMocks.swift b/packages/core/ios/Tests/RUMMocks.swift index 6a6fd94e7..01d0f9d0d 100644 --- a/packages/core/ios/Tests/RUMMocks.swift +++ b/packages/core/ios/Tests/RUMMocks.swift @@ -213,14 +213,14 @@ extension RUMActionID: RandomMockable { } } -extension RUMDevice.RUMDeviceType: RandomMockable { - static func mockRandom() -> RUMDevice.RUMDeviceType { +extension Device.DeviceType: RandomMockable { + static func mockRandom() -> Device.DeviceType { return [.mobile, .desktop, .tablet, .tv, .gamingConsole, .bot, .other].randomElement()! } } -extension RUMDevice: RandomMockable { - static func mockRandom() -> RUMDevice { +extension Device: RandomMockable { + static func mockRandom() -> Device { return .init( architecture: .mockRandom(), brand: .mockRandom(), @@ -231,8 +231,8 @@ extension RUMDevice: RandomMockable { } } -extension RUMOperatingSystem: RandomMockable { - static func mockRandom() -> RUMOperatingSystem { +extension OperatingSystem: RandomMockable { + static func mockRandom() -> OperatingSystem { return .init( build: .mockRandom(length: 5), name: .mockRandom(length: 5), From e1c80fd03979d4f1e9ff885a35a1917f501b8ad1 Mon Sep 17 00:00:00 2001 From: Sergio Barrio Date: Fri, 19 Sep 2025 11:45:00 +0200 Subject: [PATCH 016/410] Bump Native SDKs to 3.1.0 --- benchmarks/android/app/build.gradle | 2 +- benchmarks/ios/Podfile.lock | 70 +++++++-------- example-new-architecture/ios/Podfile.lock | 66 +++++++------- example/ios/Podfile.lock | 88 +++++++++---------- packages/core/DatadogSDKReactNative.podspec | 12 +-- packages/core/android/build.gradle | 10 +-- .../com/datadog/tools/unit/MockRumMonitor.kt | 23 +++++ packages/core/ios/Tests/DdLogsTests.swift | 2 + ...DatadogSDKReactNativeSessionReplay.podspec | 2 +- .../android/build.gradle | 4 +- .../DatadogSDKReactNativeWebView.podspec | 4 +- .../react-native-webview/android/build.gradle | 2 +- 12 files changed, 155 insertions(+), 130 deletions(-) diff --git a/benchmarks/android/app/build.gradle b/benchmarks/android/app/build.gradle index 7dc4b3707..04c240bd0 100644 --- a/benchmarks/android/app/build.gradle +++ b/benchmarks/android/app/build.gradle @@ -129,5 +129,5 @@ dependencies { // Benchmark tools from dd-sdk-android are used for vitals recording // Remember to bump thid alongside the main dd-sdk-android dependencies - implementation("com.datadoghq:dd-sdk-android-benchmark-internal:3.0.0") + implementation("com.datadoghq:dd-sdk-android-benchmark-internal:3.1.0") } diff --git a/benchmarks/ios/Podfile.lock b/benchmarks/ios/Podfile.lock index d27610b2c..918c795b5 100644 --- a/benchmarks/ios/Podfile.lock +++ b/benchmarks/ios/Podfile.lock @@ -1,22 +1,22 @@ PODS: - boost (1.84.0) - - DatadogCore (3.0.0): - - DatadogInternal (= 3.0.0) - - DatadogCrashReporting (3.0.0): - - DatadogInternal (= 3.0.0) + - DatadogCore (3.1.0): + - DatadogInternal (= 3.1.0) + - DatadogCrashReporting (3.1.0): + - DatadogInternal (= 3.1.0) - PLCrashReporter (~> 1.12.0) - - DatadogInternal (3.0.0) - - DatadogLogs (3.0.0): - - DatadogInternal (= 3.0.0) - - DatadogRUM (3.0.0): - - DatadogInternal (= 3.0.0) + - DatadogInternal (3.1.0) + - DatadogLogs (3.1.0): + - DatadogInternal (= 3.1.0) + - DatadogRUM (3.1.0): + - DatadogInternal (= 3.1.0) - DatadogSDKReactNative (2.12.1): - - DatadogCore (= 3.0.0) - - DatadogCrashReporting (= 3.0.0) - - DatadogLogs (= 3.0.0) - - DatadogRUM (= 3.0.0) - - DatadogTrace (= 3.0.0) - - DatadogWebViewTracking (= 3.0.0) + - DatadogCore (= 3.1.0) + - DatadogCrashReporting (= 3.1.0) + - DatadogLogs (= 3.1.0) + - DatadogRUM (= 3.1.0) + - DatadogTrace (= 3.1.0) + - DatadogWebViewTracking (= 3.1.0) - DoubleConversion - glog - hermes-engine @@ -39,7 +39,7 @@ PODS: - Yoga - DatadogSDKReactNativeSessionReplay (2.12.1): - DatadogSDKReactNative - - DatadogSessionReplay (= 3.0.0) + - DatadogSessionReplay (= 3.1.0) - DoubleConversion - glog - hermes-engine @@ -61,9 +61,9 @@ PODS: - ReactCommon/turbomodule/core - Yoga - DatadogSDKReactNativeWebView (2.12.1): - - DatadogInternal (= 3.0.0) + - DatadogInternal (= 3.1.0) - DatadogSDKReactNative - - DatadogWebViewTracking (= 3.0.0) + - DatadogWebViewTracking (= 3.1.0) - DoubleConversion - glog - hermes-engine @@ -84,13 +84,13 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - DatadogSessionReplay (3.0.0): - - DatadogInternal (= 3.0.0) - - DatadogTrace (3.0.0): - - DatadogInternal (= 3.0.0) + - DatadogSessionReplay (3.1.0): + - DatadogInternal (= 3.1.0) + - DatadogTrace (3.1.0): + - DatadogInternal (= 3.1.0) - OpenTelemetrySwiftApi (= 1.13.1) - - DatadogWebViewTracking (3.0.0): - - DatadogInternal (= 3.0.0) + - DatadogWebViewTracking (3.1.0): + - DatadogInternal (= 3.1.0) - DoubleConversion (1.1.6) - fast_float (6.1.4) - FBLazyVector (0.78.2) @@ -2069,17 +2069,17 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: boost: 7e761d76ca2ce687f7cc98e698152abd03a18f90 - DatadogCore: 0c39ba4ef3dee02071f235f2fb56af7e29f4ceb0 - DatadogCrashReporting: 604e65e524cb2fc22cda39fd9c9ea5fd3bddf24e - DatadogInternal: 442673a7fb5329299b489b91ed705aa2085380f7 - DatadogLogs: 0146a0e3140ba62fd42729593c0910d692aea5fd - DatadogRUM: f97fbd0290ecec5bc952fb03a6a1ae068649243f - DatadogSDKReactNative: 241bf982c16ceff03d94a58e6d005e55c47b99c6 - DatadogSDKReactNativeSessionReplay: bba36092686e3183e97c1a0c7f4ca8142582ae28 - DatadogSDKReactNativeWebView: 6da060df20e235abac533e582d9fc2b3a5070840 - DatadogSessionReplay: 0357b911c6ab42c77c93b29761ecc20d9282e971 - DatadogTrace: d714e7c456e612b7f171483d08086a59aa1c4213 - DatadogWebViewTracking: fb9591c09fca82b143f3843a7164a7e782175c53 + DatadogCore: d2f51c7fb4308cf3c25e55e2e7242e5d558ee71d + DatadogCrashReporting: f636f1d1c534572c0b0abcdc59df244c884d825d + DatadogInternal: 7837b2ce3d525d429682532eeda697b181299fdc + DatadogLogs: 250894b5a99da5b924a019049c0d0326823cdbd6 + DatadogRUM: 0d2a60e1abb8aacfb8827ef84f6d5deb4d5026c8 + DatadogSDKReactNative: 8e0f39de38621d4d7ed961a74d8a216fd3a38321 + DatadogSDKReactNativeSessionReplay: f9288c8e981dcc65d1f727b01421ee9a7601e75f + DatadogSDKReactNativeWebView: 993527f6c5d38e0fcc4804a6a60c334dd199dc5b + DatadogSessionReplay: 6bc71888e2b41dd0de3325f06f0c0b3cee0e6df4 + DatadogTrace: f59e933074cd285ad7e9f5af991f8fe04b095991 + DatadogWebViewTracking: 9bc92b4147aeed47eb1911451f651094aa6dd6c1 DoubleConversion: cb417026b2400c8f53ae97020b2be961b59470cb fast_float: 06eeec4fe712a76acc9376682e4808b05ce978b6 FBLazyVector: e32d34492c519a2194ec9d7f5e7a79d11b73f91c diff --git a/example-new-architecture/ios/Podfile.lock b/example-new-architecture/ios/Podfile.lock index 281513566..c0cd90bf6 100644 --- a/example-new-architecture/ios/Podfile.lock +++ b/example-new-architecture/ios/Podfile.lock @@ -1,22 +1,22 @@ PODS: - boost (1.84.0) - - DatadogCore (3.0.0): - - DatadogInternal (= 3.0.0) - - DatadogCrashReporting (3.0.0): - - DatadogInternal (= 3.0.0) + - DatadogCore (3.1.0): + - DatadogInternal (= 3.1.0) + - DatadogCrashReporting (3.1.0): + - DatadogInternal (= 3.1.0) - PLCrashReporter (~> 1.12.0) - - DatadogInternal (3.0.0) - - DatadogLogs (3.0.0): - - DatadogInternal (= 3.0.0) - - DatadogRUM (3.0.0): - - DatadogInternal (= 3.0.0) + - DatadogInternal (3.1.0) + - DatadogLogs (3.1.0): + - DatadogInternal (= 3.1.0) + - DatadogRUM (3.1.0): + - DatadogInternal (= 3.1.0) - DatadogSDKReactNative (2.12.1): - - DatadogCore (= 3.0.0) - - DatadogCrashReporting (= 3.0.0) - - DatadogLogs (= 3.0.0) - - DatadogRUM (= 3.0.0) - - DatadogTrace (= 3.0.0) - - DatadogWebViewTracking (= 3.0.0) + - DatadogCore (= 3.1.0) + - DatadogCrashReporting (= 3.1.0) + - DatadogLogs (= 3.1.0) + - DatadogRUM (= 3.1.0) + - DatadogTrace (= 3.1.0) + - DatadogWebViewTracking (= 3.1.0) - DoubleConversion - glog - hermes-engine @@ -38,12 +38,12 @@ PODS: - ReactCommon/turbomodule/core - Yoga - DatadogSDKReactNative/Tests (2.12.1): - - DatadogCore (= 3.0.0) - - DatadogCrashReporting (= 3.0.0) - - DatadogLogs (= 3.0.0) - - DatadogRUM (= 3.0.0) - - DatadogTrace (= 3.0.0) - - DatadogWebViewTracking (= 3.0.0) + - DatadogCore (= 3.1.0) + - DatadogCrashReporting (= 3.1.0) + - DatadogLogs (= 3.1.0) + - DatadogRUM (= 3.1.0) + - DatadogTrace (= 3.1.0) + - DatadogWebViewTracking (= 3.1.0) - DoubleConversion - glog - hermes-engine @@ -64,11 +64,11 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - DatadogTrace (3.0.0): - - DatadogInternal (= 3.0.0) + - DatadogTrace (3.1.0): + - DatadogInternal (= 3.1.0) - OpenTelemetrySwiftApi (= 1.13.1) - - DatadogWebViewTracking (3.0.0): - - DatadogInternal (= 3.0.0) + - DatadogWebViewTracking (3.1.0): + - DatadogInternal (= 3.1.0) - DoubleConversion (1.1.6) - fast_float (6.1.4) - FBLazyVector (0.76.9) @@ -1850,14 +1850,14 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: boost: 1dca942403ed9342f98334bf4c3621f011aa7946 - DatadogCore: 0c39ba4ef3dee02071f235f2fb56af7e29f4ceb0 - DatadogCrashReporting: 604e65e524cb2fc22cda39fd9c9ea5fd3bddf24e - DatadogInternal: 442673a7fb5329299b489b91ed705aa2085380f7 - DatadogLogs: 0146a0e3140ba62fd42729593c0910d692aea5fd - DatadogRUM: f97fbd0290ecec5bc952fb03a6a1ae068649243f - DatadogSDKReactNative: 0a80aa75958d595a99be54d2838db53eda7a08af - DatadogTrace: d714e7c456e612b7f171483d08086a59aa1c4213 - DatadogWebViewTracking: fb9591c09fca82b143f3843a7164a7e782175c53 + DatadogCore: d2f51c7fb4308cf3c25e55e2e7242e5d558ee71d + DatadogCrashReporting: f636f1d1c534572c0b0abcdc59df244c884d825d + DatadogInternal: 7837b2ce3d525d429682532eeda697b181299fdc + DatadogLogs: 250894b5a99da5b924a019049c0d0326823cdbd6 + DatadogRUM: 0d2a60e1abb8aacfb8827ef84f6d5deb4d5026c8 + DatadogSDKReactNative: 069ea9876220b2d09b0f4b180ce571b1b6ecbb35 + DatadogTrace: f59e933074cd285ad7e9f5af991f8fe04b095991 + DatadogWebViewTracking: 9bc92b4147aeed47eb1911451f651094aa6dd6c1 DoubleConversion: f16ae600a246532c4020132d54af21d0ddb2a385 fast_float: 06eeec4fe712a76acc9376682e4808b05ce978b6 FBLazyVector: 7605ea4810e0e10ae4815292433c09bf4324ba45 diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 00643c0f1..00338334c 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -1,34 +1,34 @@ PODS: - boost (1.84.0) - - DatadogCore (3.0.0): - - DatadogInternal (= 3.0.0) - - DatadogCrashReporting (3.0.0): - - DatadogInternal (= 3.0.0) + - DatadogCore (3.1.0): + - DatadogInternal (= 3.1.0) + - DatadogCrashReporting (3.1.0): + - DatadogInternal (= 3.1.0) - PLCrashReporter (~> 1.12.0) - - DatadogInternal (3.0.0) - - DatadogLogs (3.0.0): - - DatadogInternal (= 3.0.0) - - DatadogRUM (3.0.0): - - DatadogInternal (= 3.0.0) + - DatadogInternal (3.1.0) + - DatadogLogs (3.1.0): + - DatadogInternal (= 3.1.0) + - DatadogRUM (3.1.0): + - DatadogInternal (= 3.1.0) - DatadogSDKReactNative (2.12.1): - - DatadogCore (= 3.0.0) - - DatadogCrashReporting (= 3.0.0) - - DatadogLogs (= 3.0.0) - - DatadogRUM (= 3.0.0) - - DatadogTrace (= 3.0.0) - - DatadogWebViewTracking (= 3.0.0) + - DatadogCore (= 3.1.0) + - DatadogCrashReporting (= 3.1.0) + - DatadogLogs (= 3.1.0) + - DatadogRUM (= 3.1.0) + - DatadogTrace (= 3.1.0) + - DatadogWebViewTracking (= 3.1.0) - React-Core - DatadogSDKReactNative/Tests (2.12.1): - - DatadogCore (= 3.0.0) - - DatadogCrashReporting (= 3.0.0) - - DatadogLogs (= 3.0.0) - - DatadogRUM (= 3.0.0) - - DatadogTrace (= 3.0.0) - - DatadogWebViewTracking (= 3.0.0) + - DatadogCore (= 3.1.0) + - DatadogCrashReporting (= 3.1.0) + - DatadogLogs (= 3.1.0) + - DatadogRUM (= 3.1.0) + - DatadogTrace (= 3.1.0) + - DatadogWebViewTracking (= 3.1.0) - React-Core - DatadogSDKReactNativeSessionReplay (2.12.1): - DatadogSDKReactNative - - DatadogSessionReplay (= 3.0.0) + - DatadogSessionReplay (= 3.1.0) - DoubleConversion - glog - hermes-engine @@ -51,7 +51,7 @@ PODS: - Yoga - DatadogSDKReactNativeSessionReplay/Tests (2.12.1): - DatadogSDKReactNative - - DatadogSessionReplay (= 3.0.0) + - DatadogSessionReplay (= 3.1.0) - DoubleConversion - glog - hermes-engine @@ -74,24 +74,24 @@ PODS: - ReactCommon/turbomodule/core - Yoga - DatadogSDKReactNativeWebView (2.12.1): - - DatadogInternal (= 3.0.0) + - DatadogInternal (= 3.1.0) - DatadogSDKReactNative - - DatadogWebViewTracking (= 3.0.0) + - DatadogWebViewTracking (= 3.1.0) - React-Core - DatadogSDKReactNativeWebView/Tests (2.12.1): - - DatadogInternal (= 3.0.0) + - DatadogInternal (= 3.1.0) - DatadogSDKReactNative - - DatadogWebViewTracking (= 3.0.0) + - DatadogWebViewTracking (= 3.1.0) - React-Core - react-native-webview - React-RCTText - - DatadogSessionReplay (3.0.0): - - DatadogInternal (= 3.0.0) - - DatadogTrace (3.0.0): - - DatadogInternal (= 3.0.0) + - DatadogSessionReplay (3.1.0): + - DatadogInternal (= 3.1.0) + - DatadogTrace (3.1.0): + - DatadogInternal (= 3.1.0) - OpenTelemetrySwiftApi (= 1.13.1) - - DatadogWebViewTracking (3.0.0): - - DatadogInternal (= 3.0.0) + - DatadogWebViewTracking (3.1.0): + - DatadogInternal (= 3.1.0) - DoubleConversion (1.1.6) - fast_float (6.1.4) - FBLazyVector (0.76.9) @@ -1988,17 +1988,17 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: boost: 1dca942403ed9342f98334bf4c3621f011aa7946 - DatadogCore: 0c39ba4ef3dee02071f235f2fb56af7e29f4ceb0 - DatadogCrashReporting: 604e65e524cb2fc22cda39fd9c9ea5fd3bddf24e - DatadogInternal: 442673a7fb5329299b489b91ed705aa2085380f7 - DatadogLogs: 0146a0e3140ba62fd42729593c0910d692aea5fd - DatadogRUM: f97fbd0290ecec5bc952fb03a6a1ae068649243f - DatadogSDKReactNative: e430b3f4d7adb0fac07b61e2fb65ceacdaf82b3e - DatadogSDKReactNativeSessionReplay: 4b2a3d166a79581f18522795b40141c34cf3685d - DatadogSDKReactNativeWebView: 35dc2b9736e1aaa82b366bf6b8a8a959a9b088c5 - DatadogSessionReplay: 0357b911c6ab42c77c93b29761ecc20d9282e971 - DatadogTrace: d714e7c456e612b7f171483d08086a59aa1c4213 - DatadogWebViewTracking: fb9591c09fca82b143f3843a7164a7e782175c53 + DatadogCore: d2f51c7fb4308cf3c25e55e2e7242e5d558ee71d + DatadogCrashReporting: f636f1d1c534572c0b0abcdc59df244c884d825d + DatadogInternal: 7837b2ce3d525d429682532eeda697b181299fdc + DatadogLogs: 250894b5a99da5b924a019049c0d0326823cdbd6 + DatadogRUM: 0d2a60e1abb8aacfb8827ef84f6d5deb4d5026c8 + DatadogSDKReactNative: af351a4e1ce08124c290c52de94b0062a166cc67 + DatadogSDKReactNativeSessionReplay: dcbd55d9d0f2b86026996a8b7ec9654922d5dfe1 + DatadogSDKReactNativeWebView: 096ac87eb753b6a217b93441983264b9837c3b7e + DatadogSessionReplay: 6bc71888e2b41dd0de3325f06f0c0b3cee0e6df4 + DatadogTrace: f59e933074cd285ad7e9f5af991f8fe04b095991 + DatadogWebViewTracking: 9bc92b4147aeed47eb1911451f651094aa6dd6c1 DoubleConversion: f16ae600a246532c4020132d54af21d0ddb2a385 fast_float: 06eeec4fe712a76acc9376682e4808b05ce978b6 FBLazyVector: 7605ea4810e0e10ae4815292433c09bf4324ba45 diff --git a/packages/core/DatadogSDKReactNative.podspec b/packages/core/DatadogSDKReactNative.podspec index a2054d6ca..3f5026f5a 100644 --- a/packages/core/DatadogSDKReactNative.podspec +++ b/packages/core/DatadogSDKReactNative.podspec @@ -19,14 +19,14 @@ Pod::Spec.new do |s| s.dependency "React-Core" # /!\ Remember to keep the versions in sync with DatadogSDKReactNativeSessionReplay.podspec - s.dependency 'DatadogCore', '3.0.0' - s.dependency 'DatadogLogs', '3.0.0' - s.dependency 'DatadogTrace', '3.0.0' - s.dependency 'DatadogRUM', '3.0.0' - s.dependency 'DatadogCrashReporting', '3.0.0' + s.dependency 'DatadogCore', '3.1.0' + s.dependency 'DatadogLogs', '3.1.0' + s.dependency 'DatadogTrace', '3.1.0' + s.dependency 'DatadogRUM', '3.1.0' + s.dependency 'DatadogCrashReporting', '3.1.0' # DatadogWebViewTracking is not available for tvOS - s.ios.dependency 'DatadogWebViewTracking', '3.0.0' + s.ios.dependency 'DatadogWebViewTracking', '3.1.0' s.test_spec 'Tests' do |test_spec| test_spec.source_files = 'ios/Tests/**/*.{swift,json}' diff --git a/packages/core/android/build.gradle b/packages/core/android/build.gradle index 17b26cdbd..2b893ac37 100644 --- a/packages/core/android/build.gradle +++ b/packages/core/android/build.gradle @@ -197,16 +197,16 @@ dependencies { // This breaks builds if the React Native target is below 0.76.0. as it relies on Gradle 8.5.0. // To avoid this, we enforce 1.0.0-beta01 on RN < 0.76.0 if (reactNativeMinorVersion < 76) { - implementation("com.datadoghq:dd-sdk-android-rum:3.0.0") { + implementation("com.datadoghq:dd-sdk-android-rum:3.1.0") { exclude group: "androidx.metrics", module: "metrics-performance" } implementation "androidx.metrics:metrics-performance:1.0.0-beta01" } else { - implementation "com.datadoghq:dd-sdk-android-rum:3.0.0" + implementation "com.datadoghq:dd-sdk-android-rum:3.1.0" } - implementation "com.datadoghq:dd-sdk-android-logs:3.0.0" - implementation "com.datadoghq:dd-sdk-android-trace:3.0.0" - implementation "com.datadoghq:dd-sdk-android-webview:3.0.0" + implementation "com.datadoghq:dd-sdk-android-logs:3.1.0" + implementation "com.datadoghq:dd-sdk-android-trace:3.1.0" + implementation "com.datadoghq:dd-sdk-android-webview:3.1.0" implementation "com.google.code.gson:gson:2.10.0" testImplementation "org.junit.platform:junit-platform-launcher:1.6.2" testImplementation "org.junit.jupiter:junit-jupiter-api:5.6.2" diff --git a/packages/core/android/src/test/kotlin/com/datadog/tools/unit/MockRumMonitor.kt b/packages/core/android/src/test/kotlin/com/datadog/tools/unit/MockRumMonitor.kt index 7c5585edd..702cc2533 100644 --- a/packages/core/android/src/test/kotlin/com/datadog/tools/unit/MockRumMonitor.kt +++ b/packages/core/android/src/test/kotlin/com/datadog/tools/unit/MockRumMonitor.kt @@ -13,6 +13,7 @@ import com.datadog.android.rum.RumMonitor import com.datadog.android.rum.RumResourceKind import com.datadog.android.rum.RumResourceMethod import com.datadog.android.rum._RumInternalProxy +import com.datadog.android.rum.featureoperations.FailureReason class MockRumMonitor : RumMonitor { override var debug = false @@ -123,4 +124,26 @@ class MockRumMonitor : RumMonitor { key: Any, attributes: Map ) {} + + @ExperimentalRumApi + override fun startFeatureOperation( + name: String, + operationKey: String?, + attributes: Map + ) {} + + @ExperimentalRumApi + override fun succeedFeatureOperation( + name: String, + operationKey: String?, + attributes: Map + ) {} + + @ExperimentalRumApi + override fun failFeatureOperation( + name: String, + operationKey: String?, + failureReason: FailureReason, + attributes: Map + ) {} } diff --git a/packages/core/ios/Tests/DdLogsTests.swift b/packages/core/ios/Tests/DdLogsTests.swift index 2d9fdebac..60640e807 100644 --- a/packages/core/ios/Tests/DdLogsTests.swift +++ b/packages/core/ios/Tests/DdLogsTests.swift @@ -463,6 +463,8 @@ private class MockNativeLogger: LoggerProtocol { } extension MockNativeLogger: InternalLoggerProtocol { + func critical(message: String, error: (any Error)?, attributes: [String : any Encodable]?, completionHandler: @escaping DatadogInternal.CompletionHandler) {} + func log(level: DatadogLogs.LogLevel, message: String, errorKind: String?, errorMessage: String?, stackTrace: String?, attributes: [String : Encodable]?) { receivedMethodCalls.append(MethodCall( kind: MockNativeLogger.MethodCall.Kind(from: level), diff --git a/packages/react-native-session-replay/DatadogSDKReactNativeSessionReplay.podspec b/packages/react-native-session-replay/DatadogSDKReactNativeSessionReplay.podspec index 77450eb93..60609c8d5 100644 --- a/packages/react-native-session-replay/DatadogSDKReactNativeSessionReplay.podspec +++ b/packages/react-native-session-replay/DatadogSDKReactNativeSessionReplay.podspec @@ -19,7 +19,7 @@ Pod::Spec.new do |s| s.dependency "React-Core" # /!\ Remember to keep the version in sync with DatadogSDKReactNative.podspec - s.dependency 'DatadogSessionReplay', '3.0.0' + s.dependency 'DatadogSessionReplay', '3.1.0' s.dependency 'DatadogSDKReactNative' s.test_spec 'Tests' do |test_spec| diff --git a/packages/react-native-session-replay/android/build.gradle b/packages/react-native-session-replay/android/build.gradle index 365b089b1..bbd09d9a1 100644 --- a/packages/react-native-session-replay/android/build.gradle +++ b/packages/react-native-session-replay/android/build.gradle @@ -211,8 +211,8 @@ dependencies { api "com.facebook.react:react-android:$reactNativeVersion" } implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" - implementation "com.datadoghq:dd-sdk-android-session-replay:3.0.0" - implementation "com.datadoghq:dd-sdk-android-internal:3.0.0" + implementation "com.datadoghq:dd-sdk-android-session-replay:3.1.0" + implementation "com.datadoghq:dd-sdk-android-internal:3.1.0" implementation project(path: ':datadog_mobile-react-native') testImplementation "org.junit.platform:junit-platform-launcher:1.6.2" diff --git a/packages/react-native-webview/DatadogSDKReactNativeWebView.podspec b/packages/react-native-webview/DatadogSDKReactNativeWebView.podspec index 26e160bbc..080a853d8 100644 --- a/packages/react-native-webview/DatadogSDKReactNativeWebView.podspec +++ b/packages/react-native-webview/DatadogSDKReactNativeWebView.podspec @@ -23,8 +23,8 @@ Pod::Spec.new do |s| end # /!\ Remember to keep the version in sync with DatadogSDKReactNative.podspec - s.dependency 'DatadogWebViewTracking', '3.0.0' - s.dependency 'DatadogInternal', '3.0.0' + s.dependency 'DatadogWebViewTracking', '3.1.0' + s.dependency 'DatadogInternal', '3.1.0' s.dependency 'DatadogSDKReactNative' s.test_spec 'Tests' do |test_spec| diff --git a/packages/react-native-webview/android/build.gradle b/packages/react-native-webview/android/build.gradle index d7c930ceb..24600b881 100644 --- a/packages/react-native-webview/android/build.gradle +++ b/packages/react-native-webview/android/build.gradle @@ -189,7 +189,7 @@ dependencies { implementation "com.facebook.react:react-android:$reactNativeVersion" } - implementation "com.datadoghq:dd-sdk-android-webview:3.0.0" + implementation "com.datadoghq:dd-sdk-android-webview:3.1.0" implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation project(path: ':datadog_mobile-react-native') From 029e6c127c3cf5ee50a9529a9de9fb8857ddaea3 Mon Sep 17 00:00:00 2001 From: Sergio Barrio Date: Fri, 19 Sep 2025 14:33:51 +0200 Subject: [PATCH 017/410] Fix internaltTestingTools tests --- .../DdInternalTestingImplementation.kt | 45 ++++++++------- .../DdInternalTestingImplementationTest.kt | 55 ++++++++++++------- 2 files changed, 60 insertions(+), 40 deletions(-) diff --git a/packages/internal-testing-tools/android/src/main/kotlin/com/datadog/reactnative/internaltesting/DdInternalTestingImplementation.kt b/packages/internal-testing-tools/android/src/main/kotlin/com/datadog/reactnative/internaltesting/DdInternalTestingImplementation.kt index 5729ae8c5..5e0756cf2 100644 --- a/packages/internal-testing-tools/android/src/main/kotlin/com/datadog/reactnative/internaltesting/DdInternalTestingImplementation.kt +++ b/packages/internal-testing-tools/android/src/main/kotlin/com/datadog/reactnative/internaltesting/DdInternalTestingImplementation.kt @@ -6,11 +6,13 @@ package com.datadog.reactnative.internaltesting +import androidx.annotation.WorkerThread import com.datadog.android.api.InternalLogger import com.datadog.android.Datadog import com.datadog.android.api.context.DatadogContext import com.datadog.android.api.context.NetworkInfo import com.datadog.android.api.context.TimeInfo +import com.datadog.android.api.feature.EventWriteScope import com.datadog.android.api.feature.Feature import com.datadog.android.api.feature.FeatureScope import com.datadog.android.api.storage.EventBatchWriter @@ -112,53 +114,54 @@ internal class FeatureScopeInterceptor( private val featureScope: FeatureScope, private val core: InternalSdkCore, ) : FeatureScope by featureScope { - private val eventsBatchInterceptor = EventBatchInterceptor() + private val eventWriteScopeInterceptor = EventWriteScopeInterceptor() fun eventsWritten(): List { - return eventsBatchInterceptor.events + return eventWriteScopeInterceptor.events } fun clearData() { - eventsBatchInterceptor.clearData() + eventWriteScopeInterceptor.clearData() } // region FeatureScope override fun withWriteContext( - forceNewBatch: Boolean, - callback: (DatadogContext, EventBatchWriter) -> Unit + withFeatureContexts: Set, + callback: (datadogContext: DatadogContext, write: EventWriteScope) -> Unit ) { - featureScope.withWriteContext(forceNewBatch, callback) + featureScope.withWriteContext(withFeatureContexts, callback) core.getDatadogContext()?.let { - callback(it, eventsBatchInterceptor) + callback(it, eventWriteScopeInterceptor) } } // endregion } - -internal class EventBatchInterceptor: EventBatchWriter { +internal class EventWriteScopeInterceptor : EventWriteScope { internal val events = mutableListOf() - override fun currentMetadata(): ByteArray? { - return null - } - fun clearData() { events.clear() } - override fun write( - event: RawBatchEvent, - batchMetadata: ByteArray?, - eventType: EventType - ): Boolean { - val eventContent = String(event.data) + private val writer = object : EventBatchWriter { + override fun currentMetadata(): ByteArray? = null - events += eventContent + override fun write( + event: RawBatchEvent, + batchMetadata: ByteArray?, + eventType: EventType + ): Boolean { + events += String(event.data) + return true + } + } - return true + override fun invoke(p1: (EventBatchWriter) -> Unit) { + p1(writer) } } + diff --git a/packages/internal-testing-tools/android/src/test/kotlin/com/datadog/reactnative/internaltesting/DdInternalTestingImplementationTest.kt b/packages/internal-testing-tools/android/src/test/kotlin/com/datadog/reactnative/internaltesting/DdInternalTestingImplementationTest.kt index 4a6938f9b..d25db9274 100644 --- a/packages/internal-testing-tools/android/src/test/kotlin/com/datadog/reactnative/internaltesting/DdInternalTestingImplementationTest.kt +++ b/packages/internal-testing-tools/android/src/test/kotlin/com/datadog/reactnative/internaltesting/DdInternalTestingImplementationTest.kt @@ -10,9 +10,9 @@ import android.content.Context import com.datadog.android.Datadog import com.datadog.android.api.SdkCore import com.datadog.android.api.context.DatadogContext +import com.datadog.android.api.feature.EventWriteScope import com.datadog.android.api.feature.Feature import com.datadog.android.api.feature.FeatureScope -import com.datadog.android.api.storage.EventBatchWriter import com.datadog.android.api.storage.EventType import com.datadog.android.api.storage.RawBatchEvent import com.datadog.android.api.storage.datastore.DataStoreHandler @@ -85,23 +85,27 @@ internal class DdInternalTestingImplementationTest { wrappedCore.registerFeature(mockFeature) requireNotNull(wrappedCore.getFeature(mockFeature.name)) - .withWriteContext { _, eventBatchWriter -> - eventBatchWriter.write( - RawBatchEvent(data = "mock event for test".toByteArray()), - batchMetadata = null, - eventType = EventType.DEFAULT + .withWriteContext { _, writeScope -> + writeScope { + val rawBatchEvent = + RawBatchEvent(data = "mock event for test".toByteArray()) + it.write( + rawBatchEvent, + batchMetadata = null, + eventType = EventType.DEFAULT + ) + } + + // Then + assertThat( + wrappedCore.featureScopes[mockFeature.name] + ?.eventsWritten() + ?.first() ) + .isEqualTo( + "mock event for test" + ) } - - // Then - assertThat( - wrappedCore.featureScopes[mockFeature.name] - ?.eventsWritten() - ?.first() - ) - .isEqualTo( - "mock event for test" - ) } } } @@ -116,10 +120,23 @@ internal class MockFeatureScope(private val feature: Feature) : FeatureScope { return feature as T } + override fun withContext( + withFeatureContexts: Set, + callback: (datadogContext: DatadogContext) -> Unit + ) { + } + override fun withWriteContext( - forceNewBatch: Boolean, - callback: (DatadogContext, EventBatchWriter) -> Unit - ) {} + withFeatureContexts: Set, + callback: (datadogContext: DatadogContext, write: EventWriteScope) -> Unit + ) { + } + + override fun getWriteContextSync( + withFeatureContexts: Set + ): Pair? { + return TODO("Provide the return value") + } } internal class MockFeature(override val name: String) : Feature { From 463a58d0aa8ae6ed16100e239dd931926e9b7cb6 Mon Sep 17 00:00:00 2001 From: Sergio Barrio Date: Fri, 5 Sep 2025 17:47:29 +0200 Subject: [PATCH 018/410] Use native sdk's core instance instead of the one inside RN SDK wrapper --- .../com/datadog/reactnative/DdSdkNativeInitialization.kt | 2 ++ .../src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt | 6 +----- .../src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt | 7 +------ 3 files changed, 4 insertions(+), 11 deletions(-) diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkNativeInitialization.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkNativeInitialization.kt index 79114b184..1dbf0b8ea 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkNativeInitialization.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkNativeInitialization.kt @@ -65,7 +65,9 @@ class DdSdkNativeInitialization internal constructor( datadog.initialize(appContext, sdkConfiguration, trackingConsent) Rum.enable(rumConfiguration, Datadog.getInstance()) + Logs.enable(logsConfiguration, Datadog.getInstance()) + Trace.enable(traceConfiguration, Datadog.getInstance()) } diff --git a/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt b/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt index 97acb2ebf..3c86e28b4 100644 --- a/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt +++ b/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt @@ -21,11 +21,7 @@ class DdSdk( ddTelemetry: DdTelemetry = DdTelemetry() ) : ReactContextBaseJavaModule(reactContext) { - private val implementation = DdSdkImplementation( - reactContext, - datadog = datadogWrapper, - ddTelemetry - ) + private val implementation = DdSdkImplementation(reactContext, datadog = datadogWrapper, ddTelemetry) private var lifecycleEventListener: LifecycleEventListener? = null override fun getName(): String = DdSdkImplementation.NAME diff --git a/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt b/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt index 9b0014b7e..1b8e5c4f0 100644 --- a/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt +++ b/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt @@ -163,12 +163,7 @@ internal class DdSdkTest { answer.getArgument(0).run() true } - testedBridgeSdk = DdSdkImplementation( - mockReactContext, - mockDatadog, - mockDdTelemetry, - TestUiThreadExecutor() - ) + testedBridgeSdk = DdSdkImplementation(mockReactContext, mockDatadog, mockDdTelemetry, TestUiThreadExecutor()) DatadogSDKWrapperStorage.onInitializedListeners.clear() } From b1dc57d34630797d3925da064a67e9ebe25df3b7 Mon Sep 17 00:00:00 2001 From: Marco Saia Date: Mon, 22 Sep 2025 16:20:27 +0200 Subject: [PATCH 019/410] Fixed internal testing tools and unit tests --- .../com/datadog/reactnative/DdSdkNativeInitialization.kt | 2 -- .../src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt | 6 +++++- .../src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt | 7 ++++++- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkNativeInitialization.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkNativeInitialization.kt index 1dbf0b8ea..79114b184 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkNativeInitialization.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkNativeInitialization.kt @@ -65,9 +65,7 @@ class DdSdkNativeInitialization internal constructor( datadog.initialize(appContext, sdkConfiguration, trackingConsent) Rum.enable(rumConfiguration, Datadog.getInstance()) - Logs.enable(logsConfiguration, Datadog.getInstance()) - Trace.enable(traceConfiguration, Datadog.getInstance()) } diff --git a/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt b/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt index 3c86e28b4..97acb2ebf 100644 --- a/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt +++ b/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt @@ -21,7 +21,11 @@ class DdSdk( ddTelemetry: DdTelemetry = DdTelemetry() ) : ReactContextBaseJavaModule(reactContext) { - private val implementation = DdSdkImplementation(reactContext, datadog = datadogWrapper, ddTelemetry) + private val implementation = DdSdkImplementation( + reactContext, + datadog = datadogWrapper, + ddTelemetry + ) private var lifecycleEventListener: LifecycleEventListener? = null override fun getName(): String = DdSdkImplementation.NAME diff --git a/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt b/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt index 1b8e5c4f0..9b0014b7e 100644 --- a/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt +++ b/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt @@ -163,7 +163,12 @@ internal class DdSdkTest { answer.getArgument(0).run() true } - testedBridgeSdk = DdSdkImplementation(mockReactContext, mockDatadog, mockDdTelemetry, TestUiThreadExecutor()) + testedBridgeSdk = DdSdkImplementation( + mockReactContext, + mockDatadog, + mockDdTelemetry, + TestUiThreadExecutor() + ) DatadogSDKWrapperStorage.onInitializedListeners.clear() } From 15a539537c38ac25c9c0562839a761b2c25c71e3 Mon Sep 17 00:00:00 2001 From: Sergio Barrio Date: Fri, 19 Sep 2025 17:37:31 +0200 Subject: [PATCH 020/410] Expose clearUserInfo API --- packages/core/__mocks__/react-native.ts | 3 + .../datadog/reactnative/DatadogSDKWrapper.kt | 4 ++ .../com/datadog/reactnative/DatadogWrapper.kt | 5 ++ .../reactnative/DdSdkImplementation.kt | 10 ++- .../kotlin/com/datadog/reactnative/DdSdk.kt | 8 +++ .../kotlin/com/datadog/reactnative/DdSdk.kt | 8 +++ .../com/datadog/reactnative/DdSdkTest.kt | 11 +++ packages/core/ios/Sources/DdSdk.mm | 12 +++- .../ios/Sources/DdSdkImplementation.swift | 8 ++- packages/core/ios/Tests/DdSdkTests.swift | 67 +++++++++++++++++++ packages/core/jest/mock.js | 3 + packages/core/src/DdSdkReactNative.tsx | 10 +++ .../src/__tests__/DdSdkReactNative.test.tsx | 26 +++++++ .../UserInfoSingleton/UserInfoSingleton.ts | 4 ++ .../__tests__/UserInfoSingleton.test.ts | 55 ++++++++++++--- packages/core/src/specs/NativeDdSdk.ts | 5 ++ packages/core/src/types.tsx | 5 ++ 17 files changed, 230 insertions(+), 14 deletions(-) diff --git a/packages/core/__mocks__/react-native.ts b/packages/core/__mocks__/react-native.ts index 260fe68a7..0e85e65ae 100644 --- a/packages/core/__mocks__/react-native.ts +++ b/packages/core/__mocks__/react-native.ts @@ -24,6 +24,9 @@ actualRN.NativeModules.DdSdk = { addUserExtraInfo: jest.fn().mockImplementation( () => new Promise(resolve => resolve()) ) as jest.MockedFunction, + clearUserInfo: jest.fn().mockImplementation( + () => new Promise(resolve => resolve()) + ) as jest.MockedFunction, setAttributes: jest.fn().mockImplementation( () => new Promise(resolve => resolve()) ) as jest.MockedFunction, diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogSDKWrapper.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogSDKWrapper.kt index 141b995da..3d344b203 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogSDKWrapper.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogSDKWrapper.kt @@ -85,6 +85,10 @@ internal class DatadogSDKWrapper : DatadogWrapper { Datadog.addUserProperties(extraInfo) } + override fun clearUserInfo() { + Datadog.clearUserInfo() + } + override fun addRumGlobalAttributes(attributes: Map) { val rumMonitor = this.getRumMonitor() for (attribute in attributes) { diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogWrapper.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogWrapper.kt index 3ae3e6266..49d606b35 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogWrapper.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogWrapper.kt @@ -86,6 +86,11 @@ interface DatadogWrapper { extraInfo: Map ) + /** + * Clears the user information. + */ + fun clearUserInfo() + /** * Adds global attributes. * diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt index 54769fb8b..467d37335 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt @@ -102,7 +102,7 @@ class DdSdkImplementation( } /** - * Sets the user information. + * Sets the user extra information. * @param userExtraInfo: The additional information. (To set the id, name or email please user setUserInfo). */ fun addUserExtraInfo( @@ -114,6 +114,14 @@ class DdSdkImplementation( promise.resolve(null) } + /** + * Clears the user information. + */ + fun clearUserInfo(promise: Promise) { + datadog.clearUserInfo() + promise.resolve(null) + } + /** * Set the tracking consent regarding the data collection. * @param trackingConsent Consent, which can take one of the following values: 'pending', diff --git a/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdSdk.kt b/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdSdk.kt index cfafffffe..4e4668a3e 100644 --- a/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdSdk.kt +++ b/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdSdk.kt @@ -67,6 +67,14 @@ class DdSdk( implementation.addUserExtraInfo(extraInfo, promise) } + /** + * Clears the user information. + */ + @ReactMethod + override fun clearUserInfo(promise: Promise) { + implementation.clearUserInfo(promise) + } + /** * Set the tracking consent regarding the data collection. * @param trackingConsent Consent, which can take one of the following values: 'pending', diff --git a/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt b/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt index 97acb2ebf..0ebdd37fb 100644 --- a/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt +++ b/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt @@ -93,6 +93,14 @@ class DdSdk( implementation.addUserExtraInfo(extraInfo, promise) } + /** + * Clears the user information. + */ + @ReactMethod + fun clearUserInfo(promise: Promise) { + implementation.clearUserInfo(promise) + } + /** * Set the tracking consent regarding the data collection. * @param trackingConsent Consent, which can take one of the following values: 'pending', diff --git a/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt b/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt index 9b0014b7e..4abf9b981 100644 --- a/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt +++ b/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt @@ -2954,6 +2954,17 @@ internal class DdSdkTest { } } + @Test + fun `𝕄 clear user info 𝕎 clearUserInfo()`() { + // When + testedBridgeSdk.clearUserInfo(mockPromise) + + // Then + argumentCaptor> { + verify(mockDatadog).clearUserInfo() + } + } + @Test fun `𝕄 set RUM attributes 𝕎 setAttributes`( @MapForgery( diff --git a/packages/core/ios/Sources/DdSdk.mm b/packages/core/ios/Sources/DdSdk.mm index 3ead770ac..98a228f76 100644 --- a/packages/core/ios/Sources/DdSdk.mm +++ b/packages/core/ios/Sources/DdSdk.mm @@ -51,6 +51,12 @@ + (void)initFromNative { [self addUserExtraInfo:extraInfo resolve:resolve reject:reject]; } +RCT_EXPORT_METHOD(clearUserInfo:(RCTPromiseResolveBlock)resolve + withRejecter:(RCTPromiseRejectBlock)reject) +{ + [self clearUserInfo:resolve reject:reject]; +} + RCT_REMAP_METHOD(setTrackingConsent, withTrackingConsent:(NSString*)trackingConsent withResolver:(RCTPromiseResolveBlock)resolve withRejecter:(RCTPromiseRejectBlock)reject) @@ -81,7 +87,7 @@ + (void)initFromNative { [self consumeWebviewEvent:message resolve:resolve reject:reject]; } -RCT_REMAP_METHOD(clearAllData, withResolver:(RCTPromiseResolveBlock)resolve +RCT_EXPORT_METHOD(clearAllData:(RCTPromiseResolveBlock)resolve withRejecter:(RCTPromiseRejectBlock)reject) { [self clearAllData:resolve reject:reject]; @@ -141,6 +147,10 @@ - (void)setUserInfo:(NSDictionary *)userInfo resolve:(RCTPromiseResolveBlock)res [self.ddSdkImplementation setUserInfoWithUserInfo:userInfo resolve:resolve reject:reject]; } +- (void)clearUserInfo:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + [self.ddSdkImplementation clearUserInfoWithResolve:resolve reject:reject]; +} + -(void)addUserExtraInfo:(NSDictionary *)extraInfo resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { [self.ddSdkImplementation addUserExtraInfoWithExtraInfo:extraInfo resolve:resolve reject:reject]; } diff --git a/packages/core/ios/Sources/DdSdkImplementation.swift b/packages/core/ios/Sources/DdSdkImplementation.swift index 5a4bbf9a5..9d057d158 100644 --- a/packages/core/ios/Sources/DdSdkImplementation.swift +++ b/packages/core/ios/Sources/DdSdkImplementation.swift @@ -102,7 +102,7 @@ public class DdSdkImplementation: NSObject { resolve(nil) } - + @objc public func addUserExtraInfo(extraInfo: NSDictionary, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { let castedExtraInfo = castAttributesToSwift(extraInfo) @@ -111,6 +111,12 @@ public class DdSdkImplementation: NSObject { resolve(nil) } + @objc + public func clearUserInfo(resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { + Datadog.clearUserInfo() + resolve(nil) + } + @objc public func setTrackingConsent(trackingConsent: NSString, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { Datadog.set(trackingConsent: (trackingConsent as NSString?).asTrackingConsent()) diff --git a/packages/core/ios/Tests/DdSdkTests.swift b/packages/core/ios/Tests/DdSdkTests.swift index 555ce4549..efbf57b96 100644 --- a/packages/core/ios/Tests/DdSdkTests.swift +++ b/packages/core/ios/Tests/DdSdkTests.swift @@ -651,6 +651,73 @@ class DdSdkTests: XCTestCase { XCTFail("extra-info-4 is not of expected type or value") } } + + func testClearUserInfo() throws { + let bridge = DdSdkImplementation( + mainDispatchQueue: DispatchQueueMock(), + jsDispatchQueue: DispatchQueueMock(), + jsRefreshRateMonitor: JSRefreshRateMonitor(), + RUMMonitorProvider: { MockRUMMonitor() }, + RUMMonitorInternalProvider: { nil } + ) + bridge.initialize( + configuration: .mockAny(), + resolve: mockResolve, + reject: mockReject + ) + + bridge.setUserInfo( + userInfo: NSDictionary( + dictionary: [ + "id": "id_123", + "name": "John Doe", + "email": "john@doe.com", + "extraInfo": [ + "extra-info-1": 123, + "extra-info-2": "abc", + "extra-info-3": true, + "extra-info-4": [ + "nested-extra-info-1": 456 + ], + ], + ] + ), + resolve: mockResolve, + reject: mockReject + ) + + var ddContext = try XCTUnwrap(CoreRegistry.default as? DatadogCore).contextProvider.read() + var userInfo = try XCTUnwrap(ddContext.userInfo) + + XCTAssertEqual(userInfo.id, "id_123") + XCTAssertEqual(userInfo.name, "John Doe") + XCTAssertEqual(userInfo.email, "john@doe.com") + XCTAssertEqual(userInfo.extraInfo["extra-info-1"] as? Int64, 123) + XCTAssertEqual(userInfo.extraInfo["extra-info-2"] as? String, "abc") + XCTAssertEqual(userInfo.extraInfo["extra-info-3"] as? Bool, true) + + if let extraInfo4Encodable = userInfo.extraInfo["extra-info-4"] + as? DatadogSDKReactNative.AnyEncodable, + let extraInfo4Dict = extraInfo4Encodable.value as? [String: Int] + { + XCTAssertEqual(extraInfo4Dict, ["nested-extra-info-1": 456]) + } else { + XCTFail("extra-info-4 is not of expected type or value") + } + + bridge.clearUserInfo(resolve: mockResolve, reject: mockReject) + + ddContext = try XCTUnwrap(CoreRegistry.default as? DatadogCore).contextProvider.read() + userInfo = try XCTUnwrap(ddContext.userInfo) + + XCTAssertEqual(userInfo.id, nil) + XCTAssertEqual(userInfo.name, nil) + XCTAssertEqual(userInfo.email, nil) + XCTAssertEqual(userInfo.extraInfo["extra-info-1"] as? Int64, nil) + XCTAssertEqual(userInfo.extraInfo["extra-info-2"] as? String, nil) + XCTAssertEqual(userInfo.extraInfo["extra-info-3"] as? Bool, nil) + XCTAssertEqual(userInfo.extraInfo["extra-info-4"] as? [String: Int], nil) + } func testSettingAttributes() { let rumMonitorMock = MockRUMMonitor() diff --git a/packages/core/jest/mock.js b/packages/core/jest/mock.js index a8161295a..8e154c4cd 100644 --- a/packages/core/jest/mock.js +++ b/packages/core/jest/mock.js @@ -33,6 +33,9 @@ module.exports = { addUserExtraInfo: jest .fn() .mockImplementation(() => new Promise(resolve => resolve())), + clearUserInfo: jest + .fn() + .mockImplementation(() => new Promise(resolve => resolve())), setAttributes: jest .fn() .mockImplementation(() => new Promise(resolve => resolve())), diff --git a/packages/core/src/DdSdkReactNative.tsx b/packages/core/src/DdSdkReactNative.tsx index 668ae09f3..1838542df 100644 --- a/packages/core/src/DdSdkReactNative.tsx +++ b/packages/core/src/DdSdkReactNative.tsx @@ -214,6 +214,16 @@ export class DdSdkReactNative { UserInfoSingleton.getInstance().setUserInfo(userInfo); }; + /** + * Clears the user information. + * @returns a Promise. + */ + static clearUserInfo = async (): Promise => { + InternalLog.log('Clearing user info', SdkVerbosity.DEBUG); + await DdSdk.clearUserInfo(); + UserInfoSingleton.getInstance().clearUserInfo(); + }; + /** * Set the user information. * @param extraUserInfo: The additional information. (To set the id, name or email please user setUserInfo). diff --git a/packages/core/src/__tests__/DdSdkReactNative.test.tsx b/packages/core/src/__tests__/DdSdkReactNative.test.tsx index f9405aa51..57ff5d0ed 100644 --- a/packages/core/src/__tests__/DdSdkReactNative.test.tsx +++ b/packages/core/src/__tests__/DdSdkReactNative.test.tsx @@ -1112,6 +1112,32 @@ describe('DdSdkReactNative', () => { }); }); + describe('clearUserInfo', () => { + it('calls SDK method when clearUserInfo, and clears the user in UserProvider', async () => { + // GIVEN + const userInfo = { + id: 'id', + name: 'name', + email: 'email', + extraInfo: { + foo: 'bar' + } + }; + + await DdSdkReactNative.setUserInfo(userInfo); + + // WHEN + await DdSdkReactNative.clearUserInfo(); + + // THEN + expect(DdSdk.clearUserInfo).toHaveBeenCalledTimes(1); + expect(DdSdk.setUserInfo).toHaveBeenCalled(); + expect(UserInfoSingleton.getInstance().getUserInfo()).toEqual( + undefined + ); + }); + }); + describe('setTrackingConsent', () => { it('calls SDK method when setTrackingConsent', async () => { // GIVEN diff --git a/packages/core/src/sdk/UserInfoSingleton/UserInfoSingleton.ts b/packages/core/src/sdk/UserInfoSingleton/UserInfoSingleton.ts index 26392d794..3ce23614b 100644 --- a/packages/core/src/sdk/UserInfoSingleton/UserInfoSingleton.ts +++ b/packages/core/src/sdk/UserInfoSingleton/UserInfoSingleton.ts @@ -16,6 +16,10 @@ class UserInfoProvider { getUserInfo = (): UserInfo | undefined => { return this.userInfo; }; + + clearUserInfo = () => { + this.userInfo = undefined; + }; } export class UserInfoSingleton { diff --git a/packages/core/src/sdk/UserInfoSingleton/__tests__/UserInfoSingleton.test.ts b/packages/core/src/sdk/UserInfoSingleton/__tests__/UserInfoSingleton.test.ts index 1f7ae84e7..f8e7276d6 100644 --- a/packages/core/src/sdk/UserInfoSingleton/__tests__/UserInfoSingleton.test.ts +++ b/packages/core/src/sdk/UserInfoSingleton/__tests__/UserInfoSingleton.test.ts @@ -7,27 +7,60 @@ import { UserInfoSingleton } from '../UserInfoSingleton'; describe('UserInfoSingleton', () => { - it('sets, returns and resets the user info', () => { + beforeEach(() => { + UserInfoSingleton.reset(); + }); + + it('returns undefined by default', () => { + expect(UserInfoSingleton.getInstance().getUserInfo()).toBeUndefined(); + }); + + it('stores and returns user info after setUserInfo', () => { + const info = { + id: 'test', + email: 'user@mail.com', + extraInfo: { loggedIn: true } + }; + + UserInfoSingleton.getInstance().setUserInfo(info); + + expect(UserInfoSingleton.getInstance().getUserInfo()).toEqual(info); + }); + + it('clears user info with clearUserInfo', () => { UserInfoSingleton.getInstance().setUserInfo({ id: 'test', email: 'user@mail.com', - extraInfo: { - loggedIn: true - } + extraInfo: { loggedIn: true } }); - expect(UserInfoSingleton.getInstance().getUserInfo()).toEqual({ + UserInfoSingleton.getInstance().clearUserInfo(); + + expect(UserInfoSingleton.getInstance().getUserInfo()).toBeUndefined(); + }); + + it('reset() replaces the provider and clears stored user info', () => { + const instanceBefore = UserInfoSingleton.getInstance(); + + UserInfoSingleton.getInstance().setUserInfo({ id: 'test', email: 'user@mail.com', - extraInfo: { - loggedIn: true - } + extraInfo: { loggedIn: true } }); UserInfoSingleton.reset(); - expect(UserInfoSingleton.getInstance().getUserInfo()).toEqual( - undefined - ); + const instanceAfter = UserInfoSingleton.getInstance(); + + expect(instanceAfter).not.toBe(instanceBefore); + + expect(instanceAfter.getUserInfo()).toBeUndefined(); + }); + + it('getInstance returns the same provider between calls (singleton behavior)', () => { + const a = UserInfoSingleton.getInstance(); + const b = UserInfoSingleton.getInstance(); + + expect(a).toBe(b); }); }); diff --git a/packages/core/src/specs/NativeDdSdk.ts b/packages/core/src/specs/NativeDdSdk.ts index bbf2572ee..a2ce1120e 100644 --- a/packages/core/src/specs/NativeDdSdk.ts +++ b/packages/core/src/specs/NativeDdSdk.ts @@ -37,6 +37,11 @@ export interface Spec extends TurboModule { */ setUserInfo(user: Object): Promise; + /** + * Clears the user information. + */ + clearUserInfo(): Promise; + /** * Add custom attributes to the current user information * @param extraInfo: The extraInfo object containing additionall custom attributes diff --git a/packages/core/src/types.tsx b/packages/core/src/types.tsx index fe1a5895c..bad19d429 100644 --- a/packages/core/src/types.tsx +++ b/packages/core/src/types.tsx @@ -98,6 +98,11 @@ export type DdSdkType = { */ setUserInfo(userInfo: UserInfo): Promise; + /** + * Clears the user information. + */ + clearUserInfo(): Promise; + /** * Add additional user information. * @param extraUserInfo: The additional information. (To set the id, name or email please user setUserInfo). From de38d911a78d717c910c10a1bdca69dde10af543 Mon Sep 17 00:00:00 2001 From: Sergio Barrio Date: Mon, 29 Sep 2025 12:20:44 +0200 Subject: [PATCH 021/410] Update attribute API --- benchmarks/src/testSetup/monitor.ts | 5 +- example/src/ddUtils.tsx | 4 +- packages/codepush/__mocks__/react-native.ts | 13 +- packages/core/__mocks__/react-native.ts | 13 +- .../datadog/reactnative/DatadogSDKWrapper.kt | 17 ++- .../com/datadog/reactnative/DatadogWrapper.kt | 22 ++++ .../reactnative/DdSdkImplementation.kt | 51 ++++++- .../kotlin/com/datadog/reactnative/DdSdk.kt | 39 +++++- .../kotlin/com/datadog/reactnative/DdSdk.kt | 37 +++++- .../com/datadog/reactnative/DdSdkTest.kt | 118 ++++++++++++++++- packages/core/ios/Sources/AnyEncodable.swift | 21 ++- packages/core/ios/Sources/DdSdk.mm | 42 +++++- .../ios/Sources/DdSdkImplementation.swift | 31 ++++- packages/core/ios/Sources/GlobalState.swift | 2 +- packages/core/ios/Tests/DdSdkTests.swift | 124 +++++++++++++++++- packages/core/ios/Tests/MockRUMMonitor.swift | 12 +- packages/core/jest/mock.js | 11 +- packages/core/src/DdSdkReactNative.tsx | 53 +++++++- .../src/__tests__/DdSdkReactNative.test.tsx | 68 +++++++++- .../AttributesSingleton.ts | 26 +++- .../__tests__/AttributesSingleton.test.ts | 60 +++++++-- packages/core/src/specs/NativeDdSdk.ts | 23 +++- packages/core/src/types.tsx | 21 ++- .../__mocks__/react-native.ts | 4 +- 24 files changed, 742 insertions(+), 75 deletions(-) diff --git a/benchmarks/src/testSetup/monitor.ts b/benchmarks/src/testSetup/monitor.ts index c7cc23473..93ea0fccd 100644 --- a/benchmarks/src/testSetup/monitor.ts +++ b/benchmarks/src/testSetup/monitor.ts @@ -4,8 +4,7 @@ * Copyright 2016-Present Datadog, Inc. */ -import { DefaultTimeProvider, RumActionType } from "@datadog/mobile-react-native"; -import { ErrorSource } from "@datadog/mobile-react-native/lib/typescript/rum/types"; +import { DefaultTimeProvider, ErrorSource, RumActionType } from "@datadog/mobile-react-native"; import type { DdRumType, ResourceKind } from "@datadog/mobile-react-native/lib/typescript/rum/types"; import type { GestureResponderEvent } from "react-native/types"; @@ -72,4 +71,4 @@ export const Monitor: Pick { DdLogs.info('The RN Sdk was properly initialized') DdSdkReactNative.setUserInfo({id: "1337", name: "Xavier", email: "xg@example.com", extraInfo: { type: "premium" } }) - DdSdkReactNative.setAttributes({campaign: "ad-network"}) + DdSdkReactNative.addAttributes({campaign: "ad-network"}) }); } diff --git a/packages/codepush/__mocks__/react-native.ts b/packages/codepush/__mocks__/react-native.ts index 046ced2f6..0c8189840 100644 --- a/packages/codepush/__mocks__/react-native.ts +++ b/packages/codepush/__mocks__/react-native.ts @@ -18,9 +18,18 @@ actualRN.NativeModules.DdSdk = { initialize: jest.fn().mockImplementation( () => new Promise(resolve => resolve()) ) as jest.MockedFunction, - setAttributes: jest.fn().mockImplementation( + addAttribute: jest.fn().mockImplementation( () => new Promise(resolve => resolve()) - ) as jest.MockedFunction, + ) as jest.MockedFunction, + removeAttribute: jest.fn().mockImplementation( + () => new Promise(resolve => resolve()) + ) as jest.MockedFunction, + addAttributes: jest.fn().mockImplementation( + () => new Promise(resolve => resolve()) + ) as jest.MockedFunction, + removeAttributes: jest.fn().mockImplementation( + () => new Promise(resolve => resolve()) + ) as jest.MockedFunction, setTrackingConsent: jest.fn().mockImplementation( () => new Promise(resolve => resolve()) ) as jest.MockedFunction, diff --git a/packages/core/__mocks__/react-native.ts b/packages/core/__mocks__/react-native.ts index 0e85e65ae..24e3f80c7 100644 --- a/packages/core/__mocks__/react-native.ts +++ b/packages/core/__mocks__/react-native.ts @@ -27,9 +27,18 @@ actualRN.NativeModules.DdSdk = { clearUserInfo: jest.fn().mockImplementation( () => new Promise(resolve => resolve()) ) as jest.MockedFunction, - setAttributes: jest.fn().mockImplementation( + addAttribute: jest.fn().mockImplementation( () => new Promise(resolve => resolve()) - ) as jest.MockedFunction, + ) as jest.MockedFunction, + removeAttribute: jest.fn().mockImplementation( + () => new Promise(resolve => resolve()) + ) as jest.MockedFunction, + addAttributes: jest.fn().mockImplementation( + () => new Promise(resolve => resolve()) + ) as jest.MockedFunction, + removeAttributes: jest.fn().mockImplementation( + () => new Promise(resolve => resolve()) + ) as jest.MockedFunction, setTrackingConsent: jest.fn().mockImplementation( () => new Promise(resolve => resolve()) ) as jest.MockedFunction, diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogSDKWrapper.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogSDKWrapper.kt index 3d344b203..c8dba8e25 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogSDKWrapper.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogSDKWrapper.kt @@ -88,11 +88,24 @@ internal class DatadogSDKWrapper : DatadogWrapper { override fun clearUserInfo() { Datadog.clearUserInfo() } + + override fun addRumGlobalAttribute(key: String, value: Any?) { + this.getRumMonitor().addAttribute(key, value) + } + + override fun removeRumGlobalAttribute(key: String) { + this.getRumMonitor().removeAttribute(key) + } override fun addRumGlobalAttributes(attributes: Map) { - val rumMonitor = this.getRumMonitor() for (attribute in attributes) { - rumMonitor.addAttribute(attribute.key, attribute.value) + this.addRumGlobalAttribute(attribute.key, attribute.value) + } + } + + override fun removeRumGlobalAttributes(keys: Array) { + for (key in keys) { + this.removeRumGlobalAttribute(key) } } diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogWrapper.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogWrapper.kt index 49d606b35..d6395b18b 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogWrapper.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogWrapper.kt @@ -91,6 +91,21 @@ interface DatadogWrapper { */ fun clearUserInfo() + + /** Adds a global attribute. + * + * @param key: Key that identifies the attribute. + * @param value: Value linked to the attribute. + */ + fun addRumGlobalAttribute(key: String, value: Any?) + + /** + * Removes a global attribute. + * + * @param key: Key that identifies the attribute. + */ + fun removeRumGlobalAttribute(key: String) + /** * Adds global attributes. * @@ -98,6 +113,13 @@ interface DatadogWrapper { */ fun addRumGlobalAttributes(attributes: Map) + /** + * Removes global attributes. + * + * @param keys Keys linked to the attributes to be removed + */ + fun removeRumGlobalAttributes(keys: Array) + /** * Sets tracking consent. */ diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt index 467d37335..ffca896e3 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt @@ -14,6 +14,7 @@ import com.datadog.android.rum.configuration.VitalsUpdateFrequency import com.facebook.react.bridge.LifecycleEventListener import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.ReadableArray import com.facebook.react.bridge.ReadableMap import java.util.Locale import java.util.concurrent.TimeUnit @@ -66,11 +67,35 @@ class DdSdkImplementation( } /** - * Sets the global context (set of attributes) attached with all future Logs, Spans and RUM + * Sets a specific attribute in the global context attached with all future Logs, Spans and RUM. + * + * @param key: Key that identifies the attribute. + * @param value: Value linked to the attribute. + */ + fun addAttribute(key: String, value: ReadableMap, promise: Promise) { + val attributeValue = value.toMap()["value"] + datadog.addRumGlobalAttribute(key, attributeValue) + GlobalState.addAttribute(key, attributeValue) + promise.resolve(null) + } + + /** + * Removes an attribute from the global context attached with all future Logs, Spans and RUM events. + * @param key: They key associated with the attribute to be removed. + */ + fun removeAttribute(key: String, promise: Promise) { + datadog.removeRumGlobalAttribute(key) + GlobalState.removeAttribute(key) + promise.resolve(null) + } + + + /** + * Adds a set of attributes to the global context that is attached with all future Logs, Spans and RUM * events. - * @param attributes The global context attributes. + * @param attributes: The global context attributes. */ - fun setAttributes(attributes: ReadableMap, promise: Promise) { + fun addAttributes(attributes: ReadableMap, promise: Promise) { datadog.addRumGlobalAttributes(attributes.toHashMap()) for ((k,v) in attributes.toHashMap()) { GlobalState.addAttribute(k, v) @@ -78,6 +103,26 @@ class DdSdkImplementation( promise.resolve(null) } + /** + * Removes a set of attributes from the global context that is attached with all future Logs, Spans and RUM + * events. + * @param keys: They keys associated with the attributes to be removed. + */ + fun removeAttributes(keys: ReadableArray, promise: Promise) { + val keysArray = mutableListOf() + for (i in 0 until keys.size()) { + val key: String = keys.getString(i) + keysArray.add(key) + } + val keysStringArray = keysArray.toTypedArray() + + datadog.removeRumGlobalAttributes(keysStringArray) + for (key in keysStringArray) { + GlobalState.removeAttribute(key) + } + promise.resolve(null) + } + /** * Set the user information. * @param userInfo The user object (use builtin attributes: 'id', 'email', 'name', and any custom diff --git a/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdSdk.kt b/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdSdk.kt index 4e4668a3e..a9d430081 100644 --- a/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdSdk.kt +++ b/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdSdk.kt @@ -12,13 +12,14 @@ import com.facebook.react.bridge.LifecycleEventListener import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.ReactMethod +import com.facebook.react.bridge.ReadableArray import com.facebook.react.bridge.ReadableMap import com.facebook.react.modules.core.DeviceEventManagerModule /** The entry point to initialize Datadog's features. */ class DdSdk( reactContext: ReactApplicationContext, - datadogWrapper: DatadogWrapper = DatadogSDKWrapper() + datadogWrapper: DatadogWrapper = DatadogSDKWrapper(), ddTelemetry: DdTelemetry = DdTelemetry() ) : NativeDdSdkSpec(reactContext) { @@ -40,13 +41,43 @@ class DdSdk( } /** - * Sets the global context (set of attributes) attached with all future Logs, Spans and RUM + * Sets a specific attribute in the global context attached with all future Logs, Spans and RUM + * + * @param key: Key that identifies the attribute. + * @param value: Value linked to the attribute. + */ + @ReactMethod + override fun addAttribute(key: String, value: ReadableMap, promise: Promise) { + implementation.addAttribute(key, value, promise) + } + + /** + * Removes an attribute from the context attached with all future Logs, Spans and RUM events. + * @param key: They key associated with the attribute to be removed. + */ + @ReactMethod + override fun removeAttribute(key: String, promise: Promise) { + implementation.removeAttribute(key, promise) + } + + /** + * Adds a set of attributes to the global context that is attached with all future Logs, Spans and RUM * events. * @param attributes The global context attributes. */ @ReactMethod - override fun setAttributes(attributes: ReadableMap, promise: Promise) { - implementation.setAttributes(attributes, promise) + override fun addAttributes(attributes: ReadableMap, promise: Promise) { + implementation.addAttributes(attributes, promise) + } + + /** + * Removes a set of attributes from the global context that is attached with all future Logs, Spans and RUM + * events. + * @param keys: They keys associated with the attributes to be removed. + */ + @ReactMethod + override fun removeAttributes(keys: ReadableArray, promise: Promise) { + implementation.removeAttributes(keys, promise) } /** diff --git a/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt b/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt index 0ebdd37fb..958ba521b 100644 --- a/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt +++ b/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt @@ -12,6 +12,7 @@ import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.ReactContextBaseJavaModule import com.facebook.react.bridge.ReactMethod +import com.facebook.react.bridge.ReadableArray import com.facebook.react.bridge.ReadableMap /** The entry point to initialize Datadog's features. */ @@ -66,13 +67,43 @@ class DdSdk( } /** - * Sets the global context (set of attributes) attached with all future Logs, Spans and RUM + * Sets a specific attribute in the global context attached with all future Logs, Spans and RUM + * + * @param key: Key that identifies the attribute. + * @param value: Value linked to the attribute. + */ + @ReactMethod + fun addAttribute(key: String, value: ReadableMap, promise: Promise) { + implementation.addAttribute(key, value, promise) + } + + /** + * Removes an attribute from the context attached with all future Logs, Spans and RUM events. + * @param key: They key associated with the attribute to be removed. + */ + @ReactMethod + fun removeAttribute(key: String, promise: Promise) { + implementation.removeAttribute(key, promise) + } + + /** + * Adds a set of attributes to the global context that is attached with all future Logs, Spans and RUM * events. * @param attributes The global context attributes. */ @ReactMethod - fun setAttributes(attributes: ReadableMap, promise: Promise) { - implementation.setAttributes(attributes, promise) + fun addAttributes(attributes: ReadableMap, promise: Promise) { + implementation.addAttributes(attributes, promise) + } + + /** + * Removes a set of attributes from the global context that is attached with all future Logs, Spans and RUM + * events. + * @param keys: They keys associated with the attributes to be removed. + */ + @ReactMethod + fun removeAttributes(keys: ReadableArray, promise: Promise) { + implementation.removeAttributes(keys, promise) } /** diff --git a/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt b/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt index 4abf9b981..88f373e30 100644 --- a/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt +++ b/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt @@ -40,6 +40,7 @@ import com.datadog.tools.unit.setStaticValue import com.datadog.tools.unit.toReadableArray import com.datadog.tools.unit.toReadableJavaOnlyMap import com.datadog.tools.unit.toReadableMap +import com.facebook.react.bridge.JavaOnlyMap import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.ReadableMap @@ -78,7 +79,6 @@ import org.mockito.kotlin.doReturn import org.mockito.kotlin.doThrow import org.mockito.kotlin.eq import org.mockito.kotlin.inOrder -import org.mockito.kotlin.isNotNull import org.mockito.kotlin.isNull import org.mockito.kotlin.mock import org.mockito.kotlin.never @@ -2966,28 +2966,96 @@ internal class DdSdkTest { } @Test - fun `𝕄 set RUM attributes 𝕎 setAttributes`( + fun `M set Rum attribute W addAttribute`( + @StringForgery(type = StringForgeryType.NUMERICAL) key: String, + @StringForgery(type = StringForgeryType.ASCII) value: String + ) { + // When + val attributeMap = JavaOnlyMap().apply { + putString("value", value) + } + testedBridgeSdk.addAttribute(key, attributeMap, mockPromise) + + // Then + verify(mockDatadog).addRumGlobalAttribute(key, value) + } + + @Test + fun `M set GlobalState attribute W addAttribute`( + @StringForgery(type = StringForgeryType.NUMERICAL) key: String, + @StringForgery(type = StringForgeryType.ASCII) value: String + ) { + // When + val attributeMap = JavaOnlyMap().apply { + putString("value", value) + } + testedBridgeSdk.addAttribute(key, attributeMap, mockPromise) + + // Then + assertThat(GlobalState.globalAttributes).containsEntry(key, value) + } + + @Test + fun `M remove Rum attribute W removeAttribute`( + @StringForgery(type = StringForgeryType.NUMERICAL) key: String, + @StringForgery(type = StringForgeryType.ASCII) value: String + ) { + // Given + val attributeMap = JavaOnlyMap().apply { + putString("value", value) + } + testedBridgeSdk.addAttribute(key, attributeMap, mockPromise) + assertThat(GlobalState.globalAttributes).containsEntry(key, value) + + // When + testedBridgeSdk.removeAttribute(key, mockPromise) + + // Then + verify(mockDatadog).removeRumGlobalAttribute(key) + } + + @Test + fun `M remove GlobalState attribute W removeAttribute`( + @StringForgery(type = StringForgeryType.NUMERICAL) key: String, + @StringForgery(type = StringForgeryType.ASCII) value: String + ) { + // Given + val attributeMap = JavaOnlyMap().apply { + putString("value", value) + } + testedBridgeSdk.addAttribute(key, attributeMap, mockPromise) + assertThat(GlobalState.globalAttributes).containsEntry(key, value) + + // When + testedBridgeSdk.removeAttribute(key, mockPromise) + + // Then + assertThat(GlobalState.globalAttributes).doesNotContainEntry(key, value) + } + + @Test + fun `𝕄 set RUM attributes 𝕎 addAttributes`( @MapForgery( key = AdvancedForgery(string = [StringForgery(StringForgeryType.NUMERICAL)]), value = AdvancedForgery(string = [StringForgery(StringForgeryType.ASCII)]) ) customAttributes: Map ) { // When - testedBridgeSdk.setAttributes(customAttributes.toReadableMap(), mockPromise) + testedBridgeSdk.addAttributes(customAttributes.toReadableMap(), mockPromise) // Then verify(mockDatadog).addRumGlobalAttributes(customAttributes) } @Test - fun `𝕄 set GlobalState attributes 𝕎 setAttributes`( + fun `𝕄 set GlobalState attributes 𝕎 addAttributes`( @MapForgery( key = AdvancedForgery(string = [StringForgery(StringForgeryType.NUMERICAL)]), value = AdvancedForgery(string = [StringForgery(StringForgeryType.ASCII)]) ) customAttributes: Map ) { // When - testedBridgeSdk.setAttributes(customAttributes.toReadableMap(), mockPromise) + testedBridgeSdk.addAttributes(customAttributes.toReadableMap(), mockPromise) // Then customAttributes.forEach { (k, v) -> @@ -2995,6 +3063,46 @@ internal class DdSdkTest { } } + @Test + fun `𝕄 remove RUM attributes 𝕎 removeAttributes`( + @MapForgery( + key = AdvancedForgery(string = [StringForgery(StringForgeryType.NUMERICAL)]), + value = AdvancedForgery(string = [StringForgery(StringForgeryType.ASCII)]) + ) customAttributes: Map + ) { + // Given + testedBridgeSdk.addAttributes(customAttributes.toReadableMap(), mockPromise) + verify(mockDatadog).addRumGlobalAttributes(customAttributes) + + // When + val keys = customAttributes.keys.toReadableArray() + testedBridgeSdk.removeAttributes(keys, mockPromise) + + // Then + verify(mockDatadog).removeRumGlobalAttributes(customAttributes.keys.toTypedArray()) + } + + @Test + fun `𝕄 remve GlobalState attributes 𝕎 removeAttributes`( + @MapForgery( + key = AdvancedForgery(string = [StringForgery(StringForgeryType.NUMERICAL)]), + value = AdvancedForgery(string = [StringForgery(StringForgeryType.ASCII)]) + ) customAttributes: Map + ) { + // Given + testedBridgeSdk.addAttributes(customAttributes.toReadableMap(), mockPromise) + verify(mockDatadog).addRumGlobalAttributes(customAttributes) + + // When + val keys = customAttributes.keys.toReadableArray() + testedBridgeSdk.removeAttributes(keys, mockPromise) + + // Then + customAttributes.forEach { (k, v) -> + assertThat(GlobalState.globalAttributes).doesNotContainEntry(k, v) + } + } + @Test fun `𝕄 build Granted consent 𝕎 buildTrackingConsent {granted}`(forge: Forge) { // When diff --git a/packages/core/ios/Sources/AnyEncodable.swift b/packages/core/ios/Sources/AnyEncodable.swift index 39821af87..7fac7bb3b 100644 --- a/packages/core/ios/Sources/AnyEncodable.swift +++ b/packages/core/ios/Sources/AnyEncodable.swift @@ -14,18 +14,25 @@ internal func castAttributesToSwift(_ attributes: [String: Any]) -> [String: Enc var casted: [String: Encodable] = [:] attributes.forEach { key, value in - if let castedValue = castByPreservingTypeInformation(attributeValue: value) { - // If possible, cast attribute by preserving its type information - casted[key] = castedValue - } else { - // Otherwise, cast by preserving its encoded value (and loosing type information) - casted[key] = castByPreservingEncodedValue(attributeValue: value) - } + casted[key] = castValueToSwift(value) } return casted } +internal func castValueToSwift(_ value: Any) -> Encodable { + var casted: Encodable + if let castedValue = castByPreservingTypeInformation(attributeValue: value) { + // If possible, cast attribute by preserving its type information + casted = castedValue + } else { + // Otherwise, cast by preserving its encoded value (and loosing type information) + casted = castByPreservingEncodedValue(attributeValue: value) + } + + return casted +} + /// Casts `Any` value to `Encodable` by preserving its type information. private func castByPreservingTypeInformation(attributeValue: Any) -> Encodable? { switch attributeValue { diff --git a/packages/core/ios/Sources/DdSdk.mm b/packages/core/ios/Sources/DdSdk.mm index 98a228f76..7129d7af1 100644 --- a/packages/core/ios/Sources/DdSdk.mm +++ b/packages/core/ios/Sources/DdSdk.mm @@ -30,11 +30,33 @@ + (void)initFromNative { [self initialize:configuration resolve:resolve reject:reject]; } -RCT_REMAP_METHOD(setAttributes, withAttributes:(NSDictionary*)attributes +RCT_EXPORT_METHOD(addAttribute:(NSString*) key + withValue:(NSDictionary*) value + withResolver:(RCTPromiseResolveBlock)resolve + withRejecter:(RCTPromiseRejectBlock)reject) +{ + [self addAttribute:key value:value resolve:resolve reject:reject]; +} + +RCT_EXPORT_METHOD(removeAttribute:(NSString*) key + withResolver:(RCTPromiseResolveBlock)resolve + withRejecter:(RCTPromiseRejectBlock)reject) +{ + [self removeAttribute:key resolve:resolve reject:reject]; +} + +RCT_REMAP_METHOD(addAttributes, withAttributes:(NSDictionary*)attributes withResolver:(RCTPromiseResolveBlock)resolve withRejecter:(RCTPromiseRejectBlock)reject) { - [self setAttributes:attributes resolve:resolve reject:reject]; + [self addAttributes:attributes resolve:resolve reject:reject]; +} + +RCT_REMAP_METHOD(removeAttributes, withKeys:(NSArray *)keys + withResolver:(RCTPromiseResolveBlock)resolve + withRejecter:(RCTPromiseRejectBlock)reject) +{ + [self removeAttributes:keys resolve:resolve reject:reject]; } RCT_REMAP_METHOD(setUserInfo, withUserInfo:(NSDictionary*)userInfo @@ -135,8 +157,20 @@ - (void)initialize:(NSDictionary *)configuration resolve:(RCTPromiseResolveBlock [self.ddSdkImplementation initializeWithConfiguration:configuration resolve:resolve reject:reject]; } -- (void)setAttributes:(NSDictionary *)attributes resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { - [self.ddSdkImplementation setAttributesWithAttributes:attributes resolve:resolve reject:reject]; +- (void)addAttribute:(NSString *)key value:(NSDictionary *)value resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + [self.ddSdkImplementation addAttributeWithKey:key value:value resolve:resolve reject:reject]; +} + +- (void)removeAttribute:(NSString *)key resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + [self.ddSdkImplementation removeAttributeWithKey:key resolve:resolve reject:reject]; +} + +- (void)addAttributes:(NSDictionary *)attributes resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + [self.ddSdkImplementation addAttributesWithAttributes:attributes resolve:resolve reject:reject]; +} + +- (void)removeAttributes:(NSArray *)keys resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + [self.ddSdkImplementation removeAttributesWithKeys:keys resolve:resolve reject:reject]; } - (void)setTrackingConsent:(NSString *)trackingConsent resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { diff --git a/packages/core/ios/Sources/DdSdkImplementation.swift b/packages/core/ios/Sources/DdSdkImplementation.swift index 9d057d158..16adb50f2 100644 --- a/packages/core/ios/Sources/DdSdkImplementation.swift +++ b/packages/core/ios/Sources/DdSdkImplementation.swift @@ -69,14 +69,43 @@ public class DdSdkImplementation: NSObject { resolve(nil) } + + @objc + public func addAttribute(key: AttributeKey, value: NSDictionary, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { + if let attributeValue = value.object(forKey: "value") { + let castedValue = castValueToSwift(attributeValue) + RUMMonitorProvider().addAttribute(forKey: key, value: castedValue) + GlobalState.addAttribute(forKey: key, value: castedValue) + } + + resolve(nil) + } + + @objc + public func removeAttribute(key: AttributeKey, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { + RUMMonitorProvider().removeAttribute(forKey: key) + GlobalState.removeAttribute(key: key) + + resolve(nil) + } @objc - public func setAttributes(attributes: NSDictionary, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { + public func addAttributes(attributes: NSDictionary, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { let castedAttributes = castAttributesToSwift(attributes) for (key, value) in castedAttributes { RUMMonitorProvider().addAttribute(forKey: key, value: value) GlobalState.addAttribute(forKey: key, value: value) } + + resolve(nil) + } + + @objc + public func removeAttributes(keys: [AttributeKey], resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { + RUMMonitorProvider().removeAttributes(forKeys: keys) + for (key) in keys { + GlobalState.removeAttribute(key: key) + } resolve(nil) } diff --git a/packages/core/ios/Sources/GlobalState.swift b/packages/core/ios/Sources/GlobalState.swift index b932803a1..a758bf0ef 100644 --- a/packages/core/ios/Sources/GlobalState.swift +++ b/packages/core/ios/Sources/GlobalState.swift @@ -15,7 +15,7 @@ internal struct GlobalState { } internal static func removeAttribute(key: String) { - GlobalState.globalAttributes.removeValue(forKey: key) + GlobalState.globalAttributes[key] = nil } } diff --git a/packages/core/ios/Tests/DdSdkTests.swift b/packages/core/ios/Tests/DdSdkTests.swift index efbf57b96..adbb57da9 100644 --- a/packages/core/ios/Tests/DdSdkTests.swift +++ b/packages/core/ios/Tests/DdSdkTests.swift @@ -651,7 +651,7 @@ class DdSdkTests: XCTestCase { XCTFail("extra-info-4 is not of expected type or value") } } - + func testClearUserInfo() throws { let bridge = DdSdkImplementation( mainDispatchQueue: DispatchQueueMock(), @@ -704,12 +704,12 @@ class DdSdkTests: XCTestCase { } else { XCTFail("extra-info-4 is not of expected type or value") } - + bridge.clearUserInfo(resolve: mockResolve, reject: mockReject) - + ddContext = try XCTUnwrap(CoreRegistry.default as? DatadogCore).contextProvider.read() userInfo = try XCTUnwrap(ddContext.userInfo) - + XCTAssertEqual(userInfo.id, nil) XCTAssertEqual(userInfo.name, nil) XCTAssertEqual(userInfo.email, nil) @@ -719,7 +719,59 @@ class DdSdkTests: XCTestCase { XCTAssertEqual(userInfo.extraInfo["extra-info-4"] as? [String: Int], nil) } - func testSettingAttributes() { + func testRemovingAttribute() { + let rumMonitorMock = MockRUMMonitor() + let bridge = DdSdkImplementation( + mainDispatchQueue: DispatchQueueMock(), + jsDispatchQueue: DispatchQueueMock(), + jsRefreshRateMonitor: JSRefreshRateMonitor(), + RUMMonitorProvider: { rumMonitorMock }, + RUMMonitorInternalProvider: { nil } + ) + + bridge.initialize( + configuration: .mockAny(), + resolve: mockResolve, + reject: mockReject + ) + + bridge.addAttributes( + attributes: NSDictionary( + dictionary: [ + "attribute-1": 123, + "attribute-2": "abc", + ] + ), + resolve: mockResolve, + reject: mockReject + ) + + XCTAssertEqual(rumMonitorMock.addedAttributes["attribute-1"] as? Int64, 123) + XCTAssertEqual(rumMonitorMock.addedAttributes["attribute-2"] as? String, "abc") + + XCTAssertEqual(GlobalState.globalAttributes["attribute-1"] as? Int64, 123) + XCTAssertEqual(GlobalState.globalAttributes["attribute-2"] as? String, "abc") + + bridge.removeAttribute(key: "attribute-1", resolve: mockResolve, reject: mockReject) + + XCTAssertEqual(rumMonitorMock.addedAttributes["attribute-1"] as? Int64, nil) + XCTAssertEqual(rumMonitorMock.addedAttributes["attribute-2"] as? String, "abc") + + XCTAssertEqual(GlobalState.globalAttributes["attribute-1"] as? Int64, nil) + XCTAssertEqual(GlobalState.globalAttributes["attribute-2"] as? String, "abc") + + bridge.removeAttribute(key: "attribute-2", resolve: mockResolve, reject: mockReject) + + XCTAssertEqual(rumMonitorMock.addedAttributes["attribute-1"] as? Int64, nil) + XCTAssertEqual(rumMonitorMock.addedAttributes["attribute-2"] as? String, nil) + + XCTAssertEqual(GlobalState.globalAttributes["attribute-1"] as? Int64, nil) + XCTAssertEqual(GlobalState.globalAttributes["attribute-2"] as? String, nil) + + GlobalState.globalAttributes.removeAll() + } + + func testAddingAttributes() { let rumMonitorMock = MockRUMMonitor() let bridge = DdSdkImplementation( mainDispatchQueue: DispatchQueueMock(), @@ -734,7 +786,7 @@ class DdSdkTests: XCTestCase { reject: mockReject ) - bridge.setAttributes( + bridge.addAttributes( attributes: NSDictionary( dictionary: [ "attribute-1": 123, @@ -757,6 +809,66 @@ class DdSdkTests: XCTestCase { GlobalState.globalAttributes.removeAll() } + func testRemovingAttributes() { + let rumMonitorMock = MockRUMMonitor() + let bridge = DdSdkImplementation( + mainDispatchQueue: DispatchQueueMock(), + jsDispatchQueue: DispatchQueueMock(), + jsRefreshRateMonitor: JSRefreshRateMonitor(), + RUMMonitorProvider: { rumMonitorMock }, + RUMMonitorInternalProvider: { nil } + ) + bridge.initialize( + configuration: .mockAny(), + resolve: mockResolve, + reject: mockReject + ) + + bridge.addAttributes( + attributes: NSDictionary( + dictionary: [ + "attribute-1": 123, + "attribute-2": "abc", + "attribute-3": true, + ] + ), + resolve: mockResolve, + reject: mockReject + ) + + XCTAssertEqual(rumMonitorMock.addedAttributes["attribute-1"] as? Int64, 123) + XCTAssertEqual(rumMonitorMock.addedAttributes["attribute-2"] as? String, "abc") + XCTAssertEqual(rumMonitorMock.addedAttributes["attribute-3"] as? Bool, true) + + XCTAssertEqual(GlobalState.globalAttributes["attribute-1"] as? Int64, 123) + XCTAssertEqual(GlobalState.globalAttributes["attribute-2"] as? String, "abc") + XCTAssertEqual(GlobalState.globalAttributes["attribute-3"] as? Bool, true) + + bridge.removeAttributes( + keys: ["attribute-1", "attribute-2"], resolve: mockResolve, reject: mockReject) + + XCTAssertEqual(rumMonitorMock.addedAttributes["attribute-1"] as? Int64, nil) + XCTAssertEqual(rumMonitorMock.addedAttributes["attribute-2"] as? String, nil) + XCTAssertEqual(rumMonitorMock.addedAttributes["attribute-3"] as? Bool, true) + + XCTAssertEqual(GlobalState.globalAttributes["attribute-1"] as? Int64, nil) + XCTAssertEqual(GlobalState.globalAttributes["attribute-2"] as? String, nil) + XCTAssertEqual(GlobalState.globalAttributes["attribute-3"] as? Bool, true) + + bridge.removeAttributes(keys: ["attribute-3"], resolve: mockResolve, reject: mockReject) + + XCTAssertEqual(rumMonitorMock.addedAttributes["attribute-1"] as? Int64, nil) + XCTAssertEqual(rumMonitorMock.addedAttributes["attribute-2"] as? String, nil) + XCTAssertEqual(rumMonitorMock.addedAttributes["attribute-3"] as? Bool, nil) + + XCTAssertEqual(GlobalState.globalAttributes["attribute-1"] as? Int64, nil) + XCTAssertEqual(GlobalState.globalAttributes["attribute-2"] as? String, nil) + XCTAssertEqual(GlobalState.globalAttributes["attribute-3"] as? Bool, nil) + + GlobalState.globalAttributes.removeAll() + + } + func testBuildLongTaskThreshold() { let configuration: DdSdkConfiguration = .mockAny(nativeLongTaskThresholdMs: 2500) diff --git a/packages/core/ios/Tests/MockRUMMonitor.swift b/packages/core/ios/Tests/MockRUMMonitor.swift index f0fa03364..3a882ed47 100644 --- a/packages/core/ios/Tests/MockRUMMonitor.swift +++ b/packages/core/ios/Tests/MockRUMMonitor.swift @@ -38,14 +38,20 @@ internal class MockRUMMonitor: RUMMonitorProtocol { addedAttributes[key] = value } - func removeAttribute(forKey key: DatadogInternal.AttributeKey) {} + func removeAttribute(forKey key: DatadogInternal.AttributeKey) { + addedAttributes.removeValue(forKey: key) + } func addAttributes(_ attributes: [DatadogInternal.AttributeKey : any DatadogInternal.AttributeValue]) { - // Not implemented + for (key, value) in attributes { + addAttribute(forKey: key, value: value) + } } func removeAttributes(forKeys keys: [DatadogInternal.AttributeKey]) { - // Not implemented + for key in keys { + removeAttribute(forKey: key) + } } var debug: Bool diff --git a/packages/core/jest/mock.js b/packages/core/jest/mock.js index 8e154c4cd..c49d13f48 100644 --- a/packages/core/jest/mock.js +++ b/packages/core/jest/mock.js @@ -36,7 +36,16 @@ module.exports = { clearUserInfo: jest .fn() .mockImplementation(() => new Promise(resolve => resolve())), - setAttributes: jest + addAttribute: jest + .fn() + .mockImplementation(() => new Promise(resolve => resolve())), + removeAttribute: jest + .fn() + .mockImplementation(() => new Promise(resolve => resolve())), + addAttributes: jest + .fn() + .mockImplementation(() => new Promise(resolve => resolve())), + removeAttributes: jest .fn() .mockImplementation(() => new Promise(resolve => resolve())), setTrackingConsent: jest diff --git a/packages/core/src/DdSdkReactNative.tsx b/packages/core/src/DdSdkReactNative.tsx index 1838542df..8360a695b 100644 --- a/packages/core/src/DdSdkReactNative.tsx +++ b/packages/core/src/DdSdkReactNative.tsx @@ -175,20 +175,61 @@ export class DdSdkReactNative { ); }; + /** + * Adds a specific attribute to the global context attached with all future Logs, Spans and RUM. + * @param key: Key that identifies the attribute. + * @param value: Value linked to the attribute. + */ + static addAttribute = async ( + key: string, + value: unknown + ): Promise => { + InternalLog.log( + `Adding attribute ${JSON.stringify(value)} for key ${key}`, + SdkVerbosity.DEBUG + ); + await DdSdk.addAttribute(key, { value }); + AttributesSingleton.getInstance().addAttribute(key, value); + }; + + /** + * Removes an attribute from the context attached with all future Logs, Spans and RUM events. + * @param key: They key associated with the attribute to be removed. + */ + static removeAttribute = async (key: string): Promise => { + InternalLog.log( + `Removing attribute for key ${key}`, + SdkVerbosity.DEBUG + ); + await DdSdk.removeAttribute(key); + AttributesSingleton.getInstance().removeAttribute(key); + }; + /** * Adds a set of attributes to the global context attached with all future Logs, Spans and RUM events. - * To remove an attribute, set it to `undefined` in a call to `setAttributes`. * @param attributes: The global context attributes. * @returns a Promise. */ - // eslint-disable-next-line @typescript-eslint/ban-types - static setAttributes = async (attributes: Attributes): Promise => { + static addAttributes = async (attributes: Attributes): Promise => { + InternalLog.log( + `Adding attributes ${JSON.stringify(attributes)}`, + SdkVerbosity.DEBUG + ); + await DdSdk.addAttributes(attributes); + AttributesSingleton.getInstance().addAttributes(attributes); + }; + + /** + * Removes a set of attributes from the context attached with all future Logs, Spans and RUM events. + * @param keys: They keys associated with the attributes to be removed. + */ + static removeAttributes = async (keys: string[]): Promise => { InternalLog.log( - `Setting attributes ${JSON.stringify(attributes)}`, + `Removing attributes for keys ${JSON.stringify(keys)}`, SdkVerbosity.DEBUG ); - await DdSdk.setAttributes(attributes); - AttributesSingleton.getInstance().setAttributes(attributes); + await DdSdk.removeAttributes(keys); + AttributesSingleton.getInstance().removeAttributes(keys); }; /** diff --git a/packages/core/src/__tests__/DdSdkReactNative.test.tsx b/packages/core/src/__tests__/DdSdkReactNative.test.tsx index 57ff5d0ed..5e6f8c447 100644 --- a/packages/core/src/__tests__/DdSdkReactNative.test.tsx +++ b/packages/core/src/__tests__/DdSdkReactNative.test.tsx @@ -62,7 +62,7 @@ beforeEach(async () => { GlobalState.instance.isInitialized = false; DdSdkReactNative['wasAutoInstrumented'] = false; NativeModules.DdSdk.initialize.mockClear(); - NativeModules.DdSdk.setAttributes.mockClear(); + NativeModules.DdSdk.addAttributes.mockClear(); NativeModules.DdSdk.setTrackingConsent.mockClear(); NativeModules.DdSdk.onRUMSessionStarted.mockClear(); @@ -1045,24 +1045,80 @@ describe('DdSdkReactNative', () => { }); }); - describe('setAttributes', () => { - it('calls SDK method when setAttributes', async () => { + describe('addAttribute', () => { + it('calls SDK method when addAttribute', async () => { + // GIVEN + const key = 'foo'; + const value = 'bar'; + + // WHEN + + await DdSdkReactNative.addAttribute(key, value); + + // THEN + expect(DdSdk.addAttribute).toHaveBeenCalledTimes(1); + expect(DdSdk.addAttribute).toHaveBeenCalledWith(key, { value }); + expect(AttributesSingleton.getInstance().getAttribute(key)).toEqual( + value + ); + }); + }); + + describe('removeAttribute', () => { + it('calls SDK method when removeAttribute', async () => { + // GIVEN + const key = 'foo'; + const value = 'bar'; + await DdSdkReactNative.addAttribute(key, value); + + // WHEN + await DdSdkReactNative.removeAttribute(key); + + // THEN + expect(DdSdk.removeAttribute).toHaveBeenCalledTimes(1); + expect(DdSdk.removeAttribute).toHaveBeenCalledWith(key); + expect(AttributesSingleton.getInstance().getAttribute(key)).toEqual( + undefined + ); + }); + }); + + describe('addAttributes', () => { + it('calls SDK method when addAttributes', async () => { // GIVEN const attributes = { foo: 'bar' }; // WHEN - await DdSdkReactNative.setAttributes(attributes); + await DdSdkReactNative.addAttributes(attributes); // THEN - expect(DdSdk.setAttributes).toHaveBeenCalledTimes(1); - expect(DdSdk.setAttributes).toHaveBeenCalledWith(attributes); + expect(DdSdk.addAttributes).toHaveBeenCalledTimes(1); + expect(DdSdk.addAttributes).toHaveBeenCalledWith(attributes); expect(AttributesSingleton.getInstance().getAttributes()).toEqual({ foo: 'bar' }); }); }); + describe('removeAttributes', () => { + it('calls SDK method when removeAttributes', async () => { + // GIVEN + const attributes = { foo: 'bar', baz: 'quux' }; + await DdSdkReactNative.addAttributes(attributes); + + // WHEN + await DdSdkReactNative.removeAttributes(['foo', 'baz']); + + // THEN + expect(DdSdk.removeAttributes).toHaveBeenCalledTimes(1); + expect(DdSdk.removeAttributes).toHaveBeenCalledWith(['foo', 'baz']); + expect(AttributesSingleton.getInstance().getAttributes()).toEqual( + {} + ); + }); + }); + describe('setUserInfo', () => { it('calls SDK method when setUserInfo, and sets the user in UserProvider', async () => { // GIVEN diff --git a/packages/core/src/sdk/AttributesSingleton/AttributesSingleton.ts b/packages/core/src/sdk/AttributesSingleton/AttributesSingleton.ts index a51bb6c99..ac92c2d32 100644 --- a/packages/core/src/sdk/AttributesSingleton/AttributesSingleton.ts +++ b/packages/core/src/sdk/AttributesSingleton/AttributesSingleton.ts @@ -9,13 +9,37 @@ import type { Attributes } from './types'; class AttributesProvider { private attributes: Attributes = {}; - setAttributes = (attributes: Attributes) => { + addAttribute = (key: string, value: unknown) => { + const newAttributes = { ...this.attributes }; + newAttributes[key] = value; + this.attributes = newAttributes; + }; + + removeAttribute = (key: string) => { + const updatedAttributes = { ...this.attributes }; + delete updatedAttributes[key]; + this.attributes = updatedAttributes; + }; + + addAttributes = (attributes: Attributes) => { this.attributes = { ...this.attributes, ...attributes }; }; + removeAttributes = (keys: string[]) => { + const updated = { ...this.attributes }; + for (const k of keys) { + delete updated[k]; + } + this.attributes = updated; + }; + + getAttribute = (key: string): unknown | undefined => { + return this.attributes[key]; + }; + getAttributes = (): Attributes => { return this.attributes; }; diff --git a/packages/core/src/sdk/AttributesSingleton/__tests__/AttributesSingleton.test.ts b/packages/core/src/sdk/AttributesSingleton/__tests__/AttributesSingleton.test.ts index 23fbe5ad7..90d1133b4 100644 --- a/packages/core/src/sdk/AttributesSingleton/__tests__/AttributesSingleton.test.ts +++ b/packages/core/src/sdk/AttributesSingleton/__tests__/AttributesSingleton.test.ts @@ -7,9 +7,12 @@ import { AttributesSingleton } from '../AttributesSingleton'; describe('AttributesSingleton', () => { - it('adds, returns and resets the user info', () => { - // Adding first attributes - AttributesSingleton.getInstance().setAttributes({ + beforeEach(() => { + AttributesSingleton.reset(); + }); + + it('adds, returns and resets the attributes', () => { + AttributesSingleton.getInstance().addAttributes({ appType: 'student', extraInfo: { loggedIn: true @@ -23,11 +26,8 @@ describe('AttributesSingleton', () => { } }); - // Removing and adding new attributes - AttributesSingleton.getInstance().setAttributes({ - appType: undefined, - newAttribute: false - }); + AttributesSingleton.getInstance().removeAttribute('appType'); + AttributesSingleton.getInstance().addAttribute('newAttribute', false); expect(AttributesSingleton.getInstance().getAttributes()).toEqual({ newAttribute: false, @@ -41,4 +41,48 @@ describe('AttributesSingleton', () => { expect(AttributesSingleton.getInstance().getAttributes()).toEqual({}); }); + + it('addAttribute sets a single key and getAttribute returns it', () => { + AttributesSingleton.getInstance().addAttribute('userId', '123'); + expect(AttributesSingleton.getInstance().getAttribute('userId')).toBe( + '123' + ); + expect(AttributesSingleton.getInstance().getAttributes()).toEqual({ + userId: '123' + }); + }); + + it('removeAttribute removes a single key and leaves others intact', () => { + AttributesSingleton.getInstance().addAttributes({ + a: 1, + b: 2 + }); + + AttributesSingleton.getInstance().removeAttribute('a'); + + expect( + AttributesSingleton.getInstance().getAttribute('a') + ).toBeUndefined(); + expect(AttributesSingleton.getInstance().getAttributes()).toEqual({ + b: 2 + }); + }); + + it('removeAttributes removes multiple keys (missing keys are ignored)', () => { + AttributesSingleton.getInstance().addAttributes({ + keyToKeep: 'yes', + keyToRemove1: true, + keyToRemove2: false + }); + + AttributesSingleton.getInstance().removeAttributes([ + 'keyToRemove1', + 'keyToRemove2', + 'keyToIgnore' + ]); + + expect(AttributesSingleton.getInstance().getAttributes()).toEqual({ + keyToKeep: 'yes' + }); + }); }); diff --git a/packages/core/src/specs/NativeDdSdk.ts b/packages/core/src/specs/NativeDdSdk.ts index a2ce1120e..70401fe3c 100644 --- a/packages/core/src/specs/NativeDdSdk.ts +++ b/packages/core/src/specs/NativeDdSdk.ts @@ -26,10 +26,29 @@ export interface Spec extends TurboModule { initialize(configuration: Object): Promise; /** - * Sets the global context (set of attributes) attached with all future Logs, Spans and RUM events. + * Adds a specific attribute to the global context attached with all future Logs, Spans and RUM. + * @param key: Key that identifies the attribute. + * @param value: Value linked to the attribute. + */ + addAttribute(key: string, value: Object): Promise; + + /** + * Removes an attribute from the context attached with all future Logs, Spans and RUM events. + * @param key: They key associated with the attribute to be removed. + */ + removeAttribute(key: string): Promise; + + /** + * Adds the global context (set of attributes) attached with all future Logs, Spans and RUM events. * @param attributes: The global context attributes. */ - setAttributes(attributes: Object): Promise; + addAttributes(attributes: Object): Promise; + + /** + * Removes a set of attributes from the context attached with all future Logs, Spans and RUM events. + * @param keys: They keys associated with the attributes to be removed. + */ + removeAttributes(keys: string[]): Promise; /** * Set the user information. diff --git a/packages/core/src/types.tsx b/packages/core/src/types.tsx index bad19d429..c8d9821cc 100644 --- a/packages/core/src/types.tsx +++ b/packages/core/src/types.tsx @@ -83,11 +83,30 @@ export type DdSdkType = { */ initialize(configuration: DdSdkConfiguration): Promise; + /** + * Sets a specific attribute in the global context attached with all future Logs, Spans and RUM + * @param key: Key that identifies the attribute. + * @param value: Value linked to the attribute. + */ + addAttribute(key: string, value: object): Promise; + + /** + * Removes an attribute from the context attached with all future Logs, Spans and RUM events. + * @param key: They key associated with the attribute to be removed. + */ + removeAttribute(key: string): Promise; + /** * Sets the global context (set of attributes) attached with all future Logs, Spans and RUM events. * @param attributes: The global context attributes. */ - setAttributes(attributes: object): Promise; + addAttributes(attributes: object): Promise; + + /** + * Removes a set of attributes from the context attached with all future Logs, Spans and RUM events. + * @param keys: They keys associated with the attributes to be removed. + */ + removeAttributes(keys: string[]): Promise; /** * Sets the user information. diff --git a/packages/react-native-apollo-client/__mocks__/react-native.ts b/packages/react-native-apollo-client/__mocks__/react-native.ts index 046ced2f6..bbac607d3 100644 --- a/packages/react-native-apollo-client/__mocks__/react-native.ts +++ b/packages/react-native-apollo-client/__mocks__/react-native.ts @@ -18,9 +18,9 @@ actualRN.NativeModules.DdSdk = { initialize: jest.fn().mockImplementation( () => new Promise(resolve => resolve()) ) as jest.MockedFunction, - setAttributes: jest.fn().mockImplementation( + addAttributes: jest.fn().mockImplementation( () => new Promise(resolve => resolve()) - ) as jest.MockedFunction, + ) as jest.MockedFunction, setTrackingConsent: jest.fn().mockImplementation( () => new Promise(resolve => resolve()) ) as jest.MockedFunction, From 6e58ddfc03c6186857ad8a8b45936f543efae4ed Mon Sep 17 00:00:00 2001 From: Sergio Barrio Date: Thu, 16 Oct 2025 11:01:21 +0200 Subject: [PATCH 022/410] JS refresh rate normalization --- .../reactnative/DdSdkImplementation.kt | 51 ++++++- .../com/datadog/reactnative/DdSdkTest.kt | 128 +++++++++++++++++- .../ios/Sources/DdSdkImplementation.swift | 21 ++- packages/core/ios/Tests/DdSdkTests.swift | 108 +++++++++++++++ 4 files changed, 305 insertions(+), 3 deletions(-) diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt index 467d37335..d2d561c7a 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt @@ -7,7 +7,10 @@ package com.datadog.reactnative import android.content.Context +import android.hardware.display.DisplayManager +import android.os.Build import android.util.Log +import android.view.Display import com.datadog.android.privacy.TrackingConsent import com.datadog.android.rum.RumPerformanceMetric import com.datadog.android.rum.configuration.VitalsUpdateFrequency @@ -18,6 +21,7 @@ import com.facebook.react.bridge.ReadableMap import java.util.Locale import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicBoolean +import kotlin.math.max /** The entry point to initialize Datadog's features. */ @Suppress("TooManyFunctions") @@ -248,9 +252,10 @@ class DdSdkImplementation( return { if (jsRefreshRateMonitoringEnabled && it > 0.0) { + val normalizedFrameTimeSeconds = normalizeFrameTime(it, appContext) datadog.getRumMonitor() ._getInternal() - ?.updatePerformanceMetric(RumPerformanceMetric.JS_FRAME_TIME, it) + ?.updatePerformanceMetric(RumPerformanceMetric.JS_FRAME_TIME, normalizedFrameTimeSeconds) } if (jsLongTasksMonitoringEnabled && it > @@ -263,6 +268,49 @@ class DdSdkImplementation( } } + /** + * Normalizes frameTime values so when are turned into FPS metrics they are normalized on a range of zero to 60fps. + * @param frameTimeSeconds: the frame time to normalize. In seconds. + * @param context: The current app context + * @param fpsBudget: The maximum fps under which the frame Time will be normalized [0-fpsBudget]. Defaults to 60Hz. + * @param deviceDisplayFps: The maximum fps supported by the device. If not provided it will be set from the value obtained from the app context. + */ + @Suppress("CyclomaticComplexMethod") + fun normalizeFrameTime( + frameTimeSeconds: Double, + context: Context, + fpsBudget: Double? = null, + deviceDisplayFps: Double? = null, + ) : Double { + val frameTimeMs = frameTimeSeconds * 1000.0 + val frameBudgetHz = fpsBudget ?: DEFAULT_REFRESH_HZ + val maxDeviceDisplayHz = deviceDisplayFps ?: getMaxDisplayRefreshRate(context) + ?: 60.0 + + val maxDeviceFrameTimeMs = 1000.0 / maxDeviceDisplayHz + val budgetFrameTimeMs = 1000.0 / frameBudgetHz + + if (listOf( + maxDeviceDisplayHz, frameTimeMs, frameBudgetHz, budgetFrameTimeMs, maxDeviceFrameTimeMs + ).any { !it.isFinite() || it <= 0.0 } + ) return 1.0 / DEFAULT_REFRESH_HZ + + + var normalizedFrameTimeMs = frameTimeMs / (maxDeviceFrameTimeMs / budgetFrameTimeMs) + + normalizedFrameTimeMs = max(normalizedFrameTimeMs, maxDeviceFrameTimeMs) + + return normalizedFrameTimeMs / 1000.0 // in seconds + } + + @Suppress("CyclomaticComplexMethod") + private fun getMaxDisplayRefreshRate(context: Context?): Double { + val dm = context?.getSystemService(Context.DISPLAY_SERVICE) as? DisplayManager ?: return 60.0 + val display: Display = dm.getDisplay(Display.DEFAULT_DISPLAY) ?: return DEFAULT_REFRESH_HZ + + return display.supportedModes.maxOf { it.refreshRate.toDouble() } + } + // endregion companion object { @@ -273,6 +321,7 @@ class DdSdkImplementation( internal const val DD_DROP_ACTION = "_dd.action.drop_action" internal const val MONITOR_JS_ERROR_MESSAGE = "Error monitoring JS refresh rate" internal const val PACKAGE_INFO_NOT_FOUND_ERROR_MESSAGE = "Error getting package info" + internal const val DEFAULT_REFRESH_HZ = 60.0 internal const val NAME = "DdSdk" } } diff --git a/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt b/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt index 4abf9b981..5df758890 100644 --- a/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt +++ b/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt @@ -59,6 +59,7 @@ import java.util.Locale import java.util.stream.Stream import kotlin.time.Duration.Companion.seconds import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.data.Offset import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -78,7 +79,6 @@ import org.mockito.kotlin.doReturn import org.mockito.kotlin.doThrow import org.mockito.kotlin.eq import org.mockito.kotlin.inOrder -import org.mockito.kotlin.isNotNull import org.mockito.kotlin.isNull import org.mockito.kotlin.mock import org.mockito.kotlin.never @@ -3130,6 +3130,132 @@ internal class DdSdkTest { } } + @Test + fun `𝕄 normalize frameTime according to the device's refresh rate`() { + // 10 fps, 60Hz device, 60 fps budget -> 10 fps + var frameTimeSeconds = testedBridgeSdk.normalizeFrameTime( + frameTimeSeconds = 0.1, + context = mockContext, + fpsBudget = 60.0, + deviceDisplayFps = 60.0 + ) + assertThat(frameTimeSeconds).isEqualTo(0.1) + + // 30 fps, 60Hz device, 60 fps budget -> 30 fps + frameTimeSeconds = testedBridgeSdk.normalizeFrameTime( + frameTimeSeconds = 0.03, + context = mockContext, + fpsBudget = 60.0, + deviceDisplayFps = 60.0 + ) + assertThat(frameTimeSeconds).isEqualTo(0.03) + + // 60 fps, 60Hz device, 60 fps budget -> 60 fps + frameTimeSeconds = testedBridgeSdk.normalizeFrameTime( + frameTimeSeconds = 0.016, + context = mockContext, + fpsBudget = 60.0, + deviceDisplayFps = 60.0 + ) + assertThat(frameTimeSeconds).isEqualTo(0.016, Offset.offset(0.005)) + + // 60 fps, 120Hz device, 60 fps budget -> 30 fps + frameTimeSeconds = testedBridgeSdk.normalizeFrameTime( + frameTimeSeconds = 0.016, + context = mockContext, + fpsBudget = 60.0, + deviceDisplayFps = 120.0 + ) + assertThat(frameTimeSeconds).isEqualTo(0.032) + + // 120 fps, 120Hz device, 60 fps budget -> 60 fps + frameTimeSeconds = testedBridgeSdk.normalizeFrameTime( + frameTimeSeconds = 0.0083, + context = mockContext, + fpsBudget = 60.0, + deviceDisplayFps = 120.0 + ) + assertThat(frameTimeSeconds).isEqualTo(0.016, Offset.offset(0.005)) + + // 90 fps, 120Hz device, 60 fps budget -> 45 fps + frameTimeSeconds = testedBridgeSdk.normalizeFrameTime( + frameTimeSeconds = 0.0111, + context = mockContext, + fpsBudget = 60.0, + deviceDisplayFps = 120.0 + ) + assertThat(frameTimeSeconds).isEqualTo(0.0222, Offset.offset(0.001)) + + // 100 fps, 120Hz device, 60 fps budget -> 50 fps + frameTimeSeconds = testedBridgeSdk.normalizeFrameTime( + frameTimeSeconds = 0.01, + context = mockContext, + fpsBudget = 60.0, + deviceDisplayFps = 120.0 + ) + assertThat(frameTimeSeconds).isEqualTo(0.02, Offset.offset(0.001)) + + // 120 fps, 120Hz device, 120 fps budget -> 120 fps + frameTimeSeconds = testedBridgeSdk.normalizeFrameTime( + frameTimeSeconds = 0.0083, + context = mockContext, + fpsBudget = 120.0, + deviceDisplayFps = 120.0 + ) + assertThat(frameTimeSeconds).isEqualTo(0.0083, Offset.offset(0.001)) + + // 80 fps, 160Hz device, 60 fps budget -> 30 fps + frameTimeSeconds = testedBridgeSdk.normalizeFrameTime( + frameTimeSeconds = 0.0125, + context = mockContext, + fpsBudget = 60.0, + deviceDisplayFps = 160.0 + ) + assertThat(frameTimeSeconds).isEqualTo(0.033, Offset.offset(0.001)) + + // 160 fps, 160Hz device, 60 fps budget -> 60 fps + frameTimeSeconds = testedBridgeSdk.normalizeFrameTime( + frameTimeSeconds = 0.00625, + context = mockContext, + fpsBudget = 60.0, + deviceDisplayFps = 160.0 + ) + assertThat(frameTimeSeconds).isEqualTo(0.016, Offset.offset(0.001)) + + // Edge cases + frameTimeSeconds = testedBridgeSdk.normalizeFrameTime( + frameTimeSeconds = 0.0, + context = mockContext, + fpsBudget = 0.0, + deviceDisplayFps = 0.0 + ) + assertThat(frameTimeSeconds).isEqualTo(0.016, Offset.offset(0.001)) + + frameTimeSeconds = testedBridgeSdk.normalizeFrameTime( + frameTimeSeconds = 0.016, + context = mockContext, + fpsBudget = 0.0, + deviceDisplayFps = 0.0 + ) + assertThat(frameTimeSeconds).isEqualTo(0.016, Offset.offset(0.001)) + + frameTimeSeconds = testedBridgeSdk.normalizeFrameTime( + frameTimeSeconds = 0.016, + context = mockContext, + fpsBudget = 60.0, + deviceDisplayFps = 0.0 + ) + assertThat(frameTimeSeconds).isEqualTo(0.016, Offset.offset(0.001)) + + frameTimeSeconds = testedBridgeSdk.normalizeFrameTime( + frameTimeSeconds = 0.016, + context = mockContext, + fpsBudget = 0.0, + deviceDisplayFps = 60.0 + ) + assertThat(frameTimeSeconds).isEqualTo(0.016, Offset.offset(0.001)) + } + // endregion // region Internal diff --git a/packages/core/ios/Sources/DdSdkImplementation.swift b/packages/core/ios/Sources/DdSdkImplementation.swift index 9d057d158..387f8495d 100644 --- a/packages/core/ios/Sources/DdSdkImplementation.swift +++ b/packages/core/ios/Sources/DdSdkImplementation.swift @@ -203,7 +203,8 @@ public class DdSdkImplementation: NSObject { // Leave JS thread ASAP to give as much time to JS engine work. sharedQueue.async { if (shouldRecordFrameTime) { - rumMonitorInternal.updatePerformanceMetric(at: now, metric: .jsFrameTimeSeconds, value: frameTime, attributes: [:]) + let normalizedFrameTimeSeconds = DdSdkImplementation.normalizeFrameTimeForDeviceRefreshRate(frameTime) + rumMonitorInternal.updatePerformanceMetric(at: now, metric: .jsFrameTimeSeconds, value: normalizedFrameTimeSeconds, attributes: [:]) } if (shouldRecordLongTask) { rumMonitorInternal.addLongTask(at: now, duration: frameTime, attributes: ["long_task.target": "javascript"]) @@ -213,5 +214,23 @@ public class DdSdkImplementation: NSObject { return frameTimeCallback } + + // Normalizes frameTime values so when they are turned into FPS metrics they are normalized on a range between 0 and fpsBudget. If fpsBudget is not provided it will default to 60hz. + public static func normalizeFrameTimeForDeviceRefreshRate(_ frameTime: Double, fpsBudget: Double? = nil, deviceDisplayFps: Double? = nil) -> Double { + let DEFAULT_REFRESH_HZ = 60.0 + let frameTimeMs: Double = frameTime * 1000.0 + let frameBudgetHz: Double = fpsBudget ?? DEFAULT_REFRESH_HZ + let maxDeviceDisplayHz = deviceDisplayFps ?? Double(UIScreen.main.maximumFramesPerSecond) + let maxDeviceFrameTimeMs = 1000.0 / maxDeviceDisplayHz + let budgetFrameTimeMs = 1000.0 / frameBudgetHz + + guard maxDeviceDisplayHz > 0, frameTimeMs.isFinite, frameTimeMs > 0, frameBudgetHz > 0, budgetFrameTimeMs.isFinite, budgetFrameTimeMs > 0, maxDeviceFrameTimeMs.isFinite, maxDeviceFrameTimeMs > 0 else { + return 1.0 / DEFAULT_REFRESH_HZ + } + + var normalizedFrameTimeMs = frameTimeMs / (maxDeviceFrameTimeMs / budgetFrameTimeMs) + normalizedFrameTimeMs = max(normalizedFrameTimeMs, maxDeviceFrameTimeMs) + return normalizedFrameTimeMs / 1000.0 // in seconds + } } diff --git a/packages/core/ios/Tests/DdSdkTests.swift b/packages/core/ios/Tests/DdSdkTests.swift index efbf57b96..bf610997b 100644 --- a/packages/core/ios/Tests/DdSdkTests.swift +++ b/packages/core/ios/Tests/DdSdkTests.swift @@ -1069,6 +1069,114 @@ class DdSdkTests: XCTestCase { XCTAssertEqual(rumMonitorMock.receivedLongTasks.first?.value, 0.25) XCTAssertEqual(rumMonitorMock.lastReceivedPerformanceMetrics[.jsFrameTimeSeconds], 0.25) } + + func testFrameTimeNormalizationFromCallback() { + let mockRefreshRateMonitor = MockJSRefreshRateMonitor() + let rumMonitorMock = MockRUMMonitor() + + DdSdkImplementation( + mainDispatchQueue: DispatchQueueMock(), + jsDispatchQueue: DispatchQueueMock(), + jsRefreshRateMonitor: mockRefreshRateMonitor, + RUMMonitorProvider: { rumMonitorMock }, + RUMMonitorInternalProvider: { rumMonitorMock._internalMock } + ).initialize( + configuration: .mockAny( + longTaskThresholdMs: 200, + vitalsUpdateFrequency: "average" + ), + resolve: mockResolve, + reject: mockReject + ) + + XCTAssertTrue(mockRefreshRateMonitor.isStarted) + + // 10 fps + mockRefreshRateMonitor.executeFrameCallback(frameTime: 0.1) + sharedQueue.sync {} + XCTAssertEqual(rumMonitorMock.lastReceivedPerformanceMetrics[.jsFrameTimeSeconds], 0.1) + + // 30 fps + mockRefreshRateMonitor.executeFrameCallback(frameTime: 0.03) + sharedQueue.sync {} + XCTAssertEqual(rumMonitorMock.lastReceivedPerformanceMetrics[.jsFrameTimeSeconds], 0.03) + + // 45 fps + mockRefreshRateMonitor.executeFrameCallback(frameTime: 0.02) + sharedQueue.sync {} + XCTAssertEqual(rumMonitorMock.lastReceivedPerformanceMetrics[.jsFrameTimeSeconds], 0.02) + + // 60 fps + mockRefreshRateMonitor.executeFrameCallback(frameTime: 0.016) + sharedQueue.sync {} + XCTAssertEqual(rumMonitorMock.lastReceivedPerformanceMetrics[.jsFrameTimeSeconds]!, 0.016, accuracy: 0.001) + + // 90 fps + mockRefreshRateMonitor.executeFrameCallback(frameTime: 0.011) + sharedQueue.sync {} + XCTAssertEqual(rumMonitorMock.lastReceivedPerformanceMetrics[.jsFrameTimeSeconds]!, 0.016, accuracy: 0.001) + + // 120 fps + mockRefreshRateMonitor.executeFrameCallback(frameTime: 0.008) + sharedQueue.sync {} + XCTAssertEqual(rumMonitorMock.lastReceivedPerformanceMetrics[.jsFrameTimeSeconds]!, 0.016, accuracy: 0.001) + } + + func testFrameTimeNormalizationUtilityFunction() { + + // 10 fps, 60fps capable device, 60 fps budget -> Normalized to 10fps + var frameTimeSeconds = DdSdkImplementation.normalizeFrameTimeForDeviceRefreshRate(0.1, fpsBudget: 60.0, deviceDisplayFps: 60.0) + XCTAssertEqual(frameTimeSeconds, 0.1, accuracy: 0.01) + + // 30 fps, 60fps capable device, 60 fps budget -> Normalized to 30fps + frameTimeSeconds = DdSdkImplementation.normalizeFrameTimeForDeviceRefreshRate(0.03, fpsBudget: 60.0, deviceDisplayFps: 60.0) + XCTAssertEqual(frameTimeSeconds, 0.03, accuracy: 0.01) + + // 60 fps, 60fps capable device, 60 fps budget-> Normalized to 60fps + frameTimeSeconds = DdSdkImplementation.normalizeFrameTimeForDeviceRefreshRate(0.016, fpsBudget: 60.0, deviceDisplayFps: 60.0) + XCTAssertEqual(frameTimeSeconds, 0.016, accuracy: 0.01) + + // 60 fps, 120fps capable device, 60 fps budget -> Normalized to 30fps + frameTimeSeconds = DdSdkImplementation.normalizeFrameTimeForDeviceRefreshRate(0.016, fpsBudget: 60.0, deviceDisplayFps: 120.0) + XCTAssertEqual(frameTimeSeconds, 0.03, accuracy: 0.01) + + // 120 fps, 120fps capable device, 60 fps budget -> Normalized to 60fps + frameTimeSeconds = DdSdkImplementation.normalizeFrameTimeForDeviceRefreshRate(0.0083, fpsBudget: 60.0, deviceDisplayFps: 120.0) + XCTAssertEqual(frameTimeSeconds, 0.016, accuracy: 0.001) + + // 90 fps, 120fps capable device, 60 fps budget -> Normalized to 45fps + frameTimeSeconds = DdSdkImplementation.normalizeFrameTimeForDeviceRefreshRate(0.0111, fpsBudget: 60.0, deviceDisplayFps: 120.0) + XCTAssertEqual(frameTimeSeconds, 0.0222, accuracy: 0.001) + + // 100 fps, 120fps capable device, 60 fps budget -> Normalized to 50fps + frameTimeSeconds = DdSdkImplementation.normalizeFrameTimeForDeviceRefreshRate(0.01, fpsBudget: 60.0, deviceDisplayFps: 120.0) + XCTAssertEqual(frameTimeSeconds, 0.02, accuracy: 0.001) + + // 120 fps, 120fps capable device, 120 fps budget -> Normalized to 120fps + frameTimeSeconds = DdSdkImplementation.normalizeFrameTimeForDeviceRefreshRate(0.0083, fpsBudget: 120.0, deviceDisplayFps: 120.0) + XCTAssertEqual(frameTimeSeconds, 0.0083, accuracy: 0.001) + + // 80 fps, 160fps capable device, 60 fps budget -> Normalized to 30fps + frameTimeSeconds = DdSdkImplementation.normalizeFrameTimeForDeviceRefreshRate(0.0125, fpsBudget: 60.0, deviceDisplayFps: 160.0) + XCTAssertEqual(frameTimeSeconds, 0.033, accuracy: 0.001) + + // 160 fps, 160fps capable device, 60 fps budget -> Normalized to 60fps + frameTimeSeconds = DdSdkImplementation.normalizeFrameTimeForDeviceRefreshRate(0.00625, fpsBudget: 60.0, deviceDisplayFps: 160.0) + XCTAssertEqual(frameTimeSeconds, 0.016, accuracy: 0.001) + + // Edge cases + frameTimeSeconds = DdSdkImplementation.normalizeFrameTimeForDeviceRefreshRate(0, fpsBudget: 0, deviceDisplayFps: 0) + XCTAssertEqual(frameTimeSeconds, 0.016, accuracy: 0.001) + + frameTimeSeconds = DdSdkImplementation.normalizeFrameTimeForDeviceRefreshRate(0.016, fpsBudget: 0, deviceDisplayFps: 0) + XCTAssertEqual(frameTimeSeconds, 0.016, accuracy: 0.001) + + frameTimeSeconds = DdSdkImplementation.normalizeFrameTimeForDeviceRefreshRate(0.016, fpsBudget: 60.0, deviceDisplayFps: 0) + XCTAssertEqual(frameTimeSeconds, 0.016, accuracy: 0.001) + + frameTimeSeconds = DdSdkImplementation.normalizeFrameTimeForDeviceRefreshRate(0.016, fpsBudget: 0, deviceDisplayFps: 60.0) + XCTAssertEqual(frameTimeSeconds, 0.016, accuracy: 0.001) + } func testSDKInitializationWithCustomEndpoints() throws { let mockRefreshRateMonitor = MockJSRefreshRateMonitor() From 10209fa3edbece5f16e5eef774ef654ca1d193a5 Mon Sep 17 00:00:00 2001 From: Sergio Barrio Date: Fri, 10 Oct 2025 14:52:21 +0200 Subject: [PATCH 023/410] Expose view Attributes API --- packages/core/__mocks__/react-native.ts | 12 ++ .../reactnative/DdRumImplementation.kt | 44 +++++++ .../reactnative/DdSdkImplementation.kt | 2 - .../kotlin/com/datadog/reactnative/DdRum.kt | 38 ++++++ .../kotlin/com/datadog/reactnative/DdRum.kt | 38 ++++++ .../com/datadog/reactnative/DdRumTest.kt | 58 +++++++++ .../com/datadog/tools/unit/MockRumMonitor.kt | 17 +-- packages/core/ios/Sources/DdRum.mm | 47 ++++++- .../ios/Sources/DdRumImplementation.swift | 28 ++++ .../ios/Sources/DdSdkImplementation.swift | 120 +++++++++++------- packages/core/ios/Tests/DdRumTests.swift | 59 +++++++++ packages/core/ios/Tests/MockRUMMonitor.swift | 38 +++--- packages/core/jest/mock.js | 12 ++ packages/core/src/rum/DdRum.ts | 45 +++++++ packages/core/src/rum/__tests__/DdRum.test.ts | 92 ++++++++++++++ packages/core/src/rum/types.ts | 26 ++++ packages/core/src/specs/NativeDdRum.ts | 25 ++++ 17 files changed, 631 insertions(+), 70 deletions(-) diff --git a/packages/core/__mocks__/react-native.ts b/packages/core/__mocks__/react-native.ts index 24e3f80c7..73308f711 100644 --- a/packages/core/__mocks__/react-native.ts +++ b/packages/core/__mocks__/react-native.ts @@ -132,6 +132,18 @@ actualRN.NativeModules.DdRum = { addTiming: jest.fn().mockImplementation( () => new Promise(resolve => resolve()) ) as jest.MockedFunction, + addViewAttribute: jest.fn().mockImplementation( + () => new Promise(resolve => resolve()) + ) as jest.MockedFunction, + removeViewAttribute: jest.fn().mockImplementation( + () => new Promise(resolve => resolve()) + ) as jest.MockedFunction, + addViewAttributes: jest.fn().mockImplementation( + () => new Promise(resolve => resolve()) + ) as jest.MockedFunction, + removeViewAttributes: jest.fn().mockImplementation( + () => new Promise(resolve => resolve()) + ) as jest.MockedFunction, addViewLoadingTime: jest.fn().mockImplementation( () => new Promise(resolve => resolve()) ) as jest.MockedFunction, diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdRumImplementation.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdRumImplementation.kt index 35471374e..977f2bec0 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdRumImplementation.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdRumImplementation.kt @@ -13,6 +13,7 @@ import com.datadog.android.rum.RumErrorSource import com.datadog.android.rum.RumResourceKind import com.datadog.android.rum.RumResourceMethod import com.facebook.react.bridge.Promise +import com.facebook.react.bridge.ReadableArray import com.facebook.react.bridge.ReadableMap import java.util.Locale @@ -248,6 +249,49 @@ class DdRumImplementation(private val datadog: DatadogWrapper = DatadogSDKWrappe promise.resolve(null) } + /** + * Adds a custom attribute to the active RUM View. It will be propagated to all future RUM events associated with the active View. + * @param key: key for this view attribute. + * @param value: value for this attribute. + */ + fun addViewAttribute(key: String, value: ReadableMap, promise: Promise) { + val attributeValue = value.toMap()["value"] + val attributes = mutableMapOf() + attributes[key] = attributeValue + datadog.getRumMonitor().addViewAttributes(attributes) + promise.resolve(null) + } + + /** + * Removes an attribute from the active RUM View. + * @param key: key for the attribute to be removed from the view. + */ + fun removeViewAttribute(key: String, promise: Promise) { + val keysToDelete: Collection = listOf(key) + datadog.getRumMonitor().removeViewAttributes(keysToDelete) + promise.resolve(null) + } + + /** + * Adds multiple attributes to the active RUM View. They will be propagated to all future RUM events associated with the active View. + * @param attributes: key/value object containing all attributes to be added to the view. + */ + fun addViewAttributes(attributes: ReadableMap, promise: Promise) { + datadog.getRumMonitor().addViewAttributes(attributes.toMap()) + promise.resolve(null) + } + + /** + * Removes multiple attributes from the active RUM View. + * @param keys: keys for the attributes to be removed from the view. + */ + fun removeViewAttributes(keys: ReadableArray, promise: Promise) { + val keysToDelete = (0 until keys.size()) + .mapNotNull { keys.getString(it) } + datadog.getRumMonitor().removeViewAttributes(keysToDelete) + promise.resolve(null) + } + /** * Adds the loading time of the view to the active view. * It is calculated as the difference between the current time and the start time of the view. diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt index 4d0d3e355..148459331 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt @@ -143,8 +143,6 @@ class DdSdkImplementation( if (id != null) { datadog.setUserInfo(id, name, email, extraInfo) - } else { - // TO DO - Log warning? } promise.resolve(null) diff --git a/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdRum.kt b/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdRum.kt index ce8104685..6cb2b385b 100644 --- a/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdRum.kt +++ b/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdRum.kt @@ -9,6 +9,7 @@ package com.datadog.reactnative import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.ReactMethod +import com.facebook.react.bridge.ReadableArray import com.facebook.react.bridge.ReadableMap /** @@ -201,6 +202,43 @@ class DdRum( implementation.addTiming(name, promise) } + /** + * Adds a custom attribute to the active RUM View. It will be propagated to all future RUM events associated with the active View. + * @param key: key for this view attribute. + * @param value: value for this attribute. + */ + @ReactMethod + override fun addViewAttribute(key: String, value: ReadableMap, promise: Promise) { + implementation.addViewAttribute(key, value, promise) + } + + /** + * Removes an attribute from the active RUM View. + * @param key: key for the attribute to be removed from the view. + */ + @ReactMethod + override fun removeViewAttribute(key: String, promise: Promise) { + implementation.removeViewAttribute(key, promise) + } + + /** + * Adds multiple attributes to the active RUM View. They will be propagated to all future RUM events associated with the active View. + * @param attributes: key/value object containing all attributes to be added to the view. + */ + @ReactMethod + override fun addViewAttributes(attributes: ReadableMap, promise: Promise) { + implementation.addViewAttributes(attributes, promise) + } + + /** + * Removes multiple attributes from the active RUM View. + * @param keys: keys for the attributes to be removed from the view. + */ + @ReactMethod + override fun removeViewAttributes(keys: ReadableArray, promise: Promise) { + implementation.removeViewAttributes(keys, promise) + } + /** * Adds the loading time of the view to the active view. * It is calculated as the difference between the current time and the start time of the view. diff --git a/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdRum.kt b/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdRum.kt index 79742e854..a6c4965ea 100644 --- a/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdRum.kt +++ b/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdRum.kt @@ -10,6 +10,7 @@ import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.ReactContextBaseJavaModule import com.facebook.react.bridge.ReactMethod +import com.facebook.react.bridge.ReadableArray import com.facebook.react.bridge.ReadableMap /** @@ -192,6 +193,43 @@ class DdRum( implementation.addTiming(name, promise) } + /** + * Adds a custom attribute to the active RUM View. It will be propagated to all future RUM events associated with the active View. + * @param key: key for this view attribute. + * @param value: value for this attribute. + */ + @ReactMethod + fun addViewAttribute(key: String, value: ReadableMap, promise: Promise) { + implementation.addViewAttribute(key, value, promise) + } + + /** + * Removes an attribute from the active RUM View. + * @param key: key for the attribute to be removed from the view. + */ + @ReactMethod + fun removeViewAttribute(key: String, promise: Promise) { + implementation.removeViewAttribute(key, promise) + } + + /** + * Adds multiple attributes to the active RUM View. They will be propagated to all future RUM events associated with the active View. + * @param attributes: key/value object containing all attributes to be added to the view. + */ + @ReactMethod + fun addViewAttributes(attributes: ReadableMap, promise: Promise) { + implementation.addViewAttributes(attributes, promise) + } + + /** + * Removes multiple attributes from the active RUM View. + * @param keys: keys for the attributes to be removed from the view. + */ + @ReactMethod + fun removeViewAttributes(keys: ReadableArray, promise: Promise) { + implementation.removeViewAttributes(keys, promise) + } + /** * Adds the loading time of the view to the active view. * It is calculated as the difference between the current time and the start time of the view. diff --git a/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdRumTest.kt b/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdRumTest.kt index be1c57b3a..9619794df 100644 --- a/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdRumTest.kt +++ b/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdRumTest.kt @@ -13,13 +13,16 @@ import com.datadog.android.rum.RumMonitor import com.datadog.android.rum.RumResourceKind import com.datadog.android.rum.RumResourceMethod import com.datadog.tools.unit.forge.BaseConfigurator +import com.datadog.tools.unit.toReadableArray import com.datadog.tools.unit.toReadableMap import com.facebook.react.bridge.Promise import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.annotation.AdvancedForgery import fr.xgouchet.elmyr.annotation.BoolForgery import fr.xgouchet.elmyr.annotation.DoubleForgery import fr.xgouchet.elmyr.annotation.Forgery import fr.xgouchet.elmyr.annotation.IntForgery +import fr.xgouchet.elmyr.annotation.MapForgery import fr.xgouchet.elmyr.annotation.StringForgery import fr.xgouchet.elmyr.annotation.StringForgeryType import fr.xgouchet.elmyr.junit5.ForgeConfiguration @@ -456,6 +459,61 @@ internal class DdRumTest { verify(mockRumMonitor).addTiming(timing) } + @Test + fun `M call addViewAttribute W addViewAttribute()`( + @StringForgery key: String, + @StringForgery value: String + ) { + var attributeMap = mutableMapOf() + attributeMap.put("value", value) + + var attributes = mutableMapOf() + attributes.put(key, value) + + // When + testedDdRum.addViewAttribute(key, attributeMap.toReadableMap(), mockPromise) + + // Then + verify(mockRumMonitor).addViewAttributes(attributes) + } + + @Test + fun `M call removeViewAttribute W removeViewAttribute()`(@StringForgery key: String) { + // When + testedDdRum.removeViewAttribute(key, mockPromise) + + // Then + verify(mockRumMonitor).removeViewAttributes(listOf(key)) + } + + @Test + fun `M call addViewAttributes W addViewAttributes()`( + @MapForgery( + key = AdvancedForgery(string = [StringForgery(StringForgeryType.NUMERICAL)]), + value = AdvancedForgery(string = [StringForgery(StringForgeryType.ASCII)]) + ) customAttributes: Map + ) { + // When + testedDdRum.addViewAttributes(customAttributes.toReadableMap(), mockPromise) + + // Then + verify(mockRumMonitor).addViewAttributes(customAttributes) + } + + @Test + fun `𝕄 call removeViewAttributes 𝕎 removeViewAttributes`( + @MapForgery( + key = AdvancedForgery(string = [StringForgery(StringForgeryType.NUMERICAL)]), + value = AdvancedForgery(string = [StringForgery(StringForgeryType.ASCII)]) + ) customAttributes: Map + ) { + // When + testedDdRum.removeViewAttributes(customAttributes.keys.toReadableArray(), mockPromise) + + // Then + verify(mockRumMonitor).removeViewAttributes(customAttributes.keys.toList()) + } + @Test fun `M call addViewLoadingTime w addViewLoadingTime()`(@BoolForgery overwrite: Boolean) { // When diff --git a/packages/core/android/src/test/kotlin/com/datadog/tools/unit/MockRumMonitor.kt b/packages/core/android/src/test/kotlin/com/datadog/tools/unit/MockRumMonitor.kt index 702cc2533..13f73d94a 100644 --- a/packages/core/android/src/test/kotlin/com/datadog/tools/unit/MockRumMonitor.kt +++ b/packages/core/android/src/test/kotlin/com/datadog/tools/unit/MockRumMonitor.kt @@ -30,7 +30,13 @@ class MockRumMonitor : RumMonitor { override fun addAttribute(key: String, value: Any?) {} - override fun addViewAttributes(attributes: Map) {} + override fun removeAttribute(key: String) {} + + override fun clearAttributes() {} + + override fun getAttributes(): Map { + return mapOf() + } override fun addError( message: String, @@ -55,15 +61,10 @@ class MockRumMonitor : RumMonitor { @ExperimentalRumApi override fun addViewLoadingTime(overwrite: Boolean) {} - override fun clearAttributes() {} - - override fun getAttributes(): Map { - return mapOf() - } - override fun getCurrentSessionId(callback: (String?) -> Unit) {} - override fun removeAttribute(key: String) {} + override fun addViewAttributes(attributes: Map) {} + override fun removeViewAttributes(attributes: Collection) {} override fun startAction( diff --git a/packages/core/ios/Sources/DdRum.mm b/packages/core/ios/Sources/DdRum.mm index 5d831942a..f5c324ce8 100644 --- a/packages/core/ios/Sources/DdRum.mm +++ b/packages/core/ios/Sources/DdRum.mm @@ -107,6 +107,35 @@ @implementation DdRum [self addTiming:name resolve:resolve reject:reject]; } +RCT_EXPORT_METHOD(addViewAttribute:(NSString*) key + withValue:(NSDictionary*) value + withResolver:(RCTPromiseResolveBlock)resolve + withRejecter:(RCTPromiseRejectBlock)reject) +{ + [self addViewAttribute:key value:value resolve:resolve reject:reject]; +} + +RCT_EXPORT_METHOD(removeViewAttribute:(NSString*) key + withResolver:(RCTPromiseResolveBlock)resolve + withRejecter:(RCTPromiseRejectBlock)reject) +{ + [self removeViewAttribute:key resolve:resolve reject:reject]; +} + +RCT_EXPORT_METHOD(addViewAttributes:(NSDictionary*) attributes + withResolver:(RCTPromiseResolveBlock)resolve + withRejecter:(RCTPromiseRejectBlock)reject) +{ + [self addViewAttributes:attributes resolve:resolve reject:reject]; +} + +RCT_EXPORT_METHOD(removeViewAttributes:(NSArray *)keys + withResolver:(RCTPromiseResolveBlock)resolve + withRejecter:(RCTPromiseRejectBlock)reject) +{ + [self removeViewAttributes:keys resolve:resolve reject:reject]; +} + RCT_REMAP_METHOD(addViewLoadingTime, withOverwrite:(BOOL)overwrite withResolver:(RCTPromiseResolveBlock)resolve withRejecter:(RCTPromiseRejectBlock)reject) @@ -138,7 +167,7 @@ @implementation DdRum // Thanks to this guard, we won't compile this code when we build for the old architecture. #ifdef RCT_NEW_ARCH_ENABLED - (std::shared_ptr)getTurboModule: - (const facebook::react::ObjCTurboModule::InitParams &)params +(const facebook::react::ObjCTurboModule::InitParams &)params { return std::make_shared(params); } @@ -180,6 +209,22 @@ - (void)addTiming:(NSString *)name resolve:(RCTPromiseResolveBlock)resolve rejec [self.ddRumImplementation addTimingWithName:name resolve:resolve reject:reject]; } +- (void)addViewAttribute:(NSString *)key value:(NSDictionary *)value resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + [self.ddRumImplementation addViewAttributeWithKey:key value:value resolve:resolve reject:reject]; +} + +- (void)removeViewAttribute:(NSString *)key resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + [self.ddRumImplementation removeViewAttributeWithKey:key resolve:resolve reject:reject]; +} + +- (void)addViewAttributes:(NSDictionary *)attributes resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + [self.ddRumImplementation addViewAttributesWithAttributes:attributes resolve:resolve reject:reject]; +} + +- (void)removeViewAttributes:(NSArray *)keys resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + [self.ddRumImplementation removeViewAttributesWithKeys:keys resolve:resolve reject:reject]; +} + - (void)addViewLoadingTime:(BOOL)overwrite resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {\ [self.ddRumImplementation addViewLoadingTimeWithOverwrite:overwrite resolve:resolve reject:reject]; } diff --git a/packages/core/ios/Sources/DdRumImplementation.swift b/packages/core/ios/Sources/DdRumImplementation.swift index 9f8da4c7f..6fac21f82 100644 --- a/packages/core/ios/Sources/DdRumImplementation.swift +++ b/packages/core/ios/Sources/DdRumImplementation.swift @@ -181,6 +181,34 @@ public class DdRumImplementation: NSObject { resolve(nil) } + @objc + public func addViewAttribute(key: AttributeKey, value: NSDictionary, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { + if let attributeValue = value.object(forKey: "value") { + let castedAttribute = castValueToSwift(attributeValue) + nativeRUM.addViewAttribute(forKey: key, value: castedAttribute) + } + resolve(nil) + } + + @objc + public func removeViewAttribute(key: AttributeKey, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { + nativeRUM.removeViewAttribute(forKey: key) + resolve(nil) + } + + @objc + public func addViewAttributes(attributes: NSDictionary, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { + let castedAttributes = castAttributesToSwift(attributes) + nativeRUM.addViewAttributes(castedAttributes) + resolve(nil) + } + + @objc + public func removeViewAttributes(keys: [AttributeKey], resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { + nativeRUM.removeViewAttributes(forKeys: keys) + resolve(nil) + } + @objc public func addViewLoadingTime(overwrite: Bool, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { nativeRUM.addViewLoadingTime(overwrite: overwrite) diff --git a/packages/core/ios/Sources/DdSdkImplementation.swift b/packages/core/ios/Sources/DdSdkImplementation.swift index 6e24af4e3..87fe91729 100644 --- a/packages/core/ios/Sources/DdSdkImplementation.swift +++ b/packages/core/ios/Sources/DdSdkImplementation.swift @@ -4,18 +4,19 @@ * Copyright 2016-Present Datadog, Inc. */ -import Foundation import DatadogCore -import DatadogRUM +import DatadogCrashReporting +import DatadogInternal import DatadogLogs +import DatadogRUM import DatadogTrace -import DatadogCrashReporting import DatadogWebViewTracking -import DatadogInternal +import Foundation import React func getDefaultAppVersion() -> String { - let bundleShortVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String + let bundleShortVersion = + Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String let bundleVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String return bundleShortVersion ?? bundleVersion ?? "0.0.0" } @@ -29,7 +30,7 @@ public class DdSdkImplementation: NSObject { let RUMMonitorInternalProvider: () -> RUMMonitorInternalProtocol? var webviewMessageEmitter: InternalExtension.AbstractMessageEmitter? - private let jsLongTaskThresholdInSeconds: TimeInterval = 0.1; + private let jsLongTaskThresholdInSeconds: TimeInterval = 0.1 @objc public convenience init(bridge: RCTBridge) { @@ -41,7 +42,7 @@ public class DdSdkImplementation: NSObject { RUMMonitorInternalProvider: { RUMMonitor.shared()._internal } ) } - + init( mainDispatchQueue: DispatchQueueType, jsDispatchQueue: DispatchQueueType, @@ -56,10 +57,13 @@ public class DdSdkImplementation: NSObject { self.RUMMonitorInternalProvider = RUMMonitorInternalProvider super.init() } - + // Using @escaping RCTPromiseResolveBlock type will result in an issue when compiling the Swift header file. @objc - public func initialize(configuration: NSDictionary, resolve:@escaping ((Any?) -> Void), reject:RCTPromiseRejectBlock) -> Void { + public func initialize( + configuration: NSDictionary, resolve: @escaping ((Any?) -> Void), + reject: RCTPromiseRejectBlock + ) { let sdkConfiguration = configuration.asDdSdkConfiguration() let nativeInitialization = DdSdkNativeInitialization() @@ -111,7 +115,9 @@ public class DdSdkImplementation: NSObject { } @objc - public func setUserInfo(userInfo: NSDictionary, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { + public func setUserInfo( + userInfo: NSDictionary, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock + ) { let castedUserInfo = castAttributesToSwift(userInfo) let id = castedUserInfo["id"] as? String let name = castedUserInfo["name"] as? String @@ -119,21 +125,22 @@ public class DdSdkImplementation: NSObject { var extraInfo: [AttributeKey: AttributeValue] = [:] if let extraInfoEncodable = castedUserInfo["extraInfo"] as? AnyEncodable, - let extraInfoDict = extraInfoEncodable.value as? [String: Any] { + let extraInfoDict = extraInfoEncodable.value as? [String: Any] + { extraInfo = castAttributesToSwift(extraInfoDict) } if let validId = id { Datadog.setUserInfo(id: validId, name: name, email: email, extraInfo: extraInfo) - } else { - // TO DO - log warning message? } resolve(nil) } - + @objc - public func addUserExtraInfo(extraInfo: NSDictionary, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { + public func addUserExtraInfo( + extraInfo: NSDictionary, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock + ) { let castedExtraInfo = castAttributesToSwift(extraInfo) Datadog.addUserExtraInfo(castedExtraInfo) @@ -141,59 +148,80 @@ public class DdSdkImplementation: NSObject { } @objc - public func clearUserInfo(resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { + public func clearUserInfo(resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) { Datadog.clearUserInfo() resolve(nil) } @objc - public func setTrackingConsent(trackingConsent: NSString, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { + public func setTrackingConsent( + trackingConsent: NSString, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock + ) { Datadog.set(trackingConsent: (trackingConsent as NSString?).asTrackingConsent()) resolve(nil) } - - + @objc - public func sendTelemetryLog(message: NSString, attributes: NSDictionary, config: NSDictionary, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { + public func sendTelemetryLog( + message: NSString, attributes: NSDictionary, config: NSDictionary, + resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock + ) { let castedAttributes = castAttributesToSwift(attributes) let castedConfig = castAttributesToSwift(config) - DdTelemetry.sendTelemetryLog(message: message as String, attributes: castedAttributes, config: castedConfig) + DdTelemetry.sendTelemetryLog( + message: message as String, attributes: castedAttributes, config: castedConfig) resolve(nil) } @objc - public func telemetryDebug(message: NSString, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { - DdTelemetry.telemetryDebug(id: "datadog_react_native:\(message)", message: message as String) + public func telemetryDebug( + message: NSString, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock + ) { + DdTelemetry.telemetryDebug( + id: "datadog_react_native:\(message)", message: message as String) resolve(nil) } - + @objc - public func telemetryError(message: NSString, stack: NSString, kind: NSString, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { - DdTelemetry.telemetryError(id: "datadog_react_native:\(String(describing: kind)):\(message)", message: message as String, kind: kind as String, stack: stack as String) + public func telemetryError( + message: NSString, stack: NSString, kind: NSString, resolve: RCTPromiseResolveBlock, + reject: RCTPromiseRejectBlock + ) { + DdTelemetry.telemetryError( + id: "datadog_react_native:\(String(describing: kind)):\(message)", + message: message as String, kind: kind as String, stack: stack as String) resolve(nil) } @objc - public func consumeWebviewEvent(message: NSString, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { - do{ + public func consumeWebviewEvent( + message: NSString, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock + ) { + do { try DatadogSDKWrapper.shared.sendWebviewMessage(body: message) } catch { - DdTelemetry.telemetryError(id: "datadog_react_native:\(error.localizedDescription)", message: "The message being sent was:\(message)" as String, kind: "WebViewEventBridgeError" as String, stack: String(describing: error) as String) + DdTelemetry.telemetryError( + id: "datadog_react_native:\(error.localizedDescription)", + message: "The message being sent was:\(message)" as String, + kind: "WebViewEventBridgeError" as String, + stack: String(describing: error) as String) } resolve(nil) } - + @objc - public func clearAllData(resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { + public func clearAllData(resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) { Datadog.clearAllData() resolve(nil) } - func overrideReactNativeTelemetry(rnConfiguration: DdSdkConfiguration) -> Void { + func overrideReactNativeTelemetry(rnConfiguration: DdSdkConfiguration) { DdTelemetry.overrideTelemetryConfiguration( - initializationType: rnConfiguration.configurationForTelemetry?.initializationType as? String, - reactNativeVersion: rnConfiguration.configurationForTelemetry?.reactNativeVersion as? String, + initializationType: rnConfiguration.configurationForTelemetry?.initializationType + as? String, + reactNativeVersion: rnConfiguration.configurationForTelemetry?.reactNativeVersion + as? String, reactVersion: rnConfiguration.configurationForTelemetry?.reactVersion as? String, trackCrossPlatformLongTasks: rnConfiguration.longTaskThresholdMs != 0, trackErrors: rnConfiguration.configurationForTelemetry?.trackErrors, @@ -208,24 +236,28 @@ public class DdSdkImplementation: NSObject { func startJSRefreshRateMonitoring(sdkConfiguration: DdSdkConfiguration) { if let frameTimeCallback = buildFrameTimeCallback(sdkConfiguration: sdkConfiguration) { // Falling back to mainDispatchQueue if bridge is nil is only useful for tests - self.jsRefreshRateMonitor.startMonitoring(jsQueue: jsDispatchQueue, frameTimeCallback: frameTimeCallback) + self.jsRefreshRateMonitor.startMonitoring( + jsQueue: jsDispatchQueue, frameTimeCallback: frameTimeCallback) } } - func buildFrameTimeCallback(sdkConfiguration: DdSdkConfiguration)-> ((Double) -> ())? { + func buildFrameTimeCallback(sdkConfiguration: DdSdkConfiguration) -> ((Double) -> Void)? { let jsRefreshRateMonitoringEnabled = sdkConfiguration.vitalsUpdateFrequency != nil let jsLongTaskMonitoringEnabled = sdkConfiguration.longTaskThresholdMs != 0 - - if (!jsRefreshRateMonitoringEnabled && !jsLongTaskMonitoringEnabled) { + + if !jsRefreshRateMonitoringEnabled && !jsLongTaskMonitoringEnabled { return nil } func frameTimeCallback(frameTime: Double) { // These checks happen before dispatching because they are quick and less overhead than the dispatch itself. let shouldRecordFrameTime = jsRefreshRateMonitoringEnabled && frameTime > 0 - let shouldRecordLongTask = jsLongTaskMonitoringEnabled && frameTime > sdkConfiguration.longTaskThresholdMs / 1_000 + let shouldRecordLongTask = + jsLongTaskMonitoringEnabled + && frameTime > sdkConfiguration.longTaskThresholdMs / 1_000 guard shouldRecordFrameTime || shouldRecordLongTask, - let rumMonitorInternal = RUMMonitorInternalProvider() else { return } + let rumMonitorInternal = RUMMonitorInternalProvider() + else { return } // Record current timestamp, it may change slightly before event is created on background thread. let now = Date() @@ -235,12 +267,14 @@ public class DdSdkImplementation: NSObject { let normalizedFrameTimeSeconds = DdSdkImplementation.normalizeFrameTimeForDeviceRefreshRate(frameTime) rumMonitorInternal.updatePerformanceMetric(at: now, metric: .jsFrameTimeSeconds, value: normalizedFrameTimeSeconds, attributes: [:]) } - if (shouldRecordLongTask) { - rumMonitorInternal.addLongTask(at: now, duration: frameTime, attributes: ["long_task.target": "javascript"]) + if shouldRecordLongTask { + rumMonitorInternal.addLongTask( + at: now, duration: frameTime, attributes: ["long_task.target": "javascript"] + ) } } } - + return frameTimeCallback } diff --git a/packages/core/ios/Tests/DdRumTests.swift b/packages/core/ios/Tests/DdRumTests.swift index 1102b7b6b..5f6adc016 100644 --- a/packages/core/ios/Tests/DdRumTests.swift +++ b/packages/core/ios/Tests/DdRumTests.swift @@ -253,6 +253,65 @@ internal class DdRumTests: XCTestCase { XCTAssertEqual(mockNativeRUM.receivedAttributes.count, 0) } + func testAddViewAttribute() throws { + let viewAttributeKey = "attributeKey" + let viewAttributes = NSDictionary( + dictionary: [ + "value": 123, + ] + ) + + rum.addViewAttribute(key: viewAttributeKey, value: viewAttributes, resolve: mockResolve, reject: mockReject) + + XCTAssertEqual(mockNativeRUM.calledMethods.count, 1) + XCTAssertEqual(mockNativeRUM.calledMethods.last, .addViewAttribute(key: viewAttributeKey)) + XCTAssertEqual(mockNativeRUM.receivedAttributes.count, 1) + let lastAttributes = try XCTUnwrap(mockNativeRUM.receivedAttributes.last) + XCTAssertEqual(lastAttributes.count, 1) + XCTAssertEqual(lastAttributes["attributeKey"] as? Int64, 123) + } + + func testRemoveViewAttribute() throws { + let viewAttributeKey = "attributeKey" + + rum.removeViewAttribute(key: viewAttributeKey, resolve: mockResolve, reject: mockReject) + + XCTAssertEqual(mockNativeRUM.calledMethods.count, 1) + XCTAssertEqual(mockNativeRUM.calledMethods.last, .removeViewAttribute(key: viewAttributeKey)) + } + + func testAddViewAttributes() throws { + let viewAttributes = NSDictionary( + dictionary: [ + "attribute-1": 123, + "attribute-2": "abc", + "attribute-3": true, + ] + ) + + rum.addViewAttributes(attributes: viewAttributes, resolve: mockResolve, reject: mockReject) + + + XCTAssertEqual(mockNativeRUM.calledMethods.count, 1) + XCTAssertEqual(mockNativeRUM.calledMethods.last, .addViewAttributes()) + XCTAssertEqual(mockNativeRUM.receivedAttributes.count, 1) + let lastAttributes = try XCTUnwrap(mockNativeRUM.receivedAttributes.last) + XCTAssertEqual(lastAttributes.count, 3) + XCTAssertEqual(lastAttributes["attribute-1"] as? Int64, 123) + XCTAssertEqual(lastAttributes["attribute-2"] as? String, "abc") + XCTAssertEqual(lastAttributes["attribute-3"] as? Bool, true) + } + + + func testRemoveViewAttributes() throws { + let viewAttributeKeys = ["attributeKey1", "attributeKey2", "attributeKey3"] + + rum.removeViewAttributes(keys: viewAttributeKeys, resolve: mockResolve, reject: mockReject) + + XCTAssertEqual(mockNativeRUM.calledMethods.count, 1) + XCTAssertEqual(mockNativeRUM.calledMethods.last, .removeViewAttributes(keys: viewAttributeKeys)) + } + func testAddViewLoadingTime() throws { rum.addViewLoadingTime(overwrite: true, resolve: mockResolve, reject: mockReject) diff --git a/packages/core/ios/Tests/MockRUMMonitor.swift b/packages/core/ios/Tests/MockRUMMonitor.swift index 3a882ed47..4a1d0cd92 100644 --- a/packages/core/ios/Tests/MockRUMMonitor.swift +++ b/packages/core/ios/Tests/MockRUMMonitor.swift @@ -10,22 +10,6 @@ @testable import DatadogSDKReactNative internal class MockRUMMonitor: RUMMonitorProtocol { - func addViewAttribute(forKey key: DatadogInternal.AttributeKey, value: any DatadogInternal.AttributeValue) { - // not implemented - } - - func addViewAttributes(_ attributes: [DatadogInternal.AttributeKey : any DatadogInternal.AttributeValue]) { - // not implemented - } - - func removeViewAttribute(forKey key: DatadogInternal.AttributeKey) { - // not implemented - } - - func removeViewAttributes(forKeys keys: [DatadogInternal.AttributeKey]) { - // not implemented - } - func currentSessionID(completion: @escaping (String?) -> Void) { // not implemented } @@ -71,6 +55,10 @@ internal class MockRUMMonitor: RUMMonitorProtocol { case stopUserAction(type: RUMActionType, name: String?) case addUserAction(type: RUMActionType, name: String) case addTiming(name: String) + case addViewAttribute(key: String) + case removeViewAttribute(key: String) + case addViewAttributes(_: Int? = nil) // We need an attribute for the case to be Equatable + case removeViewAttributes(keys: [String]) case addViewLoadingTime(overwrite: Bool) case stopSession(_: Int? = nil) // We need an attribute for the case to be Equatable case addResourceMetrics(resourceKey: String, @@ -131,6 +119,24 @@ internal class MockRUMMonitor: RUMMonitorProtocol { func addTiming(name: String) { calledMethods.append(.addTiming(name: name)) } + func addViewAttribute(forKey key: DatadogInternal.AttributeKey, value: any DatadogInternal.AttributeValue) { + calledMethods.append(.addViewAttribute(key: key)) + receivedAttributes.append([key :value]) + } + + func removeViewAttribute(forKey key: DatadogInternal.AttributeKey) { + calledMethods.append(.removeViewAttribute(key: key)) + } + + func addViewAttributes(_ attributes: [DatadogInternal.AttributeKey : any DatadogInternal.AttributeValue]) { + calledMethods.append(.addViewAttributes()) + receivedAttributes.append(attributes) + } + + func removeViewAttributes(forKeys keys: [DatadogInternal.AttributeKey]) { + calledMethods.append(.removeViewAttributes(keys: keys)) + } + func addViewLoadingTime(overwrite: Bool) { calledMethods.append(.addViewLoadingTime(overwrite: overwrite)) } diff --git a/packages/core/jest/mock.js b/packages/core/jest/mock.js index c49d13f48..0dc9ec138 100644 --- a/packages/core/jest/mock.js +++ b/packages/core/jest/mock.js @@ -119,6 +119,18 @@ module.exports = { addTiming: jest .fn() .mockImplementation(() => new Promise(resolve => resolve())), + addViewAttribute: jest + .fn() + .mockImplementation(() => new Promise(resolve => resolve())), + removeViewAttribute: jest + .fn() + .mockImplementation(() => new Promise(resolve => resolve())), + addViewAttributes: jest + .fn() + .mockImplementation(() => new Promise(resolve => resolve())), + removeViewAttributes: jest + .fn() + .mockImplementation(() => new Promise(resolve => resolve())), addViewLoadingTime: jest .fn() .mockImplementation(() => new Promise(resolve => resolve())), diff --git a/packages/core/src/rum/DdRum.ts b/packages/core/src/rum/DdRum.ts index 908404fc8..e5deb8146 100644 --- a/packages/core/src/rum/DdRum.ts +++ b/packages/core/src/rum/DdRum.ts @@ -9,6 +9,7 @@ import { DdAttributes } from '../DdAttributes'; import { InternalLog } from '../InternalLog'; import { SdkVerbosity } from '../SdkVerbosity'; import type { DdNativeRumType } from '../nativeModulesTypes'; +import type { Attributes } from '../sdk/AttributesSingleton/types'; import { bufferVoidNativeCall } from '../sdk/DatadogProvider/Buffer/bufferNativeCall'; import { DdSdk } from '../sdk/DdSdk'; import { GlobalState } from '../sdk/GlobalState/GlobalState'; @@ -284,6 +285,50 @@ class DdRumWrapper implements DdRumType { return bufferVoidNativeCall(() => this.nativeRum.addTiming(name)); }; + addViewAttribute = (key: string, value: unknown): Promise => { + InternalLog.log( + `Adding view attribute “${key}" with value “${JSON.stringify( + value + )}” to RUM View`, + SdkVerbosity.DEBUG + ); + return bufferVoidNativeCall(() => + this.nativeRum.addViewAttribute(key, { value }) + ); + }; + + removeViewAttribute = (key: string): Promise => { + InternalLog.log( + `Removing view attribute “${key}" from RUM View`, + SdkVerbosity.DEBUG + ); + return bufferVoidNativeCall(() => + this.nativeRum.removeViewAttribute(key) + ); + }; + + addViewAttributes = (attributes: Attributes): Promise => { + InternalLog.log( + `Adding view attributes "${JSON.stringify( + attributes + )}” to RUM View`, + SdkVerbosity.DEBUG + ); + return bufferVoidNativeCall(() => + this.nativeRum.addViewAttributes(attributes) + ); + }; + + removeViewAttributes = (keys: string[]): Promise => { + InternalLog.log( + `Removing view attributes “${keys}" from RUM View`, + SdkVerbosity.DEBUG + ); + return bufferVoidNativeCall(() => + this.nativeRum.removeViewAttributes(keys) + ); + }; + addViewLoadingTime = (overwrite: boolean): Promise => { InternalLog.log( overwrite diff --git a/packages/core/src/rum/__tests__/DdRum.test.ts b/packages/core/src/rum/__tests__/DdRum.test.ts index 7e5fc24de..41b873fe9 100644 --- a/packages/core/src/rum/__tests__/DdRum.test.ts +++ b/packages/core/src/rum/__tests__/DdRum.test.ts @@ -1091,6 +1091,98 @@ describe('DdRum', () => { }); }); + describe('DdRum.addTiming', () => { + it('calls the native SDK when setting a timing', async () => { + // GIVEN + const timingName = 'testTiming'; + + // WHEN + await DdRum.addTiming(timingName); + + // THEN + expect(NativeModules.DdRum.addTiming).toHaveBeenCalledTimes(1); + expect(NativeModules.DdRum.addTiming).toHaveBeenCalledWith( + timingName + ); + }); + }); + + describe('DdRum.addViewAttribute', () => { + it('calls the native SDK when setting a view attribute', async () => { + // GIVEN + const key = 'testAttribute'; + const value = { test: 'attribute' }; + + // WHEN + + await DdRum.addViewAttribute(key, value); + + // THEN + expect( + NativeModules.DdRum.addViewAttribute + ).toHaveBeenCalledTimes(1); + expect( + NativeModules.DdRum.addViewAttribute + ).toHaveBeenCalledWith(key, { value }); + }); + }); + + describe('DdRum.removViewAttribute', () => { + it('calls the native SDK when removing a view attribute', async () => { + // GIVEN + const key = 'testAttribute'; + + // WHEN + await DdRum.removeViewAttribute(key); + + // THEN + expect( + NativeModules.DdRum.removeViewAttribute + ).toHaveBeenCalledTimes(1); + expect( + NativeModules.DdRum.removeViewAttribute + ).toHaveBeenCalledWith(key); + }); + }); + + describe('DdRum.addViewAttributes', () => { + it('calls the native SDK when setting view attributes', async () => { + // GIVEN + const attributes = { + test: 'attribute' + }; + + // WHEN + await DdRum.addViewAttributes(attributes); + + // THEN + expect( + NativeModules.DdRum.addViewAttributes + ).toHaveBeenCalledTimes(1); + expect( + NativeModules.DdRum.addViewAttributes + ).toHaveBeenCalledWith(attributes); + }); + }); + + describe('DdRum.removViewAttributes', () => { + it('calls the native SDK when removing view attributes', async () => { + // GIVEN + const keysToDelete = ['test1', 'test2']; + + // WHEN + await DdRum.removeViewAttributes(keysToDelete); + + // THEN + expect( + NativeModules.DdRum.removeViewAttributes + ).toHaveBeenCalledTimes(1); + expect( + NativeModules.DdRum.removeViewAttributes + ).toHaveBeenCalledWith(keysToDelete); + }); + }); + describe('DdRum.addAction', () => { test('uses given context when context is valid', async () => { const context = { diff --git a/packages/core/src/rum/types.ts b/packages/core/src/rum/types.ts index fc8d07c02..3def7f0e6 100644 --- a/packages/core/src/rum/types.ts +++ b/packages/core/src/rum/types.ts @@ -4,6 +4,7 @@ * Copyright 2016-Present Datadog, Inc. */ +import type { Attributes } from '../sdk/AttributesSingleton/types'; import type { ErrorSource } from '../types'; import type { DatadogTracingContext } from './instrumentation/resourceTracking/distributedTracing/DatadogTracingContext'; @@ -148,6 +149,31 @@ export type DdRumType = { */ addTiming(name: string): Promise; + /** + * Adds a custom attribute to the active RUM View. It will be propagated to all future RUM events associated with the active View. + * @param key: key for this view attribute. + * @param value: value for this attribute. + */ + addViewAttribute(key: string, value: unknown): Promise; + + /** + * Removes an attribute from the active RUM View. + * @param key: key for the attribute to be removed from the view. + */ + removeViewAttribute(key: string): Promise; + + /** + * Adds multiple attributes to the active RUM View. They will be propagated to all future RUM events associated with the active View. + * @param attributes: key/value object containing all attributes to be added to the view. + */ + addViewAttributes(attributes: Attributes): Promise; + + /** + * Removes multiple attributes from the active RUM View. + * @param keys: keys for the attributes to be removed from the view. + */ + removeViewAttributes(keys: string[]): Promise; + /** * Adds the loading time of the view to the active view. * It is calculated as the difference between the current time and the start time of the view. diff --git a/packages/core/src/specs/NativeDdRum.ts b/packages/core/src/specs/NativeDdRum.ts index f6f7b3daa..e31f5b925 100644 --- a/packages/core/src/specs/NativeDdRum.ts +++ b/packages/core/src/specs/NativeDdRum.ts @@ -136,6 +136,31 @@ export interface Spec extends TurboModule { */ addTiming(name: string): Promise; + /** + * Adds a custom attribute to the active RUM View. It will be propagated to all future RUM events associated with the active View. + * @param key: key for this view attribute. + * @param value: value for this attribute. + */ + addViewAttribute(key: string, value: Object): Promise; + + /** + * Removes an attribute from the active RUM View. + * @param key: key for the attribute to be removed from the view. + */ + removeViewAttribute(key: string): Promise; + + /** + * Adds multiple attributes to the active RUM View. They will be propagated to all future RUM events associated with the active View. + * @param attributes: key/value object containing all attributes to be added to the view. + */ + addViewAttributes(attributes: Object): Promise; + + /** + * Removes multiple attributes from the active RUM View. + * @param keys: keys for the attributes to be removed from the view. + */ + removeViewAttributes(keys: string[]): Promise; + /** * Adds the loading time of the view to the active view. * It is calculated as the difference between the current time and the start time of the view. From 89f08acbc10f9d42e7c0dc42a751190d91d7824e Mon Sep 17 00:00:00 2001 From: Sergio Barrio Date: Fri, 22 Aug 2025 15:43:01 +0200 Subject: [PATCH 024/410] Fix FileBasedConfiguration related issues --- example/datadog-configuration.json | 20 ++ example/src/App.tsx | 20 +- .../codepush/src/__tests__/index.test.tsx | 4 +- .../FileBasedConfiguration.ts | 54 ++--- .../__tests__/FileBasedConfiguration.test.ts | 192 ++++++++++-------- .../__fixtures__/malformed-configuration.json | 1 - 6 files changed, 157 insertions(+), 134 deletions(-) create mode 100644 example/datadog-configuration.json diff --git a/example/datadog-configuration.json b/example/datadog-configuration.json new file mode 100644 index 000000000..684e60304 --- /dev/null +++ b/example/datadog-configuration.json @@ -0,0 +1,20 @@ +{ + "$schema": "./node_modules/@datadog/mobile-react-native/datadog-configuration.schema.json", + "configuration": { + "applicationId": "APP_ID", + "batchSize": "SMALL", + "clientToken": "CLIENT_TOKEN", + "env": "ENVIRONMENT", + "longTaskThresholdMs": 1000, + "nativeCrashReportEnabled": true, + "sessionSamplingRate": 100, + "site": "US1", + "telemetrySampleRate": 20, + "trackBackgroundEvents": false, + "trackErrors": true, + "trackInteractions": true, + "trackResources": true, + "trackingConsent": "GRANTED", + "verbosity": "DEBUG" + } +} diff --git a/example/src/App.tsx b/example/src/App.tsx index cefcafffe..c29982a97 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -7,7 +7,7 @@ import AboutScreen from './screens/AboutScreen'; import style from './screens/styles'; import { navigationRef } from './NavigationRoot'; import { DdRumReactNavigationTracking, ViewNamePredicate } from '@datadog/mobile-react-navigation'; -import {DatadogProvider} from '@datadog/mobile-react-native' +import {DatadogProvider, FileBasedConfiguration} from '@datadog/mobile-react-native' import { Route } from "@react-navigation/native"; import { NestedNavigator } from './screens/NestedNavigator/NestedNavigator'; import { getDatadogConfig, onDatadogInitialization } from './ddUtils'; @@ -19,9 +19,25 @@ const viewPredicate: ViewNamePredicate = function customViewNamePredicate(route: return "Custom RN " + trackedName; } +// === Datadog Provider Configuration schemes === + +// 1.- Direct configuration +const configuration = getDatadogConfig(TrackingConsent.GRANTED) + +// 2.- File based configuration from .json +// const configuration = new FileBasedConfiguration(require("../datadog-configuration.json")); + +// 3.- File based configuration from .json and custom mapper setup +// const configuration = new FileBasedConfiguration( { +// configuration: require("../datadog-configuration.json").configuration, +// errorEventMapper: (event) => event, +// resourceEventMapper: (event) => event, +// actionEventMapper: (event) => event}); + + export default function App() { return ( - + { DdRumReactNavigationTracking.startTrackingViews(navigationRef.current, viewPredicate) }}> diff --git a/packages/codepush/src/__tests__/index.test.tsx b/packages/codepush/src/__tests__/index.test.tsx index 5c94c16ef..63e1af7d6 100644 --- a/packages/codepush/src/__tests__/index.test.tsx +++ b/packages/codepush/src/__tests__/index.test.tsx @@ -279,7 +279,7 @@ describe('AppCenter Codepush integration', () => { }; const configuration = new FileBasedConfiguration({ - configuration: { configuration: autoInstrumentationConfig } + configuration: autoInstrumentationConfig }); render(); @@ -346,7 +346,7 @@ describe('AppCenter Codepush integration', () => { }; const configuration = new FileBasedConfiguration({ - configuration: { configuration: autoInstrumentationConfig } + configuration: autoInstrumentationConfig }); render(); diff --git a/packages/core/src/sdk/FileBasedConfiguration/FileBasedConfiguration.ts b/packages/core/src/sdk/FileBasedConfiguration/FileBasedConfiguration.ts index ddd1943aa..3fc69f1f3 100644 --- a/packages/core/src/sdk/FileBasedConfiguration/FileBasedConfiguration.ts +++ b/packages/core/src/sdk/FileBasedConfiguration/FileBasedConfiguration.ts @@ -56,44 +56,7 @@ export class FileBasedConfiguration extends DatadogProviderConfiguration { const resolveJSONConfiguration = ( userSpecifiedConfiguration: unknown ): Record => { - if ( - userSpecifiedConfiguration === undefined || - userSpecifiedConfiguration === null - ) { - try { - // This corresponds to a file located at the root of a RN project. - // /!\ We have to write the require this way as dynamic requires are not supported by Hermes. - // eslint-disable-next-line global-require, @typescript-eslint/no-var-requires - const jsonContent = require('../../../../../../datadog-configuration.json'); - - if ( - typeof jsonContent !== 'object' || - !jsonContent['configuration'] - ) { - console.error(`Failed to parse the Datadog configuration file located at the root of the project. -Your configuration must validate the node_modules/@datadog/mobile-react-native/datadog-configuration.schema.json JSON schema. -You can use VSCode to check your configuration by adding the following line to your JSON file: -{ - "$schema": "./node_modules/@datadog/mobile-react-native/datadog-configuration.schema.json", -}`); - - return {}; - } - - return jsonContent.configuration as Record; - } catch (error) { - console.error(`Failed to read Datadog configuration file at the root of the project. -If you don't have a datadog-configuration.json file at the same level as your node_modules directory,\ -please use the following syntax:\n -new FileBasedConfiguration({configuration: require('./file/to/configuration-file.json')}) -`); - return {}; - } - } - if ( - typeof userSpecifiedConfiguration !== 'object' || - !(userSpecifiedConfiguration as any)['configuration'] - ) { + if (typeof userSpecifiedConfiguration !== 'object') { console.error(`Failed to parse the Datadog configuration file you provided. Your configuration must validate the node_modules/@datadog/mobile-react-native/datadog-configuration.schema.json JSON schema. You can use VSCode to check your configuration by adding the following line to your JSON file: @@ -104,10 +67,7 @@ You can use VSCode to check your configuration by adding the following line to y return {}; } - return (userSpecifiedConfiguration as any)['configuration'] as Record< - string, - any - >; + return (userSpecifiedConfiguration as any) as Record; }; export const getJSONConfiguration = ( @@ -130,6 +90,16 @@ export const getJSONConfiguration = ( } => { const configuration = resolveJSONConfiguration(userSpecifiedConfiguration); + if ( + configuration.clientToken === undefined || + configuration.env === undefined || + configuration.applicationId === undefined + ) { + console.warn( + 'DATADOG: Warning: Malformed json configuration file - clientToken, applicationId and env are mandatory properties.' + ); + } + return { clientToken: configuration.clientToken, env: configuration.env, diff --git a/packages/core/src/sdk/FileBasedConfiguration/__tests__/FileBasedConfiguration.test.ts b/packages/core/src/sdk/FileBasedConfiguration/__tests__/FileBasedConfiguration.test.ts index 716243e86..6d3ee2e44 100644 --- a/packages/core/src/sdk/FileBasedConfiguration/__tests__/FileBasedConfiguration.test.ts +++ b/packages/core/src/sdk/FileBasedConfiguration/__tests__/FileBasedConfiguration.test.ts @@ -16,35 +16,99 @@ import malformedConfiguration from './__fixtures__/malformed-configuration.json' describe('FileBasedConfiguration', () => { describe('with user-specified configuration', () => { + it('resolves configuration fields', () => { + const configuration = new FileBasedConfiguration( + configurationAllFields + ); + + expect(configuration).toMatchInlineSnapshot(` + FileBasedConfiguration { + "actionEventMapper": null, + "actionNameAttribute": "action-name-attr", + "additionalConfiguration": {}, + "applicationId": "fake-app-id", + "batchProcessingLevel": "MEDIUM", + "batchSize": "MEDIUM", + "bundleLogsWithRum": true, + "bundleLogsWithTraces": true, + "clientToken": "fake-client-token", + "customEndpoints": {}, + "env": "fake-env", + "errorEventMapper": null, + "firstPartyHosts": [ + { + "match": "example.com", + "propagatorTypes": [ + "b3multi", + "tracecontext", + ], + }, + ], + "initializationMode": "SYNC", + "logEventMapper": null, + "longTaskThresholdMs": 44, + "nativeCrashReportEnabled": false, + "nativeInteractionTracking": false, + "nativeLongTaskThresholdMs": 200, + "nativeViewTracking": false, + "proxyConfig": undefined, + "resourceEventMapper": null, + "resourceTracingSamplingRate": 33, + "serviceName": undefined, + "sessionSamplingRate": 100, + "site": "US5", + "telemetrySampleRate": 20, + "trackBackgroundEvents": false, + "trackErrors": true, + "trackFrustrations": true, + "trackInteractions": true, + "trackResources": true, + "trackWatchdogTerminations": false, + "trackingConsent": "not_granted", + "uploadFrequency": "AVERAGE", + "useAccessibilityLabel": false, + "verbosity": "warn", + "vitalsUpdateFrequency": "AVERAGE", + } + `); + }); + + it('prints a warning message when the configuration file cannot be parsed correctly', () => { + const warnSpy = jest.spyOn(console, 'warn'); + getJSONConfiguration(malformedConfiguration); + + expect(warnSpy).toHaveBeenCalledWith( + 'DATADOG: Warning: Malformed json configuration file - clientToken, applicationId and env are mandatory properties.' + ); + }); + it('resolves all properties from a given file path', () => { const config = new FileBasedConfiguration({ configuration: { - configuration: { - applicationId: 'fake-app-id', - env: 'fake-env', - clientToken: 'fake-client-token', - trackInteractions: true, - trackResources: true, - trackErrors: true, - trackingConsent: 'NOT_GRANTED', - longTaskThresholdMs: 44, - site: 'US5', - verbosity: 'WARN', - actionNameAttribute: 'action-name-attr', - useAccessibilityLabel: false, - resourceTracingSamplingRate: 33, - firstPartyHosts: [ - { - match: 'example.com', - propagatorTypes: [ - 'B3MULTI', - 'TRACECONTEXT', - 'B3', - 'DATADOG' - ] - } - ] - } + applicationId: 'fake-app-id', + env: 'fake-env', + clientToken: 'fake-client-token', + trackInteractions: true, + trackResources: true, + trackErrors: true, + trackingConsent: 'NOT_GRANTED', + longTaskThresholdMs: 44, + site: 'US5', + verbosity: 'WARN', + actionNameAttribute: 'action-name-attr', + useAccessibilityLabel: false, + resourceTracingSamplingRate: 33, + firstPartyHosts: [ + { + match: 'example.com', + propagatorTypes: [ + 'B3MULTI', + 'TRACECONTEXT', + 'B3', + 'DATADOG' + ] + } + ] } }); expect(config).toMatchInlineSnapshot(` @@ -103,11 +167,9 @@ describe('FileBasedConfiguration', () => { it('applies default values to configuration from a given file path', () => { const config = new FileBasedConfiguration({ configuration: { - configuration: { - applicationId: 'fake-app-id', - env: 'fake-env', - clientToken: 'fake-client-token' - } + applicationId: 'fake-app-id', + env: 'fake-env', + clientToken: 'fake-client-token' } }); expect(config).toMatchInlineSnapshot(` @@ -159,11 +221,9 @@ describe('FileBasedConfiguration', () => { const resourceEventMapper = () => null; const config = new FileBasedConfiguration({ configuration: { - configuration: { - applicationId: 'fake-app-id', - env: 'fake-env', - clientToken: 'fake-client-token' - } + applicationId: 'fake-app-id', + env: 'fake-env', + clientToken: 'fake-client-token' }, actionEventMapper, errorEventMapper, @@ -188,62 +248,20 @@ describe('FileBasedConfiguration', () => { it('prints a warning message when the first party hosts contain unknown propagator types', () => { const config = new FileBasedConfiguration({ configuration: { - configuration: { - applicationId: 'fake-app-id', - env: 'fake-env', - clientToken: 'fake-client-token', - firstPartyHosts: [ - { - match: 'example.com', - propagatorTypes: ['UNKNOWN'] - } - ] - } + applicationId: 'fake-app-id', + env: 'fake-env', + clientToken: 'fake-client-token', + firstPartyHosts: [ + { + match: 'example.com', + propagatorTypes: ['UNKNOWN'] + } + ] } }); expect(config.firstPartyHosts).toHaveLength(0); }); }); - describe('with resolved file configuration', () => { - it('resolves configuration fields', () => { - const configuration = getJSONConfiguration(configurationAllFields); - - expect(configuration).toMatchInlineSnapshot(` - { - "actionNameAttribute": "action-name-attr", - "applicationId": "fake-app-id", - "clientToken": "fake-client-token", - "env": "fake-env", - "firstPartyHosts": [ - { - "match": "example.com", - "propagatorTypes": [ - "b3multi", - "tracecontext", - ], - }, - ], - "longTaskThresholdMs": 44, - "resourceTracingSamplingRate": 33, - "site": "US5", - "trackErrors": true, - "trackInteractions": true, - "trackResources": true, - "trackingConsent": "not_granted", - "useAccessibilityLabel": false, - "verbosity": "warn", - } - `); - }); - it('prints a warning message when the configuration file is not found', () => { - expect(() => getJSONConfiguration(undefined)).not.toThrow(); - }); - it('prints a warning message when the configuration file cannot be parsed correctly', () => { - expect(() => - getJSONConfiguration(malformedConfiguration) - ).not.toThrow(); - }); - }); describe('formatPropagatorType', () => { it('formats all propagatorTypes correctly', () => { diff --git a/packages/core/src/sdk/FileBasedConfiguration/__tests__/__fixtures__/malformed-configuration.json b/packages/core/src/sdk/FileBasedConfiguration/__tests__/__fixtures__/malformed-configuration.json index 28423084d..0e1b26639 100644 --- a/packages/core/src/sdk/FileBasedConfiguration/__tests__/__fixtures__/malformed-configuration.json +++ b/packages/core/src/sdk/FileBasedConfiguration/__tests__/__fixtures__/malformed-configuration.json @@ -1,5 +1,4 @@ { "clientToken": "clientToken", - "env": "env", "applicationId": "applicationId" } From 6414310e992409c231263be0ba4f181ca8ecb5d8 Mon Sep 17 00:00:00 2001 From: "Xavier F. Gouchet" Date: Mon, 7 Apr 2025 14:55:53 +0200 Subject: [PATCH 025/410] RUM-9023 use session id to sample network traces --- .../distributedTracing/distributedTracing.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/core/src/rum/instrumentation/resourceTracking/distributedTracing/distributedTracing.tsx b/packages/core/src/rum/instrumentation/resourceTracking/distributedTracing/distributedTracing.tsx index e529f3cd1..9c4fcbff7 100644 --- a/packages/core/src/rum/instrumentation/resourceTracking/distributedTracing/distributedTracing.tsx +++ b/packages/core/src/rum/instrumentation/resourceTracking/distributedTracing/distributedTracing.tsx @@ -77,7 +77,12 @@ export const generateTracingAttributesWithSampling = ( } const traceId = TracingIdentifier.createTraceId(); - const hash = Number(traceId.id.multiply(knuthFactor).remainder(twoPow64)); + // for a UUID with value aaaaaaaa-bbbb-Mccc-Nddd-1234567890ab + // we use as the input id the last part : 0x1234567890ab + const baseId = rumSessionId + ? BigInt(rumSessionId.split('-')[4], 16) + : traceId.id; + const hash = Number(baseId.multiply(knuthFactor).remainder(twoPow64)); const threshold = (tracingSamplingRate / 100) * Number(twoPow64); const isSampled = hash <= threshold; From b82a2bc423e874d20de7e46f788977991efa2fbb Mon Sep 17 00:00:00 2001 From: "Xavier F. Gouchet" Date: Mon, 28 Apr 2025 13:55:23 +0200 Subject: [PATCH 026/410] RUM-7747 update default tracing sampling rate --- packages/core/ios/Sources/RNDdSdkConfiguration.swift | 2 +- packages/core/src/DdSdkReactNativeConfiguration.tsx | 2 +- .../src/__tests__/DdSdkReactNativeConfiguration.test.ts | 6 +++--- .../sdk/DatadogProvider/__tests__/initialization.test.tsx | 2 +- .../__tests__/FileBasedConfiguration.test.ts | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/core/ios/Sources/RNDdSdkConfiguration.swift b/packages/core/ios/Sources/RNDdSdkConfiguration.swift index a765e9759..66437c481 100644 --- a/packages/core/ios/Sources/RNDdSdkConfiguration.swift +++ b/packages/core/ios/Sources/RNDdSdkConfiguration.swift @@ -194,7 +194,7 @@ extension NSArray { internal struct DefaultConfiguration { static let nativeCrashReportEnabled = false static let sessionSamplingRate = 100.0 - static let resourceTracingSamplingRate = 20.0 + static let resourceTracingSamplingRate = 100.0 static let longTaskThresholdMs = 0.0 static let nativeLongTaskThresholdMs = 200.0 static let nativeViewTracking = false diff --git a/packages/core/src/DdSdkReactNativeConfiguration.tsx b/packages/core/src/DdSdkReactNativeConfiguration.tsx index 158d258eb..9be08fa28 100644 --- a/packages/core/src/DdSdkReactNativeConfiguration.tsx +++ b/packages/core/src/DdSdkReactNativeConfiguration.tsx @@ -109,7 +109,7 @@ export const formatFirstPartyHosts = ( export const DEFAULTS = { nativeCrashReportEnabled: false, sessionSamplingRate: 100.0, - resourceTracingSamplingRate: 20.0, + resourceTracingSamplingRate: 100.0, site: 'US1', longTaskThresholdMs: 0, nativeLongTaskThresholdMs: 200, diff --git a/packages/core/src/__tests__/DdSdkReactNativeConfiguration.test.ts b/packages/core/src/__tests__/DdSdkReactNativeConfiguration.test.ts index 6d9d081af..b1522ef7f 100644 --- a/packages/core/src/__tests__/DdSdkReactNativeConfiguration.test.ts +++ b/packages/core/src/__tests__/DdSdkReactNativeConfiguration.test.ts @@ -51,7 +51,7 @@ describe('DdSdkReactNativeConfiguration', () => { "nativeViewTracking": false, "proxyConfig": undefined, "resourceEventMapper": null, - "resourceTracingSamplingRate": 20, + "resourceTracingSamplingRate": 100, "serviceName": undefined, "sessionSamplingRate": 100, "site": "US1", @@ -79,7 +79,7 @@ describe('DdSdkReactNativeConfiguration', () => { trackInteractions: true, trackResources: true, firstPartyHosts: ['api.com'], - resourceTracingSamplingRate: 100, + resourceTracingSamplingRate: 80, logEventMapper: event => event, errorEventMapper: event => event, resourceEventMapper: event => event, @@ -161,7 +161,7 @@ describe('DdSdkReactNativeConfiguration', () => { "type": "https", }, "resourceEventMapper": [Function], - "resourceTracingSamplingRate": 100, + "resourceTracingSamplingRate": 80, "serviceName": "com.test.app", "sessionSamplingRate": 80, "site": "EU", diff --git a/packages/core/src/sdk/DatadogProvider/__tests__/initialization.test.tsx b/packages/core/src/sdk/DatadogProvider/__tests__/initialization.test.tsx index 590c34b98..c654cd24b 100644 --- a/packages/core/src/sdk/DatadogProvider/__tests__/initialization.test.tsx +++ b/packages/core/src/sdk/DatadogProvider/__tests__/initialization.test.tsx @@ -93,7 +93,7 @@ describe('DatadogProvider', () => { "nativeLongTaskThresholdMs": 200, "nativeViewTracking": false, "proxyConfig": undefined, - "resourceTracingSamplingRate": 20, + "resourceTracingSamplingRate": 100, "sampleRate": 100, "serviceName": undefined, "site": "US1", diff --git a/packages/core/src/sdk/FileBasedConfiguration/__tests__/FileBasedConfiguration.test.ts b/packages/core/src/sdk/FileBasedConfiguration/__tests__/FileBasedConfiguration.test.ts index 523cf67cc..716243e86 100644 --- a/packages/core/src/sdk/FileBasedConfiguration/__tests__/FileBasedConfiguration.test.ts +++ b/packages/core/src/sdk/FileBasedConfiguration/__tests__/FileBasedConfiguration.test.ts @@ -134,7 +134,7 @@ describe('FileBasedConfiguration', () => { "nativeViewTracking": false, "proxyConfig": undefined, "resourceEventMapper": null, - "resourceTracingSamplingRate": 20, + "resourceTracingSamplingRate": 100, "serviceName": undefined, "sessionSamplingRate": 100, "site": "US1", From e1325c2a6346d82b397890f5fe711e4b7d9e4377 Mon Sep 17 00:00:00 2001 From: Sergio Barrio Date: Tue, 2 Sep 2025 10:49:34 +0200 Subject: [PATCH 027/410] Remove fatal errors from logs --- .../DdRumErrorTracking.test.tsx | 202 +----------------- .../core/src/logs/__tests__/DdLogs.test.ts | 4 +- .../instrumentation/DdRumErrorTracking.tsx | 41 +--- 3 files changed, 14 insertions(+), 233 deletions(-) diff --git a/packages/core/src/__tests__/rum/instrumentation/DdRumErrorTracking.test.tsx b/packages/core/src/__tests__/rum/instrumentation/DdRumErrorTracking.test.tsx index 7f66bb58e..fc34de5b0 100644 --- a/packages/core/src/__tests__/rum/instrumentation/DdRumErrorTracking.test.tsx +++ b/packages/core/src/__tests__/rum/instrumentation/DdRumErrorTracking.test.tsx @@ -6,17 +6,13 @@ import { NativeModules } from 'react-native'; -import type { - DdNativeLogsType, - DdNativeRumType -} from '../../../nativeModulesTypes'; +import type { DdNativeRumType } from '../../../nativeModulesTypes'; import { DdRumErrorTracking } from '../../../rum/instrumentation/DdRumErrorTracking'; import { BufferSingleton } from '../../../sdk/DatadogProvider/Buffer/BufferSingleton'; jest.mock('../../../utils/jsUtils'); const DdRum = NativeModules.DdRum as DdNativeRumType; -const DdLogs = NativeModules.DdLogs as DdNativeLogsType; let baseErrorHandlerCalled = false; const baseErrorHandler = (error: any, isFatal?: boolean) => { @@ -77,19 +73,6 @@ it('M intercept and send a RUM event W onGlobalError() {no message}', async () = '' ); expect(baseErrorHandlerCalled).toStrictEqual(true); - expect(DdLogs.errorWithError).toHaveBeenCalledTimes(1); - expect(DdLogs.errorWithError).toHaveBeenCalledWith( - '[object Object]', - 'Error', - '[object Object]', - 'doSomething() at ./path/to/file.js:67:3', - { - '_dd.error.raw': error, - '_dd.error.is_crash': is_fatal, - '_dd.error.source_type': 'react-native', - '_dd.error_log.is_crash': true - } - ); }); it('M intercept and send a RUM event W onGlobalError() {empty stack trace}', async () => { @@ -119,19 +102,6 @@ it('M intercept and send a RUM event W onGlobalError() {empty stack trace}', asy '' ); expect(baseErrorHandlerCalled).toStrictEqual(true); - expect(DdLogs.errorWithError).toHaveBeenCalledTimes(1); - expect(DdLogs.errorWithError).toHaveBeenCalledWith( - 'Something bad happened', - 'Error', - 'Something bad happened', - '', - { - '_dd.error.raw': error, - '_dd.error.is_crash': is_fatal, - '_dd.error.source_type': 'react-native', - '_dd.error_log.is_crash': true - } - ); }); it('M intercept and send a RUM event W onGlobalError() {Error object}', async () => { @@ -162,19 +132,6 @@ it('M intercept and send a RUM event W onGlobalError() {Error object}', async () '/packages/core/src/__tests__/rum/instrumentation/DdRumErrorTracking.test.tsx' ); expect(baseErrorHandlerCalled).toStrictEqual(true); - expect(DdLogs.errorWithError).toHaveBeenCalledTimes(1); - expect(DdLogs.errorWithError).toHaveBeenCalledWith( - 'Something bad happened', - 'Error', - 'Something bad happened', - expect.stringContaining('Error: Something bad happened'), - { - '_dd.error.raw': error, - '_dd.error.is_crash': is_fatal, - '_dd.error.source_type': 'react-native', - '_dd.error_log.is_crash': true - } - ); }); it('M intercept and send a RUM event W onGlobalError() {CustomError object}', async () => { @@ -209,19 +166,6 @@ it('M intercept and send a RUM event W onGlobalError() {CustomError object}', as '/packages/core/src/__tests__/rum/instrumentation/DdRumErrorTracking.test.tsx' ); expect(baseErrorHandlerCalled).toStrictEqual(true); - expect(DdLogs.errorWithError).toHaveBeenCalledTimes(1); - expect(DdLogs.errorWithError).toHaveBeenCalledWith( - 'Something bad happened', - 'CustomError', - 'Something bad happened', - expect.stringContaining('Error: Something bad happened'), - { - '_dd.error.raw': error, - '_dd.error.is_crash': is_fatal, - '_dd.error.source_type': 'react-native', - '_dd.error_log.is_crash': true - } - ); }); it('M intercept and send a RUM event W onGlobalError() {with source file info}', async () => { @@ -254,19 +198,6 @@ it('M intercept and send a RUM event W onGlobalError() {with source file info}', '' ); expect(baseErrorHandlerCalled).toStrictEqual(true); - expect(DdLogs.errorWithError).toHaveBeenCalledTimes(1); - expect(DdLogs.errorWithError).toHaveBeenCalledWith( - 'Something bad happened', - 'Error', - 'Something bad happened', - 'at ./path/to/file.js:1038:57', - { - '_dd.error.raw': error, - '_dd.error.is_crash': is_fatal, - '_dd.error.source_type': 'react-native', - '_dd.error_log.is_crash': true - } - ); }); it('M intercept and send a RUM event W onGlobalError() {with component stack}', async () => { @@ -301,19 +232,6 @@ it('M intercept and send a RUM event W onGlobalError() {with component stack}', '' ); expect(baseErrorHandlerCalled).toStrictEqual(true); - expect(DdLogs.errorWithError).toHaveBeenCalledTimes(1); - expect(DdLogs.errorWithError).toHaveBeenCalledWith( - 'Something bad happened', - 'Error', - 'Something bad happened', - 'doSomething() at ./path/to/file.js:67:3,nestedCall() at ./path/to/file.js:1064:9,root() at ./path/to/index.js:10:1', - { - '_dd.error.raw': error, - '_dd.error.is_crash': is_fatal, - '_dd.error.source_type': 'react-native', - '_dd.error_log.is_crash': true - } - ); }); it('M intercept and send a RUM event W onGlobalError() {with stack}', async () => { @@ -348,19 +266,6 @@ it('M intercept and send a RUM event W onGlobalError() {with stack}', async () = '' ); expect(baseErrorHandlerCalled).toStrictEqual(true); - expect(DdLogs.errorWithError).toHaveBeenCalledTimes(1); - expect(DdLogs.errorWithError).toHaveBeenCalledWith( - 'Something bad happened', - 'Error', - 'Something bad happened', - 'doSomething() at ./path/to/file.js:67:3,nestedCall() at ./path/to/file.js:1064:9,root() at ./path/to/index.js:10:1', - { - '_dd.error.raw': error, - '_dd.error.is_crash': is_fatal, - '_dd.error.source_type': 'react-native', - '_dd.error_log.is_crash': true - } - ); }); it('M intercept and send a RUM event W onGlobalError() {with stacktrace}', async () => { @@ -396,19 +301,6 @@ it('M intercept and send a RUM event W onGlobalError() {with stacktrace}', async ); expect(baseErrorHandlerCalled).toStrictEqual(true); expect(baseErrorHandlerCalled).toStrictEqual(true); - expect(DdLogs.errorWithError).toHaveBeenCalledTimes(1); - expect(DdLogs.errorWithError).toHaveBeenCalledWith( - 'Something bad happened', - 'Error', - 'Something bad happened', - 'doSomething() at ./path/to/file.js:67:3,nestedCall() at ./path/to/file.js:1064:9,root() at ./path/to/index.js:10:1', - { - '_dd.error.raw': error, - '_dd.error.is_crash': is_fatal, - '_dd.error.source_type': 'react-native', - '_dd.error_log.is_crash': true - } - ); }); it('M not report error in console handler W onGlobalError() {with console reporting handler}', async () => { @@ -450,19 +342,6 @@ it('M not report error in console handler W onGlobalError() {with console report expect(consoleReportingErrorHandler).toBeCalledTimes(1); expect(baseConsoleErrorCalled).toStrictEqual(false); expect(baseErrorHandlerCalled).toStrictEqual(true); - expect(DdLogs.errorWithError).toHaveBeenCalledTimes(1); - expect(DdLogs.errorWithError).toHaveBeenCalledWith( - 'Something bad happened', - 'Error', - 'Something bad happened', - 'doSomething() at ./path/to/file.js:67:3,nestedCall() at ./path/to/file.js:1064:9,root() at ./path/to/index.js:10:1', - { - '_dd.error.raw': error, - '_dd.error.is_crash': is_fatal, - '_dd.error.source_type': 'react-native', - '_dd.error_log.is_crash': true - } - ); }); it('M intercept and send a RUM event W onConsole() {Error with source file info}', async () => { @@ -493,17 +372,6 @@ it('M intercept and send a RUM event W onConsole() {Error with source file info} '' ); expect(baseConsoleErrorCalled).toStrictEqual(true); - expect(DdLogs.errorWithError).toHaveBeenCalledTimes(1); - expect(DdLogs.errorWithError).toHaveBeenCalledWith( - 'Oops I did it again! Something bad happened', - 'Error', - 'Oops I did it again! Something bad happened', - 'at ./path/to/file.js:1038:57', - { - '_dd.error.source_type': 'react-native', - '_dd.error_log.is_crash': true - } - ); }); it('M intercept and send a RUM event W onConsole() {Error with component stack}', async () => { @@ -536,17 +404,6 @@ it('M intercept and send a RUM event W onConsole() {Error with component stack}' '' ); expect(baseConsoleErrorCalled).toStrictEqual(true); - expect(DdLogs.errorWithError).toHaveBeenCalledTimes(1); - expect(DdLogs.errorWithError).toHaveBeenCalledWith( - 'Oops I did it again! Something bad happened', - 'Error', - 'Oops I did it again! Something bad happened', - 'doSomething() at ./path/to/file.js:67:3,nestedCall() at ./path/to/file.js:1064:9,root() at ./path/to/index.js:10:1', - { - '_dd.error.source_type': 'react-native', - '_dd.error_log.is_crash': true - } - ); }); it('M intercept and send a RUM event W onConsole() {message only}', async () => { @@ -571,17 +428,6 @@ it('M intercept and send a RUM event W onConsole() {message only}', async () => '' ); expect(baseConsoleErrorCalled).toStrictEqual(true); - expect(DdLogs.errorWithError).toHaveBeenCalledTimes(1); - expect(DdLogs.errorWithError).toHaveBeenCalledWith( - 'Something bad happened', - 'Error', - 'Something bad happened', - '', - { - '_dd.error.source_type': 'react-native', - '_dd.error_log.is_crash': true - } - ); }); it('M intercept and send a RUM event W onConsole() {Error with source file and name}', async () => { @@ -613,17 +459,6 @@ it('M intercept and send a RUM event W onConsole() {Error with source file and n '' ); expect(baseConsoleErrorCalled).toStrictEqual(true); - expect(DdLogs.errorWithError).toHaveBeenCalledTimes(1); - expect(DdLogs.errorWithError).toHaveBeenCalledWith( - 'Oops I did it again! Something bad happened', - 'CustomConsoleError', - 'Oops I did it again! Something bad happened', - 'at ./path/to/file.js:1038:57', - { - '_dd.error.source_type': 'react-native', - '_dd.error_log.is_crash': true - } - ); }); describe.each([ @@ -661,17 +496,6 @@ describe.each([ '' ); expect(baseConsoleErrorCalled).toStrictEqual(true); - expect(DdLogs.errorWithError).toHaveBeenCalledTimes(1); - expect(DdLogs.errorWithError).toHaveBeenCalledWith( - errorMessage, - 'Error', - errorMessage, - '', - { - '_dd.error.source_type': 'react-native', - '_dd.error_log.is_crash': true - } - ); }); }); @@ -704,19 +528,6 @@ it('M intercept and send a RUM event W on error() {called from RNErrorHandler}', '/packages/core/src/__tests__/rum/instrumentation/DdRumErrorTracking.test.tsx' ); expect(baseErrorHandlerCalled).toStrictEqual(true); - expect(DdLogs.errorWithError).toHaveBeenCalledTimes(1); - expect(DdLogs.errorWithError).toHaveBeenCalledWith( - 'Something bad happened', - 'Error', - 'Something bad happened', - expect.stringContaining('Error: Something bad happened'), - { - '_dd.error.raw': error, - '_dd.error.is_crash': is_fatal, - '_dd.error.source_type': 'react-native', - '_dd.error_log.is_crash': true - } - ); }); it('M intercept and send a RUM event W onConsole() {called from RNErrorHandler}', async () => { @@ -742,17 +553,6 @@ it('M intercept and send a RUM event W onConsole() {called from RNErrorHandler}' '' ); expect(baseConsoleErrorCalled).toStrictEqual(true); - expect(DdLogs.errorWithError).toHaveBeenCalledTimes(1); - expect(DdLogs.errorWithError).toHaveBeenCalledWith( - 'Oops I did it again!', - 'Error', - 'Oops I did it again!', - '', - { - '_dd.error.source_type': 'react-native', - '_dd.error_log.is_crash': true - } - ); }); /** diff --git a/packages/core/src/logs/__tests__/DdLogs.test.ts b/packages/core/src/logs/__tests__/DdLogs.test.ts index f99280bfe..6a530ec36 100644 --- a/packages/core/src/logs/__tests__/DdLogs.test.ts +++ b/packages/core/src/logs/__tests__/DdLogs.test.ts @@ -225,7 +225,7 @@ describe('DdLogs', () => { console.error('console-error-message'); expect(NativeModules.DdLogs.error).not.toHaveBeenCalled(); expect(InternalLog.log).toHaveBeenCalledWith( - 'error log dropped by log mapper: "console-error-message"', + 'Adding RUM Error “console-error-message”', 'debug' ); @@ -278,7 +278,7 @@ describe('DdLogs', () => { console.error('console-error-message'); expect(NativeModules.DdLogs.error).not.toHaveBeenCalled(); expect(InternalLog.log).toHaveBeenCalledWith( - 'Tracking error log "console-error-message"', + 'Adding RUM Error “console-error-message”', 'debug' ); }); diff --git a/packages/core/src/rum/instrumentation/DdRumErrorTracking.tsx b/packages/core/src/rum/instrumentation/DdRumErrorTracking.tsx index 04b01290c..5a45bdc65 100644 --- a/packages/core/src/rum/instrumentation/DdRumErrorTracking.tsx +++ b/packages/core/src/rum/instrumentation/DdRumErrorTracking.tsx @@ -8,7 +8,6 @@ import type { ErrorHandlerCallback } from 'react-native'; import { InternalLog } from '../../InternalLog'; import { SdkVerbosity } from '../../SdkVerbosity'; -import { DdLogs } from '../../logs/DdLogs'; import { getErrorMessage, getErrorStackTrace, @@ -71,8 +70,7 @@ export class DdRumErrorTracking { static onGlobalError = (error: any, isFatal?: boolean): void => { const message = getErrorMessage(error); const stacktrace = getErrorStackTrace(error); - const errorName = getErrorName(error); - this.reportError(message, ErrorSource.SOURCE, stacktrace, errorName, { + this.reportError(message, ErrorSource.SOURCE, stacktrace, { '_dd.error.is_crash': isFatal, '_dd.error.raw': error }).then(async () => { @@ -131,39 +129,22 @@ export class DdRumErrorTracking { }) .join(' '); - this.reportError(message, ErrorSource.CONSOLE, stack, errorName).then( - () => { - DdRumErrorTracking.defaultConsoleError.apply(console, params); - } - ); + this.reportError(message, ErrorSource.CONSOLE, stack).then(() => { + DdRumErrorTracking.defaultConsoleError.apply(console, params); + }); }; private static reportError = ( message: string, source: ErrorSource, stacktrace: string, - errorName: string, context: object = {} - ): Promise<[void, void]> => { - return Promise.all([ - DdRum.addError( - message, - source, - stacktrace, - getErrorContext(context) - ), - DdLogs.error( - message, - errorName, - message, - stacktrace, - { - ...context, - '_dd.error_log.is_crash': true - }, - undefined, - source - ) - ]); + ): Promise => { + return DdRum.addError( + message, + source, + stacktrace, + getErrorContext(context) + ); }; } From 2d5ce9716af59e488121f316c0f9292b67ae67e7 Mon Sep 17 00:00:00 2001 From: Sergio Barrio Date: Tue, 2 Sep 2025 17:06:28 +0200 Subject: [PATCH 028/410] Improve module wrapper singleton creation --- packages/core/src/logs/DdLogs.ts | 4 +- packages/core/src/rum/DdRum.ts | 5 +- packages/core/src/trace/DdTrace.ts | 10 ++- .../utils/__tests__/singletonUtils.test.ts | 75 +++++++++++++++++++ packages/core/src/utils/singletonUtils.ts | 12 +++ 5 files changed, 101 insertions(+), 5 deletions(-) create mode 100644 packages/core/src/utils/__tests__/singletonUtils.test.ts create mode 100644 packages/core/src/utils/singletonUtils.ts diff --git a/packages/core/src/logs/DdLogs.ts b/packages/core/src/logs/DdLogs.ts index 9ac35fa73..e00e4fc0d 100644 --- a/packages/core/src/logs/DdLogs.ts +++ b/packages/core/src/logs/DdLogs.ts @@ -10,6 +10,7 @@ import type { DdNativeLogsType } from '../nativeModulesTypes'; import { DdAttributes } from '../rum/DdAttributes'; import type { ErrorSource } from '../rum/types'; import { validateContext } from '../utils/argsUtils'; +import { getGlobalInstance } from '../utils/singletonUtils'; import { generateEventMapper } from './eventMapper'; import type { @@ -21,6 +22,7 @@ import type { RawLogWithError } from './types'; +const LOGS_MODULE = 'com.datadog.reactnative.logs'; const SDK_NOT_INITIALIZED_MESSAGE = 'DD_INTERNAL_LOG_SENT_BEFORE_SDK_INIT'; const generateEmptyPromise = () => new Promise(resolve => resolve()); @@ -240,4 +242,4 @@ class DdLogsWrapper implements DdLogsType { } } -export const DdLogs = new DdLogsWrapper(); +export const DdLogs = getGlobalInstance(LOGS_MODULE, () => new DdLogsWrapper()); diff --git a/packages/core/src/rum/DdRum.ts b/packages/core/src/rum/DdRum.ts index 8943ed5b0..45cafca5b 100644 --- a/packages/core/src/rum/DdRum.ts +++ b/packages/core/src/rum/DdRum.ts @@ -13,6 +13,7 @@ import { DdSdk } from '../sdk/DdSdk'; import { GlobalState } from '../sdk/GlobalState/GlobalState'; import { validateContext } from '../utils/argsUtils'; import { getErrorContext } from '../utils/errorUtils'; +import { getGlobalInstance } from '../utils/singletonUtils'; import { DefaultTimeProvider } from '../utils/time-provider/DefaultTimeProvider'; import type { TimeProvider } from '../utils/time-provider/TimeProvider'; @@ -43,6 +44,8 @@ import type { PropagatorType } from './types'; +const RUM_MODULE = 'com.datadog.reactnative.rum'; + const generateEmptyPromise = () => new Promise(resolve => resolve()); class DdRumWrapper implements DdRumType { @@ -501,4 +504,4 @@ const isOldStopActionAPI = ( return typeof args[0] === 'object' || typeof args[0] === 'undefined'; }; -export const DdRum = new DdRumWrapper(); +export const DdRum = getGlobalInstance(RUM_MODULE, () => new DdRumWrapper()); diff --git a/packages/core/src/trace/DdTrace.ts b/packages/core/src/trace/DdTrace.ts index b106a97d8..119716914 100644 --- a/packages/core/src/trace/DdTrace.ts +++ b/packages/core/src/trace/DdTrace.ts @@ -13,8 +13,11 @@ import { } from '../sdk/DatadogProvider/Buffer/bufferNativeCall'; import type { DdTraceType } from '../types'; import { validateContext } from '../utils/argsUtils'; +import { getGlobalInstance } from '../utils/singletonUtils'; import { DefaultTimeProvider } from '../utils/time-provider/DefaultTimeProvider'; +const TRACE_MODULE = 'com.datadog.reactnative.trace'; + const timeProvider = new DefaultTimeProvider(); class DdTraceWrapper implements DdTraceType { @@ -59,6 +62,7 @@ class DdTraceWrapper implements DdTraceType { }; } -const DdTrace: DdTraceType = new DdTraceWrapper(); - -export { DdTrace }; +export const DdTrace: DdTraceType = getGlobalInstance( + TRACE_MODULE, + () => new DdTraceWrapper() +); diff --git a/packages/core/src/utils/__tests__/singletonUtils.test.ts b/packages/core/src/utils/__tests__/singletonUtils.test.ts new file mode 100644 index 000000000..f424562c6 --- /dev/null +++ b/packages/core/src/utils/__tests__/singletonUtils.test.ts @@ -0,0 +1,75 @@ +import { getGlobalInstance } from '../singletonUtils'; + +describe('singletonUtils', () => { + const createdSymbols: symbol[] = []; + const g = (globalThis as unknown) as Record; + + afterEach(() => { + for (const symbol of createdSymbols) { + delete g[symbol]; + } + + createdSymbols.length = 0; + jest.restoreAllMocks(); + }); + + it('only creates one instance for the same key', () => { + const key = 'com.datadog.reactnative.test'; + const symbol = Symbol.for(key); + createdSymbols.push(symbol); + + const objectConstructor = jest.fn(() => ({ id: 1 })); + const a = getGlobalInstance(key, objectConstructor); + const b = getGlobalInstance(key, objectConstructor); + + expect(a).toBe(b); + expect(objectConstructor).toHaveBeenCalledTimes(1); + expect(g[symbol]).toBe(a); + }); + + it('returns a pre-existing instance without creating a new one for the same key', () => { + const key = 'com.datadog.reactnative.test'; + const symbol = Symbol.for(key); + createdSymbols.push(symbol); + + const existing = { pre: true }; + g[symbol] = existing; + + const objectConstructor = jest.fn(() => ({ created: true })); + const result = getGlobalInstance(key, objectConstructor); + + expect(result).toBe(existing); + expect(objectConstructor).not.toHaveBeenCalled(); + }); + + it('creates a new instance for a different key', () => { + const keyA = 'com.datadog.reactnative.test.a'; + const keyB = 'com.datadog.reactnative.test.b'; + const symbolA = Symbol.for(keyA); + const symbolB = Symbol.for(keyB); + createdSymbols.push(symbolA, symbolB); + + const a = getGlobalInstance(keyA, () => ({ id: 'A' })); + const b = getGlobalInstance(keyB, () => ({ id: 'B' })); + + expect(a).not.toBe(b); + expect((a as any).id).toBe('A'); + expect((b as any).id).toBe('B'); + }); + + it('does not overwrite existing instance if called with a different constructor', () => { + const key = 'com.datadog.reactnative.test'; + const symbol = Symbol.for(key); + createdSymbols.push(symbol); + + const firstObjectConstructor = jest.fn(() => ({ id: 1 })); + const first = getGlobalInstance(key, firstObjectConstructor); + + const secondObjectConstructor = jest.fn(() => ({ id: 2 })); + const second = getGlobalInstance(key, secondObjectConstructor); + + expect(first).toBe(second); + expect(firstObjectConstructor).toHaveBeenCalledTimes(1); + expect(secondObjectConstructor).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/core/src/utils/singletonUtils.ts b/packages/core/src/utils/singletonUtils.ts new file mode 100644 index 000000000..9f00c2cd0 --- /dev/null +++ b/packages/core/src/utils/singletonUtils.ts @@ -0,0 +1,12 @@ +export const getGlobalInstance = ( + key: string, + objectConstructor: () => T +): T => { + const symbol = Symbol.for(key); + const g = (globalThis as unknown) as Record; + + if (!(symbol in g)) { + g[symbol] = objectConstructor(); + } + return g[symbol] as T; +}; From 09a53c6f7b079558ffe7420f56f30411f1f7432c Mon Sep 17 00:00:00 2001 From: Sergio Barrio Date: Fri, 5 Sep 2025 17:47:29 +0200 Subject: [PATCH 029/410] Use native sdk's core instance instead of the one inside RN SDK wrapper --- .../datadog/reactnative/DatadogSDKWrapper.kt | 90 +------------------ .../com/datadog/reactnative/DatadogWrapper.kt | 48 ---------- .../reactnative/DdLogsImplementation.kt | 3 +- .../reactnative/DdSdkImplementation.kt | 9 +- .../reactnative/DdSdkNativeInitialization.kt | 13 ++- .../reactnative/DdSdkReactNativePackage.kt | 3 +- .../com/datadog/reactnative/DdTelemetry.kt | 56 ++++++++++++ .../kotlin/com/datadog/reactnative/DdSdk.kt | 3 +- .../kotlin/com/datadog/reactnative/DdSdk.kt | 5 +- .../DdSdkNativeInitializationTest.kt | 4 + .../com/datadog/reactnative/DdSdkTest.kt | 6 +- .../DdInternalTestingImplementation.kt | 2 +- .../DdInternalTestingImplementationTest.kt | 74 ++++++++------- .../DdSessionReplayImplementation.kt | 8 +- 14 files changed, 140 insertions(+), 184 deletions(-) create mode 100644 packages/core/android/src/main/kotlin/com/datadog/reactnative/DdTelemetry.kt diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogSDKWrapper.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogSDKWrapper.kt index c9865a5d7..da0841ac4 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogSDKWrapper.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogSDKWrapper.kt @@ -8,23 +8,14 @@ package com.datadog.reactnative import android.content.Context -import android.util.Log import com.datadog.android.Datadog -import com.datadog.android._InternalProxy import com.datadog.android.api.InternalLogger -import com.datadog.android.api.SdkCore import com.datadog.android.api.feature.FeatureSdkCore import com.datadog.android.core.InternalSdkCore import com.datadog.android.core.configuration.Configuration -import com.datadog.android.log.Logs -import com.datadog.android.log.LogsConfiguration import com.datadog.android.privacy.TrackingConsent import com.datadog.android.rum.GlobalRumMonitor -import com.datadog.android.rum.Rum -import com.datadog.android.rum.RumConfiguration import com.datadog.android.rum.RumMonitor -import com.datadog.android.trace.Trace -import com.datadog.android.trace.TraceConfiguration import com.datadog.android.webview.WebViewTracking import com.facebook.react.bridge.ReadableMap @@ -50,50 +41,18 @@ object DatadogSDKWrapperStorage { listener(ddCore) } } - - /** - * Sets instance of core SDK to be used to initialize features. - */ - fun setSdkCore(core: InternalSdkCore?) { - this.core = core - } - - /** - * Returns the core set by setSdkCore or the default core instance by default. - */ - fun getSdkCore(): SdkCore { - core?.let { - return it - } - Log.d( - DatadogSDKWrapperStorage::class.java.canonicalName, - "SdkCore was not set in DatadogSDKWrapperStorage, using default instance." - ) - return Datadog.getInstance() - } } internal class DatadogSDKWrapper : DatadogWrapper { override var bundleLogsWithRum = DefaultConfiguration.bundleLogsWithRum override var bundleLogsWithTraces = DefaultConfiguration.bundleLogsWithTraces - // We use Kotlin backing field here to initialize once the telemetry proxy - // and make sure it is only after SDK is initialized. - private var telemetryProxy: _InternalProxy._TelemetryProxy? = null - get() { - if (field == null && isInitialized()) { - field = Datadog._internalProxy()._telemetry - } - - return field - } - // We use Kotlin backing field here to initialize once the telemetry proxy // and make sure it is only after SDK is initialized. private var webViewProxy: WebViewTracking._InternalWebViewProxy? = null get() { if (field == null && isInitialized()) { - field = WebViewTracking._InternalWebViewProxy(DatadogSDKWrapperStorage.getSdkCore()) + field = WebViewTracking._InternalWebViewProxy(Datadog.getInstance()) } return field @@ -109,20 +68,7 @@ internal class DatadogSDKWrapper : DatadogWrapper { consent: TrackingConsent ) { val core = Datadog.initialize(context, configuration, consent) - DatadogSDKWrapperStorage.setSdkCore(core as InternalSdkCore) - DatadogSDKWrapperStorage.notifyOnInitializedListeners(core) - } - - override fun enableRum(configuration: RumConfiguration) { - Rum.enable(configuration, DatadogSDKWrapperStorage.getSdkCore()) - } - - override fun enableLogs(configuration: LogsConfiguration) { - Logs.enable(configuration, DatadogSDKWrapperStorage.getSdkCore()) - } - - override fun enableTrace(configuration: TraceConfiguration) { - Trace.enable(configuration, DatadogSDKWrapperStorage.getSdkCore()) + DatadogSDKWrapperStorage.notifyOnInitializedListeners(core as InternalSdkCore) } @Deprecated("Use setUserInfo instead; the user ID is now required.") @@ -161,34 +107,6 @@ internal class DatadogSDKWrapper : DatadogWrapper { Datadog.setTrackingConsent(trackingConsent) } - override fun sendTelemetryLog(message: String, attributes: ReadableMap, config: ReadableMap) { - val core = DatadogSDKWrapperStorage.getSdkCore() as FeatureSdkCore? - val logger = core?.internalLogger; - - val additionalProperties = attributes.toMap() - val telemetryConfig = config.toMap() - - logger?.log( - level = InternalLogger.Level.INFO, - target = InternalLogger.Target.TELEMETRY, - messageBuilder = { message }, - onlyOnce = (telemetryConfig["onlyOnce"] as? Boolean) ?: true, - additionalProperties = additionalProperties - ) - } - - override fun telemetryDebug(message: String) { - telemetryProxy?.debug(message) - } - - override fun telemetryError(message: String, stack: String?, kind: String?) { - telemetryProxy?.error(message, stack, kind) - } - - override fun telemetryError(message: String, throwable: Throwable?) { - telemetryProxy?.error(message, throwable) - } - override fun consumeWebviewEvent(message: String) { webViewProxy?.consumeWebviewEvent(message) } @@ -198,11 +116,11 @@ internal class DatadogSDKWrapper : DatadogWrapper { } override fun getRumMonitor(): RumMonitor { - return GlobalRumMonitor.get(DatadogSDKWrapperStorage.getSdkCore()) + return GlobalRumMonitor.get(Datadog.getInstance()) } override fun clearAllData() { - return Datadog.clearAllData(DatadogSDKWrapperStorage.getSdkCore()) + return Datadog.clearAllData(Datadog.getInstance()) } } diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogWrapper.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogWrapper.kt index 41e86f4d5..9ac591d80 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogWrapper.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogWrapper.kt @@ -64,33 +64,6 @@ interface DatadogWrapper { consent: TrackingConsent ) - /** - * Enables the RUM feature of the SDK. - * - * @param configuration the configuration for the RUM feature - */ - fun enableRum( - configuration: RumConfiguration - ) - - /** - * Enables the Logs feature of the SDK. - * - * @param configuration the configuration for the Logs feature - */ - fun enableLogs( - configuration: LogsConfiguration - ) - - /** - * Enables the Trace feature of the SDK. - * - * @param configuration the configuration for the Trace feature - */ - fun enableTrace( - configuration: TraceConfiguration - ) - /** * Sets the user information. * @@ -144,27 +117,6 @@ interface DatadogWrapper { */ fun setTrackingConsent(trackingConsent: TrackingConsent) - - /** - * Sends telemetry event with attributes. - */ - fun sendTelemetryLog(message: String, attributes: ReadableMap, config: ReadableMap) - - /** - * Sends telemetry debug event. - */ - fun telemetryDebug(message: String) - - /** - * Sends telemetry error. - */ - fun telemetryError(message: String, stack: String?, kind: String?) - - /** - * Sends telemetry error. - */ - fun telemetryError(message: String, throwable: Throwable?) - /** * Sends Webview events. */ diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdLogsImplementation.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdLogsImplementation.kt index 9a84496ec..2f9bceff2 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdLogsImplementation.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdLogsImplementation.kt @@ -7,6 +7,7 @@ package com.datadog.reactnative import android.util.Log as AndroidLog +import com.datadog.android.Datadog import com.datadog.android.log.Logger import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReadableMap @@ -22,7 +23,7 @@ class DdLogsImplementation( val bundleLogsWithRum = datadog.bundleLogsWithRum val bundleLogsWithTraces = datadog.bundleLogsWithTraces - logger ?: Logger.Builder(DatadogSDKWrapperStorage.getSdkCore()) + logger ?: Logger.Builder(Datadog.getInstance()) .setLogcatLogsEnabled(true) .setBundleWithRumEnabled(bundleLogsWithRum) .setBundleWithTraceEnabled(bundleLogsWithTraces) diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt index cdd6b0614..b04a2ddf3 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt @@ -24,6 +24,7 @@ import java.util.concurrent.atomic.AtomicBoolean class DdSdkImplementation( private val reactContext: ReactApplicationContext, private val datadog: DatadogWrapper = DatadogSDKWrapper(), + private val ddTelemetry: DdTelemetry = DdTelemetry(), private val uiThreadExecutor: UiThreadExecutor = ReactUiThreadExecutor() ) { internal val appContext: Context = reactContext.applicationContext @@ -39,7 +40,7 @@ class DdSdkImplementation( fun initialize(configuration: ReadableMap, promise: Promise) { val ddSdkConfiguration = configuration.asDdSdkConfiguration() - val nativeInitialization = DdSdkNativeInitialization(appContext, datadog) + val nativeInitialization = DdSdkNativeInitialization(appContext, datadog, ddTelemetry) nativeInitialization.initialize(ddSdkConfiguration) this.frameRateProvider = createFrameRateProvider(ddSdkConfiguration) @@ -145,7 +146,7 @@ class DdSdkImplementation( * @param config Configuration object, can take 'onlyOnce: Boolean' */ fun sendTelemetryLog(message: String, attributes: ReadableMap, config: ReadableMap, promise: Promise) { - datadog.sendTelemetryLog(message, attributes, config) + ddTelemetry.sendTelemetryLog(message, attributes, config) promise.resolve(null) } @@ -154,7 +155,7 @@ class DdSdkImplementation( * @param message Debug message. */ fun telemetryDebug(message: String, promise: Promise) { - datadog.telemetryDebug(message) + ddTelemetry.telemetryDebug(message) promise.resolve(null) } @@ -165,7 +166,7 @@ class DdSdkImplementation( * @param kind Error kind. */ fun telemetryError(message: String, stack: String, kind: String, promise: Promise) { - datadog.telemetryError(message, stack, kind) + ddTelemetry.telemetryError(message, stack, kind) promise.resolve(null) } diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkNativeInitialization.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkNativeInitialization.kt index 9c8e8370d..ee55d08fe 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkNativeInitialization.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkNativeInitialization.kt @@ -9,14 +9,17 @@ package com.datadog.reactnative import android.content.Context import android.content.pm.PackageManager import android.util.Log +import com.datadog.android.Datadog import com.datadog.android.DatadogSite import com.datadog.android.core.configuration.BatchProcessingLevel import com.datadog.android.core.configuration.BatchSize import com.datadog.android.core.configuration.Configuration import com.datadog.android.core.configuration.UploadFrequency import com.datadog.android.event.EventMapper +import com.datadog.android.log.Logs import com.datadog.android.log.LogsConfiguration import com.datadog.android.privacy.TrackingConsent +import com.datadog.android.rum.Rum import com.datadog.android.rum.RumConfiguration import com.datadog.android.rum._RumInternalProxy import com.datadog.android.rum.configuration.VitalsUpdateFrequency @@ -25,6 +28,7 @@ import com.datadog.android.rum.model.ActionEvent import com.datadog.android.rum.model.ResourceEvent import com.datadog.android.rum.tracking.ActivityViewTrackingStrategy import com.datadog.android.telemetry.model.TelemetryConfigurationEvent +import com.datadog.android.trace.Trace import com.datadog.android.trace.TraceConfiguration import com.google.gson.Gson import java.util.Locale @@ -37,6 +41,7 @@ import kotlin.time.Duration.Companion.seconds class DdSdkNativeInitialization internal constructor( private val appContext: Context, private val datadog: DatadogWrapper = DatadogSDKWrapper(), + private val ddTelemetry: DdTelemetry = DdTelemetry(), private val jsonFileReader: JSONFileReader = JSONFileReader() ) { internal fun initialize(ddSdkConfiguration: DdSdkConfiguration) { @@ -59,11 +64,11 @@ class DdSdkNativeInitialization internal constructor( datadog.initialize(appContext, sdkConfiguration, trackingConsent) - datadog.enableRum(rumConfiguration) + Rum.enable(rumConfiguration, Datadog.getInstance()) - datadog.enableTrace(traceConfiguration) + Logs.enable(logsConfiguration, Datadog.getInstance()) - datadog.enableLogs(logsConfiguration) + Trace.enable(traceConfiguration, Datadog.getInstance()) } private fun configureRumAndTracesForLogs(configuration: DdSdkConfiguration) { @@ -95,7 +100,7 @@ class DdSdkNativeInitialization internal constructor( try { appContext.packageManager.getPackageInfo(packageName, 0) } catch (e: PackageManager.NameNotFoundException) { - datadog.telemetryError(e.message ?: DdSdkImplementation.PACKAGE_INFO_NOT_FOUND_ERROR_MESSAGE, e) + ddTelemetry.telemetryError(e.message ?: DdSdkImplementation.PACKAGE_INFO_NOT_FOUND_ERROR_MESSAGE, e) return DdSdkImplementation.DEFAULT_APP_VERSION } diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkReactNativePackage.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkReactNativePackage.kt index bac9f49f5..3a5b022c1 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkReactNativePackage.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkReactNativePackage.kt @@ -18,9 +18,10 @@ import com.facebook.react.module.model.ReactModuleInfoProvider */ class DdSdkReactNativePackage : TurboReactPackage() { private val sdkWrapper = DatadogSDKWrapper() + private val ddTelemetry = DdTelemetry() override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? { return when (name) { - DdSdkImplementation.NAME -> DdSdk(reactContext, sdkWrapper) + DdSdkImplementation.NAME -> DdSdk(reactContext, sdkWrapper, ddTelemetry) DdRumImplementation.NAME -> DdRum(reactContext, sdkWrapper) DdTraceImplementation.NAME -> DdTrace(reactContext) DdLogsImplementation.NAME -> DdLogs(reactContext, sdkWrapper) diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdTelemetry.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdTelemetry.kt new file mode 100644 index 000000000..2d60df004 --- /dev/null +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdTelemetry.kt @@ -0,0 +1,56 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.reactnative + +import com.datadog.android.Datadog +import com.datadog.android._InternalProxy +import com.datadog.android.api.InternalLogger +import com.datadog.android.api.feature.FeatureSdkCore +import com.facebook.react.bridge.ReadableMap + +class DdTelemetry { + + // We use Kotlin backing field here to initialize once the telemetry proxy + // and make sure it is only after SDK is initialized. + private var telemetryProxy: _InternalProxy._TelemetryProxy? = null + get() { + if (field == null && Datadog.isInitialized()) { + field = Datadog._internalProxy()._telemetry + } + + return field + } + + fun sendTelemetryLog(message: String, attributes: ReadableMap, config: ReadableMap) { + val core = Datadog.getInstance() as FeatureSdkCore? + val logger = core?.internalLogger; + + val additionalProperties = attributes.toMap() + val telemetryConfig = config.toMap() + + logger?.log( + level = InternalLogger.Level.INFO, + target = InternalLogger.Target.TELEMETRY, + messageBuilder = { message }, + onlyOnce = (telemetryConfig["onlyOnce"] as? Boolean) ?: true, + additionalProperties = additionalProperties + ) + } + + fun telemetryDebug(message: String) { + telemetryProxy?.debug(message) + } + + fun telemetryError(message: String, stack: String?, kind: String?) { + telemetryProxy?.error(message, stack, kind) + } + + fun telemetryError(message: String, throwable: Throwable?) { + telemetryProxy?.error(message, throwable) + } +} + diff --git a/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdSdk.kt b/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdSdk.kt index d46e53ade..5bc470947 100644 --- a/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdSdk.kt +++ b/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdSdk.kt @@ -19,9 +19,10 @@ import com.facebook.react.modules.core.DeviceEventManagerModule class DdSdk( reactContext: ReactApplicationContext, datadogWrapper: DatadogWrapper = DatadogSDKWrapper() + ddTelemetry: DdTelemetry = DdTelemetry() ) : NativeDdSdkSpec(reactContext) { - private val implementation = DdSdkImplementation(reactContext, datadog = datadogWrapper) + private val implementation = DdSdkImplementation(reactContext, datadog = datadogWrapper, ddTelemetry) override fun getName(): String = DdSdkImplementation.NAME diff --git a/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt b/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt index b41eff1db..af8f87c29 100644 --- a/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt +++ b/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt @@ -17,10 +17,11 @@ import com.facebook.react.bridge.ReadableMap /** The entry point to initialize Datadog's features. */ class DdSdk( reactContext: ReactApplicationContext, - datadogWrapper: DatadogWrapper = DatadogSDKWrapper() + datadogWrapper: DatadogWrapper = DatadogSDKWrapper(), + ddTelemetry: DdTelemetry = DdTelemetry() ) : ReactContextBaseJavaModule(reactContext) { - private val implementation = DdSdkImplementation(reactContext, datadog = datadogWrapper) + private val implementation = DdSdkImplementation(reactContext, datadog = datadogWrapper, ddTelemetry) private var lifecycleEventListener: LifecycleEventListener? = null override fun getName(): String = DdSdkImplementation.NAME diff --git a/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkNativeInitializationTest.kt b/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkNativeInitializationTest.kt index d05d43c57..e25bfe999 100644 --- a/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkNativeInitializationTest.kt +++ b/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkNativeInitializationTest.kt @@ -48,6 +48,9 @@ internal class DdSdkNativeInitializationTest { @Mock lateinit var mockDatadog: DatadogWrapper + @Mock + lateinit var mockDdTelemetry: DdTelemetry + @Mock lateinit var mockJSONFileReader: JSONFileReader @@ -64,6 +67,7 @@ internal class DdSdkNativeInitializationTest { testedNativeInitialization = DdSdkNativeInitialization( mockContext, mockDatadog, + mockDdTelemetry, mockJSONFileReader ) } diff --git a/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt b/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt index 5cda53616..9e7d671fd 100644 --- a/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt +++ b/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt @@ -120,6 +120,9 @@ internal class DdSdkTest { @Mock lateinit var mockDatadog: DatadogWrapper + @Mock + lateinit var mockDdTelemetry: DdTelemetry + @Forgery lateinit var fakeConfiguration: DdSdkConfiguration @@ -157,9 +160,8 @@ internal class DdSdkTest { answer.getArgument(0).run() true } - testedBridgeSdk = DdSdkImplementation(mockReactContext, mockDatadog, TestUiThreadExecutor()) + testedBridgeSdk = DdSdkImplementation(mockReactContext, mockDatadog, mockDdTelemetry, TestUiThreadExecutor()) - DatadogSDKWrapperStorage.setSdkCore(null) DatadogSDKWrapperStorage.onInitializedListeners.clear() } diff --git a/packages/internal-testing-tools/android/src/main/kotlin/com/datadog/reactnative/internaltesting/DdInternalTestingImplementation.kt b/packages/internal-testing-tools/android/src/main/kotlin/com/datadog/reactnative/internaltesting/DdInternalTestingImplementation.kt index 197a9df75..df0030fb5 100644 --- a/packages/internal-testing-tools/android/src/main/kotlin/com/datadog/reactnative/internaltesting/DdInternalTestingImplementation.kt +++ b/packages/internal-testing-tools/android/src/main/kotlin/com/datadog/reactnative/internaltesting/DdInternalTestingImplementation.kt @@ -7,6 +7,7 @@ package com.datadog.reactnative.internaltesting import com.datadog.android.api.InternalLogger +import com.datadog.android.Datadog import com.datadog.android.api.context.DatadogContext import com.datadog.android.api.context.NetworkInfo import com.datadog.android.api.context.TimeInfo @@ -53,7 +54,6 @@ class DdInternalTestingImplementation { fun enable(promise: Promise) { DatadogSDKWrapperStorage.addOnInitializedListener { ddCore -> this.wrappedCore = StubSDKCore(ddCore) - DatadogSDKWrapperStorage.setSdkCore(this.wrappedCore) } promise.resolve(null) } diff --git a/packages/internal-testing-tools/android/src/test/kotlin/com/datadog/reactnative/internaltesting/DdInternalTestingImplementationTest.kt b/packages/internal-testing-tools/android/src/test/kotlin/com/datadog/reactnative/internaltesting/DdInternalTestingImplementationTest.kt index 6c278026a..c542ed6bc 100644 --- a/packages/internal-testing-tools/android/src/test/kotlin/com/datadog/reactnative/internaltesting/DdInternalTestingImplementationTest.kt +++ b/packages/internal-testing-tools/android/src/test/kotlin/com/datadog/reactnative/internaltesting/DdInternalTestingImplementationTest.kt @@ -7,6 +7,8 @@ package com.datadog.reactnative.internaltesting import android.content.Context +import com.datadog.android.Datadog +import com.datadog.android.api.SdkCore import com.datadog.android.api.context.DatadogContext import com.datadog.android.api.feature.Feature import com.datadog.android.api.feature.FeatureScope @@ -24,6 +26,7 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith import org.junit.jupiter.api.extension.Extensions import org.mockito.Mock +import org.mockito.Mockito import org.mockito.Mockito.mock import org.mockito.junit.jupiter.MockitoExtension import org.mockito.junit.jupiter.MockitoSettings @@ -57,37 +60,47 @@ internal class DdInternalTestingImplementationTest { @Test fun `M return captured events W enable()`() { - // Given - val mockFeature = MockFeature("mockFeature") - val mockFeatureScope = MockFeatureScope(mockFeature) - whenever(mockCore.getFeature(mockFeature.name)).doReturn( - mockFeatureScope - ) - whenever(mockCore.getDatadogContext()).doReturn( - mockContext - ) - - // When - testedInternalTesting.enable(mockPromise) - // Simulating DdSdkImplementation initialization - DatadogSDKWrapperStorage.setSdkCore(mockCore) - DatadogSDKWrapperStorage.notifyOnInitializedListeners(mockCore) - - val wrappedCore = DatadogSDKWrapperStorage.getSdkCore() as StubSDKCore - wrappedCore.registerFeature(mockFeature) - requireNotNull(wrappedCore.getFeature(mockFeature.name)) - .withWriteContext { _, eventBatchWriter -> - eventBatchWriter.write( - RawBatchEvent(data = "mock event for test".toByteArray()), - batchMetadata = null, - eventType = EventType.DEFAULT + Mockito.mockStatic(Datadog::class.java).use { datadogStatic -> + // Given + datadogStatic.`when` { + Datadog.getInstance() + }.thenReturn(mockCore) + + val mockFeature = MockFeature("mockFeature") + val mockFeatureScope = MockFeatureScope(mockFeature) + whenever(mockCore.getFeature(mockFeature.name)).doReturn( + mockFeatureScope + ) + whenever(mockCore.getDatadogContext()).doReturn( + mockContext + ) + + // When + testedInternalTesting.enable(mockPromise) + // Simulating DdSdkImplementation initialization + DatadogSDKWrapperStorage.notifyOnInitializedListeners(mockCore) + + val wrappedCore = Datadog.getInstance() as StubSDKCore + wrappedCore.registerFeature(mockFeature) + requireNotNull(wrappedCore.getFeature(mockFeature.name)) + .withWriteContext { _, eventBatchWriter -> + eventBatchWriter.write( + RawBatchEvent(data = "mock event for test".toByteArray()), + batchMetadata = null, + eventType = EventType.DEFAULT + ) + } + + // Then + assertThat( + wrappedCore.featureScopes[mockFeature.name] + ?.eventsWritten() + ?.first() + ) + .isEqualTo( + "mock event for test" ) - } - - // Then - assertThat(wrappedCore.featureScopes[mockFeature.name]?.eventsWritten()?.first()).isEqualTo( - "mock event for test" - ) + } } } @@ -96,6 +109,7 @@ internal class MockFeatureScope(private val feature: Feature) : FeatureScope { override fun sendEvent(event: Any) {} + @Suppress("UNCHECKED_CAST") override fun unwrap(): T { return feature as T } diff --git a/packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/DdSessionReplayImplementation.kt b/packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/DdSessionReplayImplementation.kt index ef5467bc4..cc2fd64dc 100644 --- a/packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/DdSessionReplayImplementation.kt +++ b/packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/DdSessionReplayImplementation.kt @@ -7,10 +7,10 @@ package com.datadog.reactnative.sessionreplay import android.annotation.SuppressLint +import com.datadog.android.Datadog import com.datadog.android.api.feature.FeatureSdkCore import com.datadog.android.sessionreplay.SessionReplayConfiguration import com.datadog.android.sessionreplay._SessionReplayInternalProxy -import com.datadog.reactnative.DatadogSDKWrapperStorage import com.datadog.reactnative.sessionreplay.utils.text.TextViewUtils import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReactContext @@ -40,7 +40,7 @@ class DdSessionReplayImplementation( startRecordingImmediately: Boolean, promise: Promise ) { - val sdkCore = DatadogSDKWrapperStorage.getSdkCore() as FeatureSdkCore + val sdkCore = Datadog.getInstance() as FeatureSdkCore val logger = sdkCore.internalLogger val textViewUtils = TextViewUtils.create(reactContext, logger) val internalCallback = ReactNativeInternalCallback(reactContext) @@ -68,7 +68,7 @@ class DdSessionReplayImplementation( */ fun startRecording(promise: Promise) { sessionReplayProvider().startRecording( - DatadogSDKWrapperStorage.getSdkCore() as FeatureSdkCore + Datadog.getInstance() as FeatureSdkCore ) promise.resolve(null) } @@ -78,7 +78,7 @@ class DdSessionReplayImplementation( */ fun stopRecording(promise: Promise) { sessionReplayProvider().stopRecording( - DatadogSDKWrapperStorage.getSdkCore() as FeatureSdkCore + Datadog.getInstance() as FeatureSdkCore ) promise.resolve(null) } From 3e2b2d1e854ecab68877eb16a74b04a71723bf41 Mon Sep 17 00:00:00 2001 From: Marco Saia Date: Mon, 22 Sep 2025 16:20:27 +0200 Subject: [PATCH 030/410] Fixed internal testing tools and unit tests --- .../com/datadog/reactnative/DatadogWrapper.kt | 8 +- .../reactnative/DdSdkNativeInitialization.kt | 2 - .../com/datadog/reactnative/DdTelemetry.kt | 40 + .../kotlin/com/datadog/reactnative/DdSdk.kt | 6 +- .../com/datadog/reactnative/DdSdkTest.kt | 2628 ++++++++++------- .../DdInternalTestingImplementation.kt | 6 + .../DdInternalTestingImplementationTest.kt | 4 +- 7 files changed, 1695 insertions(+), 999 deletions(-) diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogWrapper.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogWrapper.kt index 9ac591d80..19b25e587 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogWrapper.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogWrapper.kt @@ -18,7 +18,7 @@ import com.facebook.react.bridge.ReadableMap import java.lang.IllegalArgumentException /** - * Wrapper around [Datadog]. + * Wrapper around [com.datadog.android.Datadog]. */ @Suppress("ComplexInterface", "TooManyFunctions") interface DatadogWrapper { @@ -49,10 +49,8 @@ interface DatadogWrapper { /** * Initializes the Datadog SDK. * @param context your application context - * @param credentials your organization credentials * @param configuration the configuration for the SDK library - * @param trackingConsent as the initial state of the tracking consent flag. - * @see [Credentials] + * @param consent as the initial state of the tracking consent flag. * @see [Configuration] * @see [TrackingConsent] * @throws IllegalArgumentException if the env name is using illegal characters and your @@ -99,7 +97,7 @@ interface DatadogWrapper { /** * Sets the user information. - * @param extraUserInfo: The additional information. (To set the id, name or email please user setUserInfo). + * @param extraInfo: The additional information. (To set the id, name or email please user setUserInfo). */ fun addUserExtraInfo( extraInfo: Map diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkNativeInitialization.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkNativeInitialization.kt index ee55d08fe..4388ad5f6 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkNativeInitialization.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkNativeInitialization.kt @@ -65,9 +65,7 @@ class DdSdkNativeInitialization internal constructor( datadog.initialize(appContext, sdkConfiguration, trackingConsent) Rum.enable(rumConfiguration, Datadog.getInstance()) - Logs.enable(logsConfiguration, Datadog.getInstance()) - Trace.enable(traceConfiguration, Datadog.getInstance()) } diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdTelemetry.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdTelemetry.kt index 2d60df004..24354ce78 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdTelemetry.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdTelemetry.kt @@ -12,6 +12,13 @@ import com.datadog.android.api.InternalLogger import com.datadog.android.api.feature.FeatureSdkCore import com.facebook.react.bridge.ReadableMap +/** + * **[INTERNAL USAGE]** + * + * Utility class used by React Native modules to forward telemetry events to the Datadog SDK. + * + * This class is **public only for Datadog internal package visibility** and should not be used. + */ class DdTelemetry { // We use Kotlin backing field here to initialize once the telemetry proxy @@ -25,6 +32,15 @@ class DdTelemetry { return field } + /** + * **[INTERNAL USAGE]** + * + * Sends a telemetry log message with additional attributes and configuration options. + * + * @param message the message to log + * @param attributes additional key–value properties to include in the log + * @param config configuration options for the telemetry log (e.g. `onlyOnce` flag) + */ fun sendTelemetryLog(message: String, attributes: ReadableMap, config: ReadableMap) { val core = Datadog.getInstance() as FeatureSdkCore? val logger = core?.internalLogger; @@ -41,14 +57,38 @@ class DdTelemetry { ) } + /** + * **[INTERNAL USAGE]** + * + * Sends a debug-level telemetry message. + * + * @param message the debug message + */ fun telemetryDebug(message: String) { telemetryProxy?.debug(message) } + /** + * **[INTERNAL USAGE]** + * + * Sends an error-level telemetry message with optional details. + * + * @param message the error message + * @param stack an optional stack trace string + * @param kind an optional error kind or category + */ fun telemetryError(message: String, stack: String?, kind: String?) { telemetryProxy?.error(message, stack, kind) } + /** + * **[INTERNAL USAGE]** + * + * Sends an error-level telemetry message with an attached [Throwable]. + * + * @param message the error message + * @param throwable the throwable associated with the error + */ fun telemetryError(message: String, throwable: Throwable?) { telemetryProxy?.error(message, throwable) } diff --git a/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt b/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt index af8f87c29..17acd6d20 100644 --- a/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt +++ b/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt @@ -21,7 +21,11 @@ class DdSdk( ddTelemetry: DdTelemetry = DdTelemetry() ) : ReactContextBaseJavaModule(reactContext) { - private val implementation = DdSdkImplementation(reactContext, datadog = datadogWrapper, ddTelemetry) + private val implementation = DdSdkImplementation( + reactContext, + datadog = datadogWrapper, + ddTelemetry + ) private var lifecycleEventListener: LifecycleEventListener? = null override fun getName(): String = DdSdkImplementation.NAME diff --git a/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt b/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt index 9e7d671fd..a39485dae 100644 --- a/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt +++ b/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt @@ -16,8 +16,10 @@ import com.datadog.android.core.configuration.BatchSize import com.datadog.android.core.configuration.Configuration import com.datadog.android.core.configuration.UploadFrequency import com.datadog.android.event.EventMapper +import com.datadog.android.log.Logs import com.datadog.android.log.LogsConfiguration import com.datadog.android.privacy.TrackingConsent +import com.datadog.android.rum.Rum import com.datadog.android.rum.RumConfiguration import com.datadog.android.rum.RumPerformanceMetric import com.datadog.android.rum._RumInternalProxy @@ -27,6 +29,7 @@ import com.datadog.android.rum.model.ActionEvent import com.datadog.android.rum.model.ResourceEvent import com.datadog.android.rum.tracking.ActivityViewTrackingStrategy import com.datadog.android.telemetry.model.TelemetryConfigurationEvent +import com.datadog.android.trace.Trace import com.datadog.android.trace.TraceConfiguration import com.datadog.android.trace.TracingHeaderType import com.datadog.tools.unit.GenericAssert.Companion.assertThat @@ -160,7 +163,12 @@ internal class DdSdkTest { answer.getArgument(0).run() true } - testedBridgeSdk = DdSdkImplementation(mockReactContext, mockDatadog, mockDdTelemetry, TestUiThreadExecutor()) + testedBridgeSdk = DdSdkImplementation( + mockReactContext, + mockDatadog, + mockDdTelemetry, + TestUiThreadExecutor() + ) DatadogSDKWrapperStorage.onInitializedListeners.clear() } @@ -181,35 +189,49 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(sdkConfigCaptor.firstValue) - .hasField("coreConfig") { - it.hasFieldEqualTo("needsClearTextHttp", false) - it.hasFieldEqualTo("firstPartyHostsWithHeaderTypes", emptyMap()) + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } - .hasFieldEqualTo("clientToken", fakeConfiguration.clientToken) - .hasFieldEqualTo("env", fakeConfiguration.env) - .hasFieldEqualTo("variant", "") - .hasFieldEqualTo("crashReportsEnabled", true) - .hasFieldEqualTo( - "additionalConfig", - fakeConfiguration.additionalConfig?.filterValues { it != null }.orEmpty() - ) - assertThat(rumConfigCaptor.firstValue) - .hasFieldEqualTo("applicationId", fakeConfiguration.applicationId) + assertThat(sdkConfigCaptor.firstValue) + .hasField("coreConfig") { + it.hasFieldEqualTo("needsClearTextHttp", false) + it.hasFieldEqualTo("firstPartyHostsWithHeaderTypes", emptyMap()) + } + .hasFieldEqualTo("clientToken", fakeConfiguration.clientToken) + .hasFieldEqualTo("env", fakeConfiguration.env) + .hasFieldEqualTo("variant", "") + .hasFieldEqualTo("crashReportsEnabled", true) + .hasFieldEqualTo( + "additionalConfig", + fakeConfiguration.additionalConfig?.filterValues { it != null }.orEmpty() + ) + assertThat(rumConfigCaptor.firstValue) + .hasFieldEqualTo("applicationId", fakeConfiguration.applicationId) + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } @Test @@ -221,75 +243,104 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(sdkConfigCaptor.firstValue) - .hasField("coreConfig") { - it.hasFieldEqualTo("needsClearTextHttp", false) - it.hasFieldEqualTo("firstPartyHostsWithHeaderTypes", emptyMap()) + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } - .hasFieldEqualTo("clientToken", fakeConfiguration.clientToken) - .hasFieldEqualTo("env", fakeConfiguration.env) - .hasFieldEqualTo("variant", "") - .hasFieldEqualTo("crashReportsEnabled", false) - .hasFieldEqualTo( - "additionalConfig", - fakeConfiguration.additionalConfig?.filterValues { it != null }.orEmpty() - ) - assertThat(rumConfigCaptor.firstValue) - .hasFieldEqualTo("applicationId", fakeConfiguration.applicationId) + assertThat(sdkConfigCaptor.firstValue) + .hasField("coreConfig") { + it.hasFieldEqualTo("needsClearTextHttp", false) + it.hasFieldEqualTo("firstPartyHostsWithHeaderTypes", emptyMap()) + } + .hasFieldEqualTo("clientToken", fakeConfiguration.clientToken) + .hasFieldEqualTo("env", fakeConfiguration.env) + .hasFieldEqualTo("variant", "") + .hasFieldEqualTo("crashReportsEnabled", false) + .hasFieldEqualTo( + "additionalConfig", + fakeConfiguration.additionalConfig?.filterValues { it != null }.orEmpty() + ) + assertThat(rumConfigCaptor.firstValue) + .hasFieldEqualTo("applicationId", fakeConfiguration.applicationId) + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } @Test fun `𝕄 initialize native SDK 𝕎 initialize() {nativeCrashReportEnabled=null}`() { // Given fakeConfiguration = fakeConfiguration.copy(nativeCrashReportEnabled = false, site = null) + val sdkConfigCaptor = argumentCaptor() val rumConfigCaptor = argumentCaptor() val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(sdkConfigCaptor.firstValue) - .hasField("coreConfig") { - it.hasFieldEqualTo("needsClearTextHttp", false) - it.hasFieldEqualTo("firstPartyHostsWithHeaderTypes", emptyMap()) + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } - .hasFieldEqualTo("clientToken", fakeConfiguration.clientToken) - .hasFieldEqualTo("env", fakeConfiguration.env) - .hasFieldEqualTo("variant", "") - .hasFieldEqualTo("crashReportsEnabled", false) - .hasFieldEqualTo( - "additionalConfig", - fakeConfiguration.additionalConfig?.filterValues { it != null }.orEmpty() - ) - assertThat(rumConfigCaptor.firstValue) - .hasFieldEqualTo("applicationId", fakeConfiguration.applicationId) + assertThat(sdkConfigCaptor.firstValue) + .hasField("coreConfig") { + it.hasFieldEqualTo("needsClearTextHttp", false) + it.hasFieldEqualTo("firstPartyHostsWithHeaderTypes", emptyMap()) + } + .hasFieldEqualTo("clientToken", fakeConfiguration.clientToken) + .hasFieldEqualTo("env", fakeConfiguration.env) + .hasFieldEqualTo("variant", "") + .hasFieldEqualTo("crashReportsEnabled", false) + .hasFieldEqualTo( + "additionalConfig", + fakeConfiguration.additionalConfig?.filterValues { it != null }.orEmpty() + ) + assertThat(rumConfigCaptor.firstValue) + .hasFieldEqualTo("applicationId", fakeConfiguration.applicationId) + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } // endregion @@ -305,37 +356,50 @@ internal class DdSdkTest { val traceConfigCaptor = argumentCaptor() val expectedRumSampleRate = fakeConfiguration.sampleRate?.toFloat() ?: 100f - // When - testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(sdkConfigCaptor.firstValue) - .hasField("coreConfig") { - it.hasFieldEqualTo("needsClearTextHttp", false) - it.hasFieldEqualTo("firstPartyHostsWithHeaderTypes", emptyMap()) - } - .hasFieldEqualTo("clientToken", fakeConfiguration.clientToken) - .hasFieldEqualTo("env", fakeConfiguration.env) - .hasFieldEqualTo("variant", "") - .hasFieldEqualTo( - "additionalConfig", - fakeConfiguration.additionalConfig?.filterValues { it != null }.orEmpty() - ) - assertThat(rumConfigCaptor.firstValue) - .hasFieldEqualTo("applicationId", fakeConfiguration.applicationId) - .hasField("featureConfiguration") { - it.hasFieldEqualTo("sampleRate", expectedRumSampleRate) + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + // When + testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } + assertThat(sdkConfigCaptor.firstValue) + .hasField("coreConfig") { + it.hasFieldEqualTo("needsClearTextHttp", false) + it.hasFieldEqualTo("firstPartyHostsWithHeaderTypes", emptyMap()) + } + .hasFieldEqualTo("clientToken", fakeConfiguration.clientToken) + .hasFieldEqualTo("env", fakeConfiguration.env) + .hasFieldEqualTo("variant", "") + .hasFieldEqualTo( + "additionalConfig", + fakeConfiguration.additionalConfig?.filterValues { it != null }.orEmpty() + ) + assertThat(rumConfigCaptor.firstValue) + .hasFieldEqualTo("applicationId", fakeConfiguration.applicationId) + .hasField("featureConfiguration") { + it.hasFieldEqualTo("sampleRate", expectedRumSampleRate) + } + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } // endregion @@ -351,37 +415,50 @@ internal class DdSdkTest { val traceConfigCaptor = argumentCaptor() val expectedTelemetrySampleRate = fakeConfiguration.telemetrySampleRate?.toFloat() ?: 20f - // When - testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(sdkConfigCaptor.firstValue) - .hasField("coreConfig") { - it.hasFieldEqualTo("needsClearTextHttp", false) - it.hasFieldEqualTo("firstPartyHostsWithHeaderTypes", emptyMap()) - } - .hasFieldEqualTo("clientToken", fakeConfiguration.clientToken) - .hasFieldEqualTo("env", fakeConfiguration.env) - .hasFieldEqualTo("variant", "") - .hasFieldEqualTo( - "additionalConfig", - fakeConfiguration.additionalConfig?.filterValues { it != null }.orEmpty() - ) - assertThat(rumConfigCaptor.firstValue) - .hasFieldEqualTo("applicationId", fakeConfiguration.applicationId) - .hasField("featureConfiguration") { - it.hasFieldEqualTo("telemetrySampleRate", expectedTelemetrySampleRate) + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + // When + testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } + assertThat(sdkConfigCaptor.firstValue) + .hasField("coreConfig") { + it.hasFieldEqualTo("needsClearTextHttp", false) + it.hasFieldEqualTo("firstPartyHostsWithHeaderTypes", emptyMap()) + } + .hasFieldEqualTo("clientToken", fakeConfiguration.clientToken) + .hasFieldEqualTo("env", fakeConfiguration.env) + .hasFieldEqualTo("variant", "") + .hasFieldEqualTo( + "additionalConfig", + fakeConfiguration.additionalConfig?.filterValues { it != null }.orEmpty() + ) + assertThat(rumConfigCaptor.firstValue) + .hasFieldEqualTo("applicationId", fakeConfiguration.applicationId) + .hasField("featureConfiguration") { + it.hasFieldEqualTo("telemetrySampleRate", expectedTelemetrySampleRate) + } + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } // endregion @@ -397,31 +474,44 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(sdkConfigCaptor.firstValue) - .hasField("coreConfig") { - it.hasFieldEqualTo("needsClearTextHttp", false) - it.hasFieldEqualTo("firstPartyHostsWithHeaderTypes", emptyMap()) + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } - .hasFieldEqualTo("clientToken", fakeConfiguration.clientToken) - .hasFieldEqualTo("env", fakeConfiguration.env) - .hasFieldEqualTo("variant", "") - .hasFieldEqualTo("additionalConfig", emptyMap()) - assertThat(rumConfigCaptor.firstValue) - .hasFieldEqualTo("applicationId", fakeConfiguration.applicationId) + assertThat(sdkConfigCaptor.firstValue) + .hasField("coreConfig") { + it.hasFieldEqualTo("needsClearTextHttp", false) + it.hasFieldEqualTo("firstPartyHostsWithHeaderTypes", emptyMap()) + } + .hasFieldEqualTo("clientToken", fakeConfiguration.clientToken) + .hasFieldEqualTo("env", fakeConfiguration.env) + .hasFieldEqualTo("variant", "") + .hasFieldEqualTo("additionalConfig", emptyMap()) + assertThat(rumConfigCaptor.firstValue) + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } @Test @@ -432,34 +522,47 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(sdkConfigCaptor.firstValue) - .hasField("coreConfig") { - it.hasFieldEqualTo("needsClearTextHttp", false) - it.hasFieldEqualTo("firstPartyHostsWithHeaderTypes", emptyMap()) + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + // When + testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } - .hasFieldEqualTo("clientToken", fakeConfiguration.clientToken) - .hasFieldEqualTo("env", fakeConfiguration.env) - .hasFieldEqualTo("variant", "") - .hasFieldEqualTo( - "additionalConfig", - fakeConfiguration.additionalConfig?.filterValues { it != null }.orEmpty() - ) - assertThat(rumConfigCaptor.firstValue) - .hasFieldEqualTo("applicationId", fakeConfiguration.applicationId) + assertThat(sdkConfigCaptor.firstValue) + .hasField("coreConfig") { + it.hasFieldEqualTo("needsClearTextHttp", false) + it.hasFieldEqualTo("firstPartyHostsWithHeaderTypes", emptyMap()) + } + .hasFieldEqualTo("clientToken", fakeConfiguration.clientToken) + .hasFieldEqualTo("env", fakeConfiguration.env) + .hasFieldEqualTo("variant", "") + .hasFieldEqualTo( + "additionalConfig", + fakeConfiguration.additionalConfig?.filterValues { it != null }.orEmpty() + ) + assertThat(rumConfigCaptor.firstValue) + .hasFieldEqualTo("applicationId", fakeConfiguration.applicationId) + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } // endregion @@ -477,35 +580,49 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(sdkConfigCaptor.firstValue) - .hasField("coreConfig") { - it.hasFieldEqualTo("needsClearTextHttp", false) - it.hasFieldEqualTo("firstPartyHostsWithHeaderTypes", emptyMap()) - it.hasFieldEqualTo("site", DatadogSite.US1) + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } - .hasFieldEqualTo("clientToken", fakeConfiguration.clientToken) - .hasFieldEqualTo("env", fakeConfiguration.env) - .hasFieldEqualTo("variant", "") - .hasFieldEqualTo( - "additionalConfig", - fakeConfiguration.additionalConfig?.filterValues { it != null }.orEmpty() - ) - assertThat(rumConfigCaptor.firstValue) - .hasFieldEqualTo("applicationId", fakeConfiguration.applicationId) + assertThat(sdkConfigCaptor.firstValue) + .hasField("coreConfig") { + it.hasFieldEqualTo("needsClearTextHttp", false) + it.hasFieldEqualTo("firstPartyHostsWithHeaderTypes", emptyMap()) + it.hasFieldEqualTo("site", DatadogSite.US1) + } + .hasFieldEqualTo("clientToken", fakeConfiguration.clientToken) + .hasFieldEqualTo("env", fakeConfiguration.env) + .hasFieldEqualTo("variant", "") + .hasFieldEqualTo( + "additionalConfig", + fakeConfiguration.additionalConfig?.filterValues { it != null }.orEmpty() + ) + assertThat(rumConfigCaptor.firstValue) + .hasFieldEqualTo("applicationId", fakeConfiguration.applicationId) + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } @Test @@ -520,35 +637,49 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(sdkConfigCaptor.firstValue) - .hasField("coreConfig") { - it.hasFieldEqualTo("needsClearTextHttp", false) - it.hasFieldEqualTo("firstPartyHostsWithHeaderTypes", emptyMap()) - it.hasFieldEqualTo("site", DatadogSite.US1) + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } - .hasFieldEqualTo("clientToken", fakeConfiguration.clientToken) - .hasFieldEqualTo("env", fakeConfiguration.env) - .hasFieldEqualTo("variant", "") - .hasFieldEqualTo( - "additionalConfig", - fakeConfiguration.additionalConfig?.filterValues { it != null }.orEmpty() - ) - assertThat(rumConfigCaptor.firstValue) - .hasFieldEqualTo("applicationId", fakeConfiguration.applicationId) + assertThat(sdkConfigCaptor.firstValue) + .hasField("coreConfig") { + it.hasFieldEqualTo("needsClearTextHttp", false) + it.hasFieldEqualTo("firstPartyHostsWithHeaderTypes", emptyMap()) + it.hasFieldEqualTo("site", DatadogSite.US1) + } + .hasFieldEqualTo("clientToken", fakeConfiguration.clientToken) + .hasFieldEqualTo("env", fakeConfiguration.env) + .hasFieldEqualTo("variant", "") + .hasFieldEqualTo( + "additionalConfig", + fakeConfiguration.additionalConfig?.filterValues { it != null }.orEmpty() + ) + assertThat(rumConfigCaptor.firstValue) + .hasFieldEqualTo("applicationId", fakeConfiguration.applicationId) + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } @Test @@ -563,35 +694,49 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(sdkConfigCaptor.firstValue) - .hasField("coreConfig") { - it.hasFieldEqualTo("needsClearTextHttp", false) - it.hasFieldEqualTo("firstPartyHostsWithHeaderTypes", emptyMap()) - it.hasFieldEqualTo("site", DatadogSite.US3) + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } - .hasFieldEqualTo("clientToken", fakeConfiguration.clientToken) - .hasFieldEqualTo("env", fakeConfiguration.env) - .hasFieldEqualTo("variant", "") - .hasFieldEqualTo( - "additionalConfig", - fakeConfiguration.additionalConfig?.filterValues { it != null }.orEmpty() - ) - assertThat(rumConfigCaptor.firstValue) - .hasFieldEqualTo("applicationId", fakeConfiguration.applicationId) + assertThat(sdkConfigCaptor.firstValue) + .hasField("coreConfig") { + it.hasFieldEqualTo("needsClearTextHttp", false) + it.hasFieldEqualTo("firstPartyHostsWithHeaderTypes", emptyMap()) + it.hasFieldEqualTo("site", DatadogSite.US3) + } + .hasFieldEqualTo("clientToken", fakeConfiguration.clientToken) + .hasFieldEqualTo("env", fakeConfiguration.env) + .hasFieldEqualTo("variant", "") + .hasFieldEqualTo( + "additionalConfig", + fakeConfiguration.additionalConfig?.filterValues { it != null }.orEmpty() + ) + assertThat(rumConfigCaptor.firstValue) + .hasFieldEqualTo("applicationId", fakeConfiguration.applicationId) + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } @Test @@ -606,35 +751,49 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(sdkConfigCaptor.firstValue) - .hasField("coreConfig") { - it.hasFieldEqualTo("needsClearTextHttp", false) - it.hasFieldEqualTo("firstPartyHostsWithHeaderTypes", emptyMap()) - it.hasFieldEqualTo("site", DatadogSite.US5) + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } - .hasFieldEqualTo("clientToken", fakeConfiguration.clientToken) - .hasFieldEqualTo("env", fakeConfiguration.env) - .hasFieldEqualTo("variant", "") - .hasFieldEqualTo( - "additionalConfig", - fakeConfiguration.additionalConfig?.filterValues { it != null }.orEmpty() - ) - assertThat(rumConfigCaptor.firstValue) - .hasFieldEqualTo("applicationId", fakeConfiguration.applicationId) + assertThat(sdkConfigCaptor.firstValue) + .hasField("coreConfig") { + it.hasFieldEqualTo("needsClearTextHttp", false) + it.hasFieldEqualTo("firstPartyHostsWithHeaderTypes", emptyMap()) + it.hasFieldEqualTo("site", DatadogSite.US5) + } + .hasFieldEqualTo("clientToken", fakeConfiguration.clientToken) + .hasFieldEqualTo("env", fakeConfiguration.env) + .hasFieldEqualTo("variant", "") + .hasFieldEqualTo( + "additionalConfig", + fakeConfiguration.additionalConfig?.filterValues { it != null }.orEmpty() + ) + assertThat(rumConfigCaptor.firstValue) + .hasFieldEqualTo("applicationId", fakeConfiguration.applicationId) + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } @Test @@ -649,35 +808,49 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(sdkConfigCaptor.firstValue) - .hasField("coreConfig") { - it.hasFieldEqualTo("needsClearTextHttp", false) - it.hasFieldEqualTo("firstPartyHostsWithHeaderTypes", emptyMap()) - it.hasFieldEqualTo("site", DatadogSite.US1_FED) + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } - .hasFieldEqualTo("clientToken", fakeConfiguration.clientToken) - .hasFieldEqualTo("env", fakeConfiguration.env) - .hasFieldEqualTo("variant", "") - .hasFieldEqualTo( - "additionalConfig", - fakeConfiguration.additionalConfig?.filterValues { it != null }.orEmpty() - ) - assertThat(rumConfigCaptor.firstValue) - .hasFieldEqualTo("applicationId", fakeConfiguration.applicationId) + assertThat(sdkConfigCaptor.firstValue) + .hasField("coreConfig") { + it.hasFieldEqualTo("needsClearTextHttp", false) + it.hasFieldEqualTo("firstPartyHostsWithHeaderTypes", emptyMap()) + it.hasFieldEqualTo("site", DatadogSite.US1_FED) + } + .hasFieldEqualTo("clientToken", fakeConfiguration.clientToken) + .hasFieldEqualTo("env", fakeConfiguration.env) + .hasFieldEqualTo("variant", "") + .hasFieldEqualTo( + "additionalConfig", + fakeConfiguration.additionalConfig?.filterValues { it != null }.orEmpty() + ) + assertThat(rumConfigCaptor.firstValue) + .hasFieldEqualTo("applicationId", fakeConfiguration.applicationId) + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } @Test @@ -692,35 +865,49 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(sdkConfigCaptor.firstValue) - .hasField("coreConfig") { - it.hasFieldEqualTo("needsClearTextHttp", false) - it.hasFieldEqualTo("firstPartyHostsWithHeaderTypes", emptyMap()) - it.hasFieldEqualTo("site", DatadogSite.EU1) + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } - .hasFieldEqualTo("clientToken", fakeConfiguration.clientToken) - .hasFieldEqualTo("env", fakeConfiguration.env) - .hasFieldEqualTo("variant", "") - .hasFieldEqualTo( - "additionalConfig", - fakeConfiguration.additionalConfig?.filterValues { it != null }.orEmpty() - ) - assertThat(rumConfigCaptor.firstValue) - .hasFieldEqualTo("applicationId", fakeConfiguration.applicationId) + assertThat(sdkConfigCaptor.firstValue) + .hasField("coreConfig") { + it.hasFieldEqualTo("needsClearTextHttp", false) + it.hasFieldEqualTo("firstPartyHostsWithHeaderTypes", emptyMap()) + it.hasFieldEqualTo("site", DatadogSite.EU1) + } + .hasFieldEqualTo("clientToken", fakeConfiguration.clientToken) + .hasFieldEqualTo("env", fakeConfiguration.env) + .hasFieldEqualTo("variant", "") + .hasFieldEqualTo( + "additionalConfig", + fakeConfiguration.additionalConfig?.filterValues { it != null }.orEmpty() + ) + assertThat(rumConfigCaptor.firstValue) + .hasFieldEqualTo("applicationId", fakeConfiguration.applicationId) + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } @Test @@ -735,35 +922,49 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(sdkConfigCaptor.firstValue) - .hasField("coreConfig") { - it.hasFieldEqualTo("needsClearTextHttp", false) - it.hasFieldEqualTo("firstPartyHostsWithHeaderTypes", emptyMap()) - it.hasFieldEqualTo("site", DatadogSite.AP1) + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } - .hasFieldEqualTo("clientToken", fakeConfiguration.clientToken) - .hasFieldEqualTo("env", fakeConfiguration.env) - .hasFieldEqualTo("variant", "") - .hasFieldEqualTo( - "additionalConfig", - fakeConfiguration.additionalConfig?.filterValues { it != null }.orEmpty() - ) - assertThat(rumConfigCaptor.firstValue) - .hasFieldEqualTo("applicationId", fakeConfiguration.applicationId) + assertThat(sdkConfigCaptor.firstValue) + .hasField("coreConfig") { + it.hasFieldEqualTo("needsClearTextHttp", false) + it.hasFieldEqualTo("firstPartyHostsWithHeaderTypes", emptyMap()) + it.hasFieldEqualTo("site", DatadogSite.AP1) + } + .hasFieldEqualTo("clientToken", fakeConfiguration.clientToken) + .hasFieldEqualTo("env", fakeConfiguration.env) + .hasFieldEqualTo("variant", "") + .hasFieldEqualTo( + "additionalConfig", + fakeConfiguration.additionalConfig?.filterValues { it != null }.orEmpty() + ) + assertThat(rumConfigCaptor.firstValue) + .hasFieldEqualTo("applicationId", fakeConfiguration.applicationId) + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } @Test @@ -778,35 +979,49 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(sdkConfigCaptor.firstValue) - .hasField("coreConfig") { - it.hasFieldEqualTo("needsClearTextHttp", false) - it.hasFieldEqualTo("firstPartyHostsWithHeaderTypes", emptyMap()) - it.hasFieldEqualTo("site", DatadogSite.AP2) + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } - .hasFieldEqualTo("clientToken", fakeConfiguration.clientToken) - .hasFieldEqualTo("env", fakeConfiguration.env) - .hasFieldEqualTo("variant", "") - .hasFieldEqualTo( - "additionalConfig", - fakeConfiguration.additionalConfig?.filterValues { it != null }.orEmpty() - ) - assertThat(rumConfigCaptor.firstValue) - .hasFieldEqualTo("applicationId", fakeConfiguration.applicationId) + assertThat(sdkConfigCaptor.firstValue) + .hasField("coreConfig") { + it.hasFieldEqualTo("needsClearTextHttp", false) + it.hasFieldEqualTo("firstPartyHostsWithHeaderTypes", emptyMap()) + it.hasFieldEqualTo("site", DatadogSite.AP2) + } + .hasFieldEqualTo("clientToken", fakeConfiguration.clientToken) + .hasFieldEqualTo("env", fakeConfiguration.env) + .hasFieldEqualTo("variant", "") + .hasFieldEqualTo( + "additionalConfig", + fakeConfiguration.additionalConfig?.filterValues { it != null }.orEmpty() + ) + assertThat(rumConfigCaptor.firstValue) + .hasFieldEqualTo("applicationId", fakeConfiguration.applicationId) + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } // endregion @@ -822,19 +1037,33 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - eq(TrackingConsent.PENDING) - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + eq(TrackingConsent.PENDING) + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } + } + } finally { + rumMock.close() + logsMock.close() + traceMock.close() } } @@ -850,19 +1079,33 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - eq(TrackingConsent.PENDING) - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + eq(TrackingConsent.PENDING) + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } + } + } finally { + rumMock.close() + logsMock.close() + traceMock.close() } } @@ -878,19 +1121,33 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - eq(TrackingConsent.GRANTED) - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + eq(TrackingConsent.GRANTED) + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } + } + } finally { + rumMock.close() + logsMock.close() + traceMock.close() } } @@ -906,19 +1163,33 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - eq(TrackingConsent.NOT_GRANTED) - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + eq(TrackingConsent.NOT_GRANTED) + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } + } + } finally { + rumMock.close() + logsMock.close() + traceMock.close() } } @@ -937,24 +1208,38 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(rumConfigCaptor.firstValue) - .hasField("featureConfiguration") { - it.hasFieldEqualTo("viewTrackingStrategy", NoOpViewTrackingStrategy) + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } + assertThat(rumConfigCaptor.firstValue) + .hasField("featureConfiguration") { + it.hasFieldEqualTo("viewTrackingStrategy", NoOpViewTrackingStrategy) + } + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } @Test @@ -970,24 +1255,38 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(rumConfigCaptor.firstValue) - .hasField("featureConfiguration") { - it.hasFieldEqualTo("viewTrackingStrategy", ActivityViewTrackingStrategy(false)) + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } + assertThat(rumConfigCaptor.firstValue) + .hasField("featureConfiguration") { + it.hasFieldEqualTo("viewTrackingStrategy", ActivityViewTrackingStrategy(false)) + } + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } @Test @@ -1003,24 +1302,38 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(rumConfigCaptor.firstValue) - .hasField("featureConfiguration") { - it.hasFieldEqualTo("userActionTracking", false) + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } + assertThat(rumConfigCaptor.firstValue) + .hasField("featureConfiguration") { + it.hasFieldEqualTo("userActionTracking", false) + } + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } @Test @@ -1036,24 +1349,38 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(rumConfigCaptor.firstValue) - .hasField("featureConfiguration") { - it.hasFieldEqualTo("trackFrustrations", true) + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } + assertThat(rumConfigCaptor.firstValue) + .hasField("featureConfiguration") { + it.hasFieldEqualTo("trackFrustrations", true) + } + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } @Test @@ -1064,29 +1391,44 @@ internal class DdSdkTest { val bridgeConfiguration = configuration.copy( trackFrustrations = false ) + val sdkConfigCaptor = argumentCaptor() val rumConfigCaptor = argumentCaptor() val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(rumConfigCaptor.firstValue) - .hasField("featureConfiguration") { - it.hasFieldEqualTo("trackFrustrations", false) + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } + assertThat(rumConfigCaptor.firstValue) + .hasField("featureConfiguration") { + it.hasFieldEqualTo("trackFrustrations", false) + } + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } @Test @@ -1102,24 +1444,39 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(rumConfigCaptor.firstValue) - .hasField("featureConfiguration") { - it.hasFieldEqualTo("userActionTracking", true) + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } + + assertThat(rumConfigCaptor.firstValue) + .hasField("featureConfiguration") { + it.hasFieldEqualTo("userActionTracking", true) + } + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } @Test @@ -1177,35 +1534,49 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(sdkConfigCaptor.firstValue) - .hasField("coreConfig") { - it.hasFieldEqualTo("needsClearTextHttp", false) - it.hasFieldEqualTo("firstPartyHostsWithHeaderTypes", emptyMap()) + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } - .hasFieldEqualTo("clientToken", bridgeConfiguration.clientToken) - .hasFieldEqualTo("env", bridgeConfiguration.env) - .hasFieldEqualTo("variant", "") - .hasFieldEqualTo("service", serviceName) - .hasFieldEqualTo( - "additionalConfig", - bridgeConfiguration.additionalConfig?.filterValues { it != null }.orEmpty() - ) - assertThat(rumConfigCaptor.firstValue) - .hasFieldEqualTo("applicationId", bridgeConfiguration.applicationId) + assertThat(sdkConfigCaptor.firstValue) + .hasField("coreConfig") { + it.hasFieldEqualTo("needsClearTextHttp", false) + it.hasFieldEqualTo("firstPartyHostsWithHeaderTypes", emptyMap()) + } + .hasFieldEqualTo("clientToken", bridgeConfiguration.clientToken) + .hasFieldEqualTo("env", bridgeConfiguration.env) + .hasFieldEqualTo("variant", "") + .hasFieldEqualTo("service", serviceName) + .hasFieldEqualTo( + "additionalConfig", + bridgeConfiguration.additionalConfig?.filterValues { it != null }.orEmpty() + ) + assertThat(rumConfigCaptor.firstValue) + .hasFieldEqualTo("applicationId", bridgeConfiguration.applicationId) + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } @Test @@ -1224,31 +1595,45 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(rumConfigCaptor.firstValue) - .hasField("featureConfiguration") { rumConfig -> - rumConfig.hasField("longTaskTrackingStrategy") { longTaskTrackingStrategy -> - longTaskTrackingStrategy - .isInstanceOf( - "com.datadog.android.rum.internal.instrumentation." + - "MainLooperLongTaskStrategy" - ) - .hasFieldEqualTo("thresholdMs", threshold.toLong()) - } + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } + assertThat(rumConfigCaptor.firstValue) + .hasField("featureConfiguration") { rumConfig -> + rumConfig.hasField("longTaskTrackingStrategy") { longTaskTrackingStrategy -> + longTaskTrackingStrategy + .isInstanceOf( + "com.datadog.android.rum.internal.instrumentation." + + "MainLooperLongTaskStrategy" + ) + .hasFieldEqualTo("thresholdMs", threshold.toLong()) + } + } + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } @Test @@ -1265,24 +1650,38 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(rumConfigCaptor.firstValue) - .hasField("featureConfiguration") { rumConfig -> - rumConfig.doesNotHaveField("longTaskTrackingStrategy") + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } + assertThat(rumConfigCaptor.firstValue) + .hasField("featureConfiguration") { rumConfig -> + rumConfig.doesNotHaveField("longTaskTrackingStrategy") + } + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } @Test @@ -1326,27 +1725,41 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(sdkConfigCaptor.firstValue) - .hasField("coreConfig") { coreConfig -> - coreConfig.hasFieldEqualTo( - "firstPartyHostsWithHeaderTypes", - tracingHosts + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } + assertThat(sdkConfigCaptor.firstValue) + .hasField("coreConfig") { coreConfig -> + coreConfig.hasFieldEqualTo( + "firstPartyHostsWithHeaderTypes", + tracingHosts + ) + } + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } @Test @@ -1385,27 +1798,41 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(sdkConfigCaptor.firstValue) - .hasField("coreConfig") { coreConfig -> - coreConfig.hasFieldEqualTo( - "firstPartyHostsWithHeaderTypes", - tracingHosts + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } + assertThat(sdkConfigCaptor.firstValue) + .hasField("coreConfig") { coreConfig -> + coreConfig.hasFieldEqualTo( + "firstPartyHostsWithHeaderTypes", + tracingHosts + ) + } + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } @Test @@ -1451,27 +1878,41 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(sdkConfigCaptor.firstValue) - .hasField("coreConfig") { coreConfig -> - coreConfig.hasFieldEqualTo( - "firstPartyHostsWithHeaderTypes", - tracingHosts + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } + assertThat(sdkConfigCaptor.firstValue) + .hasField("coreConfig") { coreConfig -> + coreConfig.hasFieldEqualTo( + "firstPartyHostsWithHeaderTypes", + tracingHosts + ) + } + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } @ParameterizedTest @@ -1490,27 +1931,41 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(sdkConfigCaptor.firstValue) - .hasField("coreConfig") { coreConfig -> - coreConfig.hasFieldEqualTo( - "uploadFrequency", - expectedUploadFrequency + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } + assertThat(sdkConfigCaptor.firstValue) + .hasField("coreConfig") { coreConfig -> + coreConfig.hasFieldEqualTo( + "uploadFrequency", + expectedUploadFrequency + ) + } + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } @ParameterizedTest @@ -1529,27 +1984,41 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(sdkConfigCaptor.firstValue) - .hasField("coreConfig") { coreConfig -> - coreConfig.hasFieldEqualTo( - "batchSize", - expectedBatchSize + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } + assertThat(sdkConfigCaptor.firstValue) + .hasField("coreConfig") { coreConfig -> + coreConfig.hasFieldEqualTo( + "batchSize", + expectedBatchSize + ) + } + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } @ParameterizedTest @@ -1568,27 +2037,41 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(sdkConfigCaptor.firstValue) - .hasField("coreConfig") { coreConfig -> - coreConfig.hasFieldEqualTo( - "batchProcessingLevel", - expectedBatchSize + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } + assertThat(sdkConfigCaptor.firstValue) + .hasField("coreConfig") { coreConfig -> + coreConfig.hasFieldEqualTo( + "batchProcessingLevel", + expectedBatchSize + ) + } + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } @Test @@ -1606,24 +2089,38 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(rumConfigCaptor.firstValue) - .hasField("featureConfiguration") { - it.hasFieldEqualTo("backgroundEventTracking", trackBackgroundEvents ?: false) + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } + assertThat(rumConfigCaptor.firstValue) + .hasField("featureConfiguration") { + it.hasFieldEqualTo("backgroundEventTracking", trackBackgroundEvents ?: false) + } + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } @Test @@ -1639,28 +2136,42 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(rumConfigCaptor.firstValue) - .hasField("featureConfiguration") { - it.hasFieldEqualTo("vitalsMonitorUpdateFrequency", VitalsUpdateFrequency.RARE) + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } + assertThat(rumConfigCaptor.firstValue) + .hasField("featureConfiguration") { + it.hasFieldEqualTo("vitalsMonitorUpdateFrequency", VitalsUpdateFrequency.RARE) + } - argumentCaptor { - verify(mockChoreographer).postFrameCallback(capture()) - assertThat(firstValue).isInstanceOf(FpsFrameCallback::class.java) + argumentCaptor { + verify(mockChoreographer).postFrameCallback(capture()) + assertThat(firstValue).isInstanceOf(FpsFrameCallback::class.java) + } + } finally { + rumMock.close() + logsMock.close() + traceMock.close() } } @@ -1679,25 +2190,37 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(rumConfigCaptor.firstValue) - .hasField("featureConfiguration") { - it.hasFieldEqualTo("vitalsMonitorUpdateFrequency", VitalsUpdateFrequency.NEVER) + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } // When + testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } - verifyNoInteractions(mockChoreographer) + assertThat(rumConfigCaptor.firstValue) + .hasField("featureConfiguration") { + it.hasFieldEqualTo("vitalsMonitorUpdateFrequency", VitalsUpdateFrequency.NEVER) + } + verifyNoInteractions(mockChoreographer) + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } @Test @@ -1719,41 +2242,56 @@ internal class DdSdkTest { val traceConfigCaptor = argumentCaptor() val frameDurationNs = threshold + frameDurationOverThreshold - // When - testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) - - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(rumConfigCaptor.firstValue) - .hasField("featureConfiguration") { - it.hasFieldEqualTo("vitalsMonitorUpdateFrequency", VitalsUpdateFrequency.AVERAGE) - } - argumentCaptor { - verify(mockChoreographer).postFrameCallback(capture()) - assertThat(firstValue).isInstanceOf(FpsFrameCallback::class.java) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // When - firstValue.doFrame(timestampNs) - firstValue.doFrame(timestampNs + frameDurationNs) + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } // When + testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) - // then - verify(mockRumMonitor._getInternal()!!).updatePerformanceMetric( - RumPerformanceMetric.JS_FRAME_TIME, - frameDurationNs.toDouble() - ) - verify(mockRumMonitor._getInternal()!!, never()).addLongTask( - frameDurationNs, - "javascript" - ) + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } + } + assertThat(rumConfigCaptor.firstValue) + .hasField("featureConfiguration") { + it.hasFieldEqualTo( + "vitalsMonitorUpdateFrequency", + VitalsUpdateFrequency.AVERAGE + ) + } + argumentCaptor { + verify(mockChoreographer).postFrameCallback(capture()) + assertThat(firstValue).isInstanceOf(FpsFrameCallback::class.java) + + // When + firstValue.doFrame(timestampNs) + firstValue.doFrame(timestampNs + frameDurationNs) + + // then + verify(mockRumMonitor._getInternal()!!).updatePerformanceMetric( + RumPerformanceMetric.JS_FRAME_TIME, + frameDurationNs.toDouble() + ) + verify(mockRumMonitor._getInternal()!!, never()).addLongTask( + frameDurationNs, + "javascript" + ) + } + } finally { + rumMock.close() + logsMock.close() + traceMock.close() } } @@ -1846,25 +2384,37 @@ internal class DdSdkTest { val traceConfigCaptor = argumentCaptor() val defaultTimeBasedIdentifier = TimeBasedInitialResourceIdentifier(100) - // When - testedBridgeSdk.initialize(configuration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } // When + testedBridgeSdk.initialize(configuration.toReadableJavaOnlyMap(), mockPromise) - assertThat(rumConfigCaptor.firstValue) - .hasField("featureConfiguration") { - it.hasFieldEqualTo("initialResourceIdentifier", defaultTimeBasedIdentifier) + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } + + assertThat(rumConfigCaptor.firstValue) + .hasField("featureConfiguration") { + it.hasFieldEqualTo("initialResourceIdentifier", defaultTimeBasedIdentifier) + } + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } @Test @@ -1884,25 +2434,37 @@ internal class DdSdkTest { thresholdInSeconds.seconds.inWholeMilliseconds ) - // When - testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } // When + testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) - assertThat(rumConfigCaptor.firstValue) - .hasField("featureConfiguration") { - it.hasFieldEqualTo("initialResourceIdentifier", timeBasedIdentifier) + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } + + assertThat(rumConfigCaptor.firstValue) + .hasField("featureConfiguration") { + it.hasFieldEqualTo("initialResourceIdentifier", timeBasedIdentifier) + } + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } // endregion @@ -1925,28 +2487,42 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(sdkConfigCaptor.firstValue) - .hasFieldEqualTo( - "additionalConfig", - mapOf( - DdSdkImplementation.DD_VERSION_SUFFIX to versionSuffix, - DdSdkImplementation.DD_VERSION to mockPackageInfo.versionName + versionSuffix + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } // When + testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() ) - ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } + } + assertThat(sdkConfigCaptor.firstValue) + .hasFieldEqualTo( + "additionalConfig", + mapOf( + DdSdkImplementation.DD_VERSION_SUFFIX to versionSuffix, + DdSdkImplementation.DD_VERSION to ( + mockPackageInfo.versionName + versionSuffix + ) + ) + ) + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } // endregion @@ -1985,47 +2561,59 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(rumConfigCaptor.firstValue) - .hasField("featureConfiguration") { - val configurationMapper = it - .getActualValue>( - "telemetryConfigurationMapper" - ) - val result = configurationMapper.map(telemetryConfigurationEvent)!! - assertThat(result.telemetry.configuration.trackNativeErrors!!).isEqualTo( - trackNativeErrors - ) - assertThat(result.telemetry.configuration.trackCrossPlatformLongTasks!!) - .isEqualTo(false) - assertThat(result.telemetry.configuration.trackLongTask!!) - .isEqualTo(false) - assertThat(result.telemetry.configuration.trackNativeLongTasks!!) - .isEqualTo(false) - - assertThat(result.telemetry.configuration.initializationType!!) - .isEqualTo(initializationType) - assertThat(result.telemetry.configuration.trackInteractions!!) - .isEqualTo(trackInteractions) - assertThat(result.telemetry.configuration.trackErrors!!).isEqualTo(trackErrors) - assertThat(result.telemetry.configuration.trackResources!!) - .isEqualTo(trackNetworkRequests) - assertThat(result.telemetry.configuration.trackNetworkRequests!!) - .isEqualTo(trackNetworkRequests) + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } // When + testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } + assertThat(rumConfigCaptor.firstValue) + .hasField("featureConfiguration") { + val configurationMapper = it + .getActualValue>( + "telemetryConfigurationMapper" + ) + val result = configurationMapper.map(telemetryConfigurationEvent)!! + assertThat(result.telemetry.configuration.trackNativeErrors!!).isEqualTo( + trackNativeErrors + ) + assertThat(result.telemetry.configuration.trackCrossPlatformLongTasks!!) + .isEqualTo(false) + assertThat(result.telemetry.configuration.trackLongTask!!) + .isEqualTo(false) + assertThat(result.telemetry.configuration.trackNativeLongTasks!!) + .isEqualTo(false) + + assertThat(result.telemetry.configuration.initializationType!!) + .isEqualTo(initializationType) + assertThat(result.telemetry.configuration.trackInteractions!!) + .isEqualTo(trackInteractions) + assertThat(result.telemetry.configuration.trackErrors!!).isEqualTo(trackErrors) + assertThat(result.telemetry.configuration.trackResources!!) + .isEqualTo(trackNetworkRequests) + assertThat(result.telemetry.configuration.trackNetworkRequests!!) + .isEqualTo(trackNetworkRequests) + } + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } // endregion @@ -2042,27 +2630,39 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(rumConfigCaptor.firstValue) - .hasField("featureConfiguration") { - val resourceMapper = it - .getActualValue>("resourceEventMapper") - val notDroppedEvent = resourceMapper.map(resourceEvent) - assertThat(notDroppedEvent).isNotNull + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } // When + testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } + assertThat(rumConfigCaptor.firstValue) + .hasField("featureConfiguration") { + val resourceMapper = it + .getActualValue>("resourceEventMapper") + val notDroppedEvent = resourceMapper.map(resourceEvent) + assertThat(notDroppedEvent).isNotNull + } + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } @Test @@ -2076,27 +2676,39 @@ internal class DdSdkTest { val traceConfigCaptor = argumentCaptor() resourceEvent.context?.additionalProperties?.put("_dd.resource.drop_resource", true) - // When - testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(rumConfigCaptor.firstValue) - .hasField("featureConfiguration") { - val resourceMapper = it - .getActualValue>("resourceEventMapper") - val droppedEvent = resourceMapper.map(resourceEvent) - assertThat(droppedEvent).isNull() + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } // When + testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } + assertThat(rumConfigCaptor.firstValue) + .hasField("featureConfiguration") { + val resourceMapper = it + .getActualValue>("resourceEventMapper") + val droppedEvent = resourceMapper.map(resourceEvent) + assertThat(droppedEvent).isNull() + } + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } // endregion @@ -2113,27 +2725,39 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(rumConfigCaptor.firstValue) - .hasField("featureConfiguration") { - val actionMapper = it - .getActualValue>("actionEventMapper") - val notDroppedEvent = actionMapper.map(actionEvent) - assertThat(notDroppedEvent).isNotNull + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } // When + testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } + assertThat(rumConfigCaptor.firstValue) + .hasField("featureConfiguration") { + val actionMapper = it + .getActualValue>("actionEventMapper") + val notDroppedEvent = actionMapper.map(actionEvent) + assertThat(notDroppedEvent).isNotNull + } + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } @Test @@ -2147,27 +2771,39 @@ internal class DdSdkTest { val traceConfigCaptor = argumentCaptor() actionEvent.context?.additionalProperties?.put("_dd.action.drop_action", true) - // When - testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(rumConfigCaptor.firstValue) - .hasField("featureConfiguration") { - val actionMapper = it - .getActualValue>("actionEventMapper") - val droppedEvent = actionMapper.map(actionEvent) - assertThat(droppedEvent).isNull() + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } // When + testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } + assertThat(rumConfigCaptor.firstValue) + .hasField("featureConfiguration") { + val actionMapper = it + .getActualValue>("actionEventMapper") + val droppedEvent = actionMapper.map(actionEvent) + assertThat(droppedEvent).isNull() + } + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } // endregion @@ -2580,24 +3216,36 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } // When + testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) - assertThat(rumConfigCaptor.firstValue) - .hasField("featureConfiguration") { - it.hasFieldEqualTo("customEndpointUrl", customRumEndpoint) + // Then + inOrder(mockDatadog) { + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } - assertThat(logsConfigCaptor.firstValue) - .hasFieldEqualTo("customEndpointUrl", customLogsEndpoint) - assertThat(traceConfigCaptor.firstValue) - .hasFieldEqualTo("customEndpointUrl", customTraceEndpoint) + + assertThat(rumConfigCaptor.firstValue) + .hasField("featureConfiguration") { + it.hasFieldEqualTo("customEndpointUrl", customRumEndpoint) + } + assertThat(logsConfigCaptor.firstValue) + .hasFieldEqualTo("customEndpointUrl", customLogsEndpoint) + assertThat(traceConfigCaptor.firstValue) + .hasFieldEqualTo("customEndpointUrl", customTraceEndpoint) + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } @Test diff --git a/packages/internal-testing-tools/android/src/main/kotlin/com/datadog/reactnative/internaltesting/DdInternalTestingImplementation.kt b/packages/internal-testing-tools/android/src/main/kotlin/com/datadog/reactnative/internaltesting/DdInternalTestingImplementation.kt index df0030fb5..b33bef6de 100644 --- a/packages/internal-testing-tools/android/src/main/kotlin/com/datadog/reactnative/internaltesting/DdInternalTestingImplementation.kt +++ b/packages/internal-testing-tools/android/src/main/kotlin/com/datadog/reactnative/internaltesting/DdInternalTestingImplementation.kt @@ -58,6 +58,12 @@ class DdInternalTestingImplementation { promise.resolve(null) } + /** + * Get wrapped core instance. + */ + internal fun getWrappedCore(): StubSDKCore? { + return wrappedCore + } internal companion object { internal const val NAME = "DdInternalTesting" diff --git a/packages/internal-testing-tools/android/src/test/kotlin/com/datadog/reactnative/internaltesting/DdInternalTestingImplementationTest.kt b/packages/internal-testing-tools/android/src/test/kotlin/com/datadog/reactnative/internaltesting/DdInternalTestingImplementationTest.kt index c542ed6bc..4a6938f9b 100644 --- a/packages/internal-testing-tools/android/src/test/kotlin/com/datadog/reactnative/internaltesting/DdInternalTestingImplementationTest.kt +++ b/packages/internal-testing-tools/android/src/test/kotlin/com/datadog/reactnative/internaltesting/DdInternalTestingImplementationTest.kt @@ -80,7 +80,9 @@ internal class DdInternalTestingImplementationTest { // Simulating DdSdkImplementation initialization DatadogSDKWrapperStorage.notifyOnInitializedListeners(mockCore) - val wrappedCore = Datadog.getInstance() as StubSDKCore + val wrappedCore = testedInternalTesting.getWrappedCore() + requireNotNull(wrappedCore) + wrappedCore.registerFeature(mockFeature) requireNotNull(wrappedCore.getFeature(mockFeature.name)) .withWriteContext { _, eventBatchWriter -> From a45ae9c9d85427b1f23c167dc722053b9f39aef2 Mon Sep 17 00:00:00 2001 From: Sergio Barrio Date: Fri, 5 Sep 2025 15:29:37 +0200 Subject: [PATCH 031/410] Remove type interdependencies between modules --- packages/core/src/{rum => }/DdAttributes.ts | 0 .../src/DdSdkReactNativeConfiguration.tsx | 2 +- .../src/__tests__/DdSdkReactNative.test.tsx | 3 +- packages/core/src/index.tsx | 3 +- packages/core/src/logs/DdLogs.ts | 5 +- .../core/src/logs/__tests__/DdLogs.test.ts | 4 +- .../src/logs/__tests__/eventMapper.test.ts | 2 +- packages/core/src/logs/eventMapper.ts | 3 +- packages/core/src/logs/types.ts | 21 +------- packages/core/src/rum/DdRum.ts | 4 +- packages/core/src/rum/__tests__/DdRum.test.ts | 3 +- .../src/rum/eventMappers/errorEventMapper.ts | 2 +- .../instrumentation/DdRumErrorTracking.tsx | 2 +- packages/core/src/rum/types.ts | 10 +--- packages/core/src/types.tsx | 49 ++++++++++++++++--- 15 files changed, 62 insertions(+), 51 deletions(-) rename packages/core/src/{rum => }/DdAttributes.ts (100%) diff --git a/packages/core/src/rum/DdAttributes.ts b/packages/core/src/DdAttributes.ts similarity index 100% rename from packages/core/src/rum/DdAttributes.ts rename to packages/core/src/DdAttributes.ts diff --git a/packages/core/src/DdSdkReactNativeConfiguration.tsx b/packages/core/src/DdSdkReactNativeConfiguration.tsx index 9be08fa28..44debb2d2 100644 --- a/packages/core/src/DdSdkReactNativeConfiguration.tsx +++ b/packages/core/src/DdSdkReactNativeConfiguration.tsx @@ -7,12 +7,12 @@ import type { ProxyConfiguration } from './ProxyConfiguration'; import type { SdkVerbosity } from './SdkVerbosity'; import { TrackingConsent } from './TrackingConsent'; -import type { LogEventMapper } from './logs/types'; import type { ActionEventMapper } from './rum/eventMappers/actionEventMapper'; import type { ErrorEventMapper } from './rum/eventMappers/errorEventMapper'; import type { ResourceEventMapper } from './rum/eventMappers/resourceEventMapper'; import type { FirstPartyHost } from './rum/types'; import { PropagatorType } from './rum/types'; +import type { LogEventMapper } from './types'; export enum VitalsUpdateFrequency { FREQUENT = 'FREQUENT', diff --git a/packages/core/src/__tests__/DdSdkReactNative.test.tsx b/packages/core/src/__tests__/DdSdkReactNative.test.tsx index 49c0bd1f2..18bf060ce 100644 --- a/packages/core/src/__tests__/DdSdkReactNative.test.tsx +++ b/packages/core/src/__tests__/DdSdkReactNative.test.tsx @@ -17,11 +17,12 @@ import { DdRum } from '../rum/DdRum'; import { DdRumErrorTracking } from '../rum/instrumentation/DdRumErrorTracking'; import { DdRumUserInteractionTracking } from '../rum/instrumentation/interactionTracking/DdRumUserInteractionTracking'; import { DdRumResourceTracking } from '../rum/instrumentation/resourceTracking/DdRumResourceTracking'; -import { ErrorSource, PropagatorType, RumActionType } from '../rum/types'; +import { PropagatorType, RumActionType } from '../rum/types'; import { AttributesSingleton } from '../sdk/AttributesSingleton/AttributesSingleton'; import { DdSdk } from '../sdk/DdSdk'; import { GlobalState } from '../sdk/GlobalState/GlobalState'; import { UserInfoSingleton } from '../sdk/UserInfoSingleton/UserInfoSingleton'; +import { ErrorSource } from '../types'; import type { DdSdkConfiguration } from '../types'; import { version as sdkVersion } from '../version'; diff --git a/packages/core/src/index.tsx b/packages/core/src/index.tsx index 9332354dc..062fecc90 100644 --- a/packages/core/src/index.tsx +++ b/packages/core/src/index.tsx @@ -37,11 +37,12 @@ import { DATADOG_GRAPH_QL_VARIABLES_HEADER } from './rum/instrumentation/resourceTracking/graphql/graphqlHeaders'; import type { FirstPartyHost } from './rum/types'; -import { ErrorSource, PropagatorType, RumActionType } from './rum/types'; +import { PropagatorType, RumActionType } from './rum/types'; import { DatadogProvider } from './sdk/DatadogProvider/DatadogProvider'; import { DdSdk } from './sdk/DdSdk'; import { FileBasedConfiguration } from './sdk/FileBasedConfiguration/FileBasedConfiguration'; import { DdTrace } from './trace/DdTrace'; +import { ErrorSource } from './types'; import { DefaultTimeProvider } from './utils/time-provider/DefaultTimeProvider'; import type { Timestamp } from './utils/time-provider/TimeProvider'; import { TimeProvider } from './utils/time-provider/TimeProvider'; diff --git a/packages/core/src/logs/DdLogs.ts b/packages/core/src/logs/DdLogs.ts index e00e4fc0d..e1936d211 100644 --- a/packages/core/src/logs/DdLogs.ts +++ b/packages/core/src/logs/DdLogs.ts @@ -4,11 +4,11 @@ * Copyright 2016-Present Datadog, Inc. */ +import { DdAttributes } from '../DdAttributes'; import { DATADOG_MESSAGE_PREFIX, InternalLog } from '../InternalLog'; import { SdkVerbosity } from '../SdkVerbosity'; import type { DdNativeLogsType } from '../nativeModulesTypes'; -import { DdAttributes } from '../rum/DdAttributes'; -import type { ErrorSource } from '../rum/types'; +import type { ErrorSource, LogEventMapper } from '../types'; import { validateContext } from '../utils/argsUtils'; import { getGlobalInstance } from '../utils/singletonUtils'; @@ -16,7 +16,6 @@ import { generateEventMapper } from './eventMapper'; import type { DdLogsType, LogArguments, - LogEventMapper, LogWithErrorArguments, NativeLogWithError, RawLogWithError diff --git a/packages/core/src/logs/__tests__/DdLogs.test.ts b/packages/core/src/logs/__tests__/DdLogs.test.ts index 6a530ec36..f7e848a0b 100644 --- a/packages/core/src/logs/__tests__/DdLogs.test.ts +++ b/packages/core/src/logs/__tests__/DdLogs.test.ts @@ -11,9 +11,9 @@ import { DdSdkReactNative } from '../../DdSdkReactNative'; import { InternalLog } from '../../InternalLog'; import { SdkVerbosity } from '../../SdkVerbosity'; import type { DdNativeLogsType } from '../../nativeModulesTypes'; -import { ErrorSource } from '../../rum/types'; +import { ErrorSource } from '../../types'; +import type { LogEventMapper } from '../../types'; import { DdLogs } from '../DdLogs'; -import type { LogEventMapper } from '../types'; jest.mock('../../InternalLog', () => { return { diff --git a/packages/core/src/logs/__tests__/eventMapper.test.ts b/packages/core/src/logs/__tests__/eventMapper.test.ts index 0999a6058..cd505f811 100644 --- a/packages/core/src/logs/__tests__/eventMapper.test.ts +++ b/packages/core/src/logs/__tests__/eventMapper.test.ts @@ -5,7 +5,7 @@ */ /* eslint-disable @typescript-eslint/ban-ts-comment */ -import { ErrorSource } from '../../rum/types'; +import { ErrorSource } from '../../types'; import { formatRawLogToLogEvent } from '../eventMapper'; describe('formatRawLogToLogEvent', () => { diff --git a/packages/core/src/logs/eventMapper.ts b/packages/core/src/logs/eventMapper.ts index 2bbd398cb..eb7b5f22c 100644 --- a/packages/core/src/logs/eventMapper.ts +++ b/packages/core/src/logs/eventMapper.ts @@ -7,10 +7,9 @@ import type { Attributes } from '../sdk/AttributesSingleton/types'; import { EventMapper } from '../sdk/EventMappers/EventMapper'; import type { UserInfo } from '../sdk/UserInfoSingleton/types'; +import type { LogEvent, LogEventMapper } from '../types'; import type { - LogEvent, - LogEventMapper, NativeLog, NativeLogWithError, RawLog, diff --git a/packages/core/src/logs/types.ts b/packages/core/src/logs/types.ts index 9c1b3cb09..18cdbc533 100644 --- a/packages/core/src/logs/types.ts +++ b/packages/core/src/logs/types.ts @@ -4,8 +4,7 @@ * Copyright 2016-Present Datadog, Inc. */ -import type { ErrorSource } from '../rum/types'; -import type { UserInfo } from '../sdk/UserInfoSingleton/types'; +import type { LogStatus, ErrorSource } from '../types'; /** * The entry point to use Datadog's Logs feature. @@ -75,24 +74,6 @@ export type NativeLogWithError = { fingerprint?: string; }; -export type LogStatus = 'debug' | 'info' | 'warn' | 'error'; - -export type LogEvent = { - message: string; - context: object; - errorKind?: string; - errorMessage?: string; - stacktrace?: string; - fingerprint?: string; - readonly source?: ErrorSource; - // readonly date: number; // TODO: RUMM-2446 & RUMM-2447 - readonly status: LogStatus; - readonly userInfo: UserInfo; - readonly attributes?: object; -}; - -export type LogEventMapper = (logEvent: LogEvent) => LogEvent | null; - export type LogArguments = [message: string, context?: object]; export type LogWithErrorArguments = [ diff --git a/packages/core/src/rum/DdRum.ts b/packages/core/src/rum/DdRum.ts index 45cafca5b..908404fc8 100644 --- a/packages/core/src/rum/DdRum.ts +++ b/packages/core/src/rum/DdRum.ts @@ -5,19 +5,20 @@ */ import type { GestureResponderEvent } from 'react-native'; +import { DdAttributes } from '../DdAttributes'; import { InternalLog } from '../InternalLog'; import { SdkVerbosity } from '../SdkVerbosity'; import type { DdNativeRumType } from '../nativeModulesTypes'; import { bufferVoidNativeCall } from '../sdk/DatadogProvider/Buffer/bufferNativeCall'; import { DdSdk } from '../sdk/DdSdk'; import { GlobalState } from '../sdk/GlobalState/GlobalState'; +import type { ErrorSource } from '../types'; import { validateContext } from '../utils/argsUtils'; import { getErrorContext } from '../utils/errorUtils'; import { getGlobalInstance } from '../utils/singletonUtils'; import { DefaultTimeProvider } from '../utils/time-provider/DefaultTimeProvider'; import type { TimeProvider } from '../utils/time-provider/TimeProvider'; -import { DdAttributes } from './DdAttributes'; import { generateActionEventMapper } from './eventMappers/actionEventMapper'; import type { ActionEventMapper } from './eventMappers/actionEventMapper'; import { generateErrorEventMapper } from './eventMappers/errorEventMapper'; @@ -36,7 +37,6 @@ import { setCachedSessionId } from './sessionId/sessionIdHelper'; import type { - ErrorSource, DdRumType, RumActionType, ResourceKind, diff --git a/packages/core/src/rum/__tests__/DdRum.test.ts b/packages/core/src/rum/__tests__/DdRum.test.ts index 527492891..7e5fc24de 100644 --- a/packages/core/src/rum/__tests__/DdRum.test.ts +++ b/packages/core/src/rum/__tests__/DdRum.test.ts @@ -12,6 +12,7 @@ import { SdkVerbosity } from '../../SdkVerbosity'; import { BufferSingleton } from '../../sdk/DatadogProvider/Buffer/BufferSingleton'; import { DdSdk } from '../../sdk/DdSdk'; import { GlobalState } from '../../sdk/GlobalState/GlobalState'; +import { ErrorSource } from '../../types'; import { DdRum } from '../DdRum'; import type { ActionEventMapper } from '../eventMappers/actionEventMapper'; import type { ErrorEventMapper } from '../eventMappers/errorEventMapper'; @@ -22,7 +23,7 @@ import { TracingIdFormat } from '../instrumentation/resourceTracking/distributed import { TracingIdentifierUtils } from '../instrumentation/resourceTracking/distributedTracing/__tests__/__utils__/TracingIdentifierUtils'; import { setCachedSessionId } from '../sessionId/sessionIdHelper'; import type { FirstPartyHost } from '../types'; -import { ErrorSource, PropagatorType, RumActionType } from '../types'; +import { PropagatorType, RumActionType } from '../types'; import * as TracingContextUtils from './__utils__/TracingHeadersUtils'; diff --git a/packages/core/src/rum/eventMappers/errorEventMapper.ts b/packages/core/src/rum/eventMappers/errorEventMapper.ts index 462754d2c..6630d59a4 100644 --- a/packages/core/src/rum/eventMappers/errorEventMapper.ts +++ b/packages/core/src/rum/eventMappers/errorEventMapper.ts @@ -6,7 +6,7 @@ import type { AdditionalEventDataForMapper } from '../../sdk/EventMappers/EventMapper'; import { EventMapper } from '../../sdk/EventMappers/EventMapper'; -import type { ErrorSource } from '../types'; +import type { ErrorSource } from '../../types'; type RawError = { message: string; diff --git a/packages/core/src/rum/instrumentation/DdRumErrorTracking.tsx b/packages/core/src/rum/instrumentation/DdRumErrorTracking.tsx index 5a45bdc65..3c3ec9f65 100644 --- a/packages/core/src/rum/instrumentation/DdRumErrorTracking.tsx +++ b/packages/core/src/rum/instrumentation/DdRumErrorTracking.tsx @@ -8,6 +8,7 @@ import type { ErrorHandlerCallback } from 'react-native'; import { InternalLog } from '../../InternalLog'; import { SdkVerbosity } from '../../SdkVerbosity'; +import { ErrorSource } from '../../types'; import { getErrorMessage, getErrorStackTrace, @@ -18,7 +19,6 @@ import { } from '../../utils/errorUtils'; import { executeWithDelay } from '../../utils/jsUtils'; import { DdRum } from '../DdRum'; -import { ErrorSource } from '../types'; /** * Provides RUM auto-instrumentation feature to track errors as RUM events. diff --git a/packages/core/src/rum/types.ts b/packages/core/src/rum/types.ts index 5834123a6..fc8d07c02 100644 --- a/packages/core/src/rum/types.ts +++ b/packages/core/src/rum/types.ts @@ -4,6 +4,8 @@ * Copyright 2016-Present Datadog, Inc. */ +import type { ErrorSource } from '../types'; + import type { DatadogTracingContext } from './instrumentation/resourceTracking/distributedTracing/DatadogTracingContext'; import type { DatadogTracingIdentifier } from './instrumentation/resourceTracking/distributedTracing/DatadogTracingIdentifier'; @@ -233,14 +235,6 @@ export type ResourceKind = | 'other' | 'native'; -export enum ErrorSource { - NETWORK = 'NETWORK', - SOURCE = 'SOURCE', - CONSOLE = 'CONSOLE', - WEBVIEW = 'WEBVIEW', - CUSTOM = 'CUSTOM' -} - /** * Type of instrumentation on the host. * - DATADOG: Datadog’s propagator (`x-datadog-*`) diff --git a/packages/core/src/types.tsx b/packages/core/src/types.tsx index 4db877469..e1c5096fb 100644 --- a/packages/core/src/types.tsx +++ b/packages/core/src/types.tsx @@ -5,6 +5,7 @@ */ import type { BatchProcessingLevel } from './DdSdkReactNativeConfiguration'; +import type { UserInfo as UserInfoSingleton } from './sdk/UserInfoSingleton/types'; declare global { // eslint-disable-next-line no-var, vars-on-top @@ -118,13 +119,6 @@ export type DdSdkType = { setTrackingConsent(trackingConsent: string): Promise; }; -export type UserInfo = { - id: string; - name?: string; - email?: string; - extraInfo?: object; -}; - /** * The entry point to use Datadog's Trace feature. */ @@ -153,3 +147,44 @@ export type DdTraceType = { timestampMs?: number ): Promise; }; + +// Shared types across modules + +// Core + +export type UserInfo = { + id: string; + name?: string; + email?: string; + extraInfo?: object; +}; + +// DdLogs + +export type LogStatus = 'debug' | 'info' | 'warn' | 'error'; + +export type LogEvent = { + message: string; + context: object; + errorKind?: string; + errorMessage?: string; + stacktrace?: string; + fingerprint?: string; + readonly source?: ErrorSource; + // readonly date: number; // TODO: RUMM-2446 & RUMM-2447 + readonly status: LogStatus; + readonly userInfo: UserInfoSingleton; + readonly attributes?: object; +}; + +export type LogEventMapper = (logEvent: LogEvent) => LogEvent | null; + +// DdRum + +export enum ErrorSource { + NETWORK = 'NETWORK', + SOURCE = 'SOURCE', + CONSOLE = 'CONSOLE', + WEBVIEW = 'WEBVIEW', + CUSTOM = 'CUSTOM' +} From 0443e0ff0b90983d6c857788b93a62e1eb74b2d3 Mon Sep 17 00:00:00 2001 From: Marco Saia Date: Tue, 9 Sep 2025 12:08:35 +0200 Subject: [PATCH 032/410] iOS: Always use SDK default core instance --- .../core/ios/Sources/DatadogSDKWrapper.swift | 137 ++---------------- .../ios/Sources/DdLogsImplementation.swift | 7 +- .../ios/Sources/DdSdkImplementation.swift | 15 +- .../Sources/DdSdkNativeInitialization.swift | 21 ++- packages/core/ios/Sources/DdTelemetry.swift | 49 +++++++ .../ios/Tests/DatadogSdkWrapperTests.swift | 17 ++- packages/core/ios/Tests/DdSdkTests.swift | 58 +++----- .../DdSessionReplayImplementation.swift | 28 +--- .../ios/Tests/DdSessionReplayTests.swift | 6 +- .../Sources/RCTDatadogWebViewTracking.swift | 18 ++- .../DatadogSDKReactNativeWebViewTests.swift | 14 +- 11 files changed, 146 insertions(+), 224 deletions(-) create mode 100644 packages/core/ios/Sources/DdTelemetry.swift diff --git a/packages/core/ios/Sources/DatadogSDKWrapper.swift b/packages/core/ios/Sources/DatadogSDKWrapper.swift index 842ee5d89..0b9e51573 100644 --- a/packages/core/ios/Sources/DatadogSDKWrapper.swift +++ b/packages/core/ios/Sources/DatadogSDKWrapper.swift @@ -14,7 +14,7 @@ import DatadogWebViewTracking import DatadogInternal import Foundation -public typealias OnCoreInitializedListener = (DatadogCoreProtocol) -> Void +public typealias OnSdkInitializedListener = () -> Void /// Wrapper around the Datadog SDK. Use DatadogSDKWrapper.shared to access the instance. public class DatadogSDKWrapper { @@ -22,25 +22,14 @@ public class DatadogSDKWrapper { public static var shared = DatadogSDKWrapper() // Initialization callbacks - internal var onCoreInitializedListeners: [OnCoreInitializedListener] = [] - internal var loggerConfiguration = DatadogLogs.Logger.Configuration() - // Core instance - private var coreInstance: DatadogCoreProtocol? = nil + internal var onSdkInitializedListeners: [OnSdkInitializedListener] = [] - private init() { } - - public func addOnCoreInitializedListener(listener:@escaping OnCoreInitializedListener) { - onCoreInitializedListeners.append(listener) - } + internal private(set) var loggerConfiguration = DatadogLogs.Logger.Configuration() - /// This is intended for internal testing only. - public func setCoreInstance(core: DatadogCoreProtocol?) { - self.coreInstance = core - } + private init() { } - /// This is not supposed to be used in the SDK itself, rather by other SDKs like Session Replay. - public func getCoreInstance() -> DatadogCoreProtocol? { - return coreInstance + public func addOnSdkInitializedListener(listener:@escaping OnSdkInitializedListener) { + onSdkInitializedListeners.append(listener) } // SDK Wrapper @@ -49,124 +38,22 @@ public class DatadogSDKWrapper { loggerConfiguration: DatadogLogs.Logger.Configuration, trackingConsent: TrackingConsent ) -> Void { - let core = Datadog.initialize(with: coreConfiguration, trackingConsent: trackingConsent) - setCoreInstance(core: core) - for listener in onCoreInitializedListeners { - listener(core) - } - - self.loggerConfiguration = loggerConfiguration - } - - internal func isInitialized() -> Bool { - return Datadog.isInitialized() - } - - internal func clearAllData() -> Void { - if let core = coreInstance { - Datadog.clearAllData(in: core) - } else { - Datadog.clearAllData() - } - } - - // Features - internal func enableRUM(with configuration: RUM.Configuration) { - if let core = coreInstance { - RUM.enable(with: configuration, in: core) - } else { - consolePrint("Core instance was not found when initializing RUM.", .critical) - } - } - - internal func enableLogs(with configuration: Logs.Configuration) { - if let core = coreInstance { - Logs.enable(with: configuration, in: core) - } else { - consolePrint("Core instance was not found when initializing Logs.", .critical) - } - } - - internal func enableTrace(with configuration: Trace.Configuration) { - if let core = coreInstance { - Trace.enable(with: configuration, in: core) - } else { - consolePrint("Core instance was not found when initializing Trace.", .critical) - } - } - - internal func enableCrashReporting() { - if let core = coreInstance { - CrashReporting.enable(in: core) - } else { - consolePrint("Core instance was not found when initializing CrashReporting.", .critical) - } - } - - internal func createLogger() -> LoggerProtocol { - let core = coreInstance ?? { - consolePrint("Core instance was not found when creating Logger.", .critical) - return CoreRegistry.default - }() + Datadog.initialize(with: coreConfiguration, trackingConsent: trackingConsent) - return DatadogLogs.Logger.create(with: loggerConfiguration, in: core) - } - - // Telemetry - internal func sendTelemetryLog(message: String, attributes: [String: any Encodable], config: [String: any Encodable]) { - if let core = coreInstance { - let id = (config["onlyOnce"] as? Bool) == true ? message : UUID().uuidString - core.telemetry.debug(id: id, message: message, attributes: attributes) - } else { - consolePrint("Core instance was not found when calling sendTelemetryLog.", .warn) + for listener in onSdkInitializedListeners { + listener() } - } - internal func telemetryDebug(id: String, message: String) { - return Datadog._internal.telemetry.debug(id: id, message: message) - } - - internal func telemetryError(id: String, message: String, kind: String?, stack: String?) { - return Datadog._internal.telemetry.error(id: id, message: message, kind: kind, stack: stack) - } - - internal func overrideTelemetryConfiguration( - initializationType: String? = nil, - reactNativeVersion: String? = nil, - reactVersion: String? = nil, - trackCrossPlatformLongTasks: Bool? = nil, - trackErrors: Bool? = nil, - trackInteractions: Bool? = nil, - trackLongTask: Bool? = nil, - trackNativeErrors: Bool? = nil, - trackNativeLongTasks: Bool? = nil, - trackNetworkRequests: Bool? = nil - ) { - coreInstance?.telemetry.configuration( - initializationType: initializationType, - reactNativeVersion: reactNativeVersion, - reactVersion: reactVersion, - trackCrossPlatformLongTasks: trackCrossPlatformLongTasks, - trackErrors: trackErrors, - trackLongTask: trackLongTask, - trackNativeErrors: trackNativeErrors, - trackNativeLongTasks: trackNativeLongTasks, - trackNetworkRequests: trackNetworkRequests, - trackUserInteractions: trackInteractions - ) + self.loggerConfiguration = loggerConfiguration } // Webview private var webviewMessageEmitter: InternalExtension.AbstractMessageEmitter? internal func enableWebviewTracking() { - if let core = coreInstance { - webviewMessageEmitter = WebViewTracking._internal.messageEmitter(in: core) - } else { - consolePrint("Core instance was not found when initializing Webview tracking.", .critical) - } + webviewMessageEmitter = WebViewTracking._internal.messageEmitter(in: CoreRegistry.default) } - + internal func sendWebviewMessage(body: NSString) throws { try self.webviewMessageEmitter?.send(body: body) } diff --git a/packages/core/ios/Sources/DdLogsImplementation.swift b/packages/core/ios/Sources/DdLogsImplementation.swift index 264a3d0b3..fe3fde092 100644 --- a/packages/core/ios/Sources/DdLogsImplementation.swift +++ b/packages/core/ios/Sources/DdLogsImplementation.swift @@ -5,7 +5,9 @@ */ import Foundation +import DatadogInternal import DatadogLogs +import DatadogCore @objc public class DdLogsImplementation: NSObject { @@ -20,7 +22,10 @@ public class DdLogsImplementation: NSObject { @objc public override convenience init() { - self.init({ DatadogSDKWrapper.shared.createLogger() }, { DatadogSDKWrapper.shared.isInitialized() }) + self.init( + { DatadogLogs.Logger.create(with: DatadogSDKWrapper.shared.loggerConfiguration) }, + { Datadog.isInitialized() } + ) } @objc diff --git a/packages/core/ios/Sources/DdSdkImplementation.swift b/packages/core/ios/Sources/DdSdkImplementation.swift index 6d060dc31..2f66f868c 100644 --- a/packages/core/ios/Sources/DdSdkImplementation.swift +++ b/packages/core/ios/Sources/DdSdkImplementation.swift @@ -133,40 +133,41 @@ public class DdSdkImplementation: NSObject { public func sendTelemetryLog(message: NSString, attributes: NSDictionary, config: NSDictionary, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { let castedAttributes = castAttributesToSwift(attributes) let castedConfig = castAttributesToSwift(config) - DatadogSDKWrapper.shared.sendTelemetryLog(message: message as String, attributes: castedAttributes, config: castedConfig) + DdTelemetry.sendTelemetryLog(message: message as String, attributes: castedAttributes, config: castedConfig) resolve(nil) } @objc public func telemetryDebug(message: NSString, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { - DatadogSDKWrapper.shared.telemetryDebug(id: "datadog_react_native:\(message)", message: message as String) + DdTelemetry.telemetryDebug(id: "datadog_react_native:\(message)", message: message as String) resolve(nil) } @objc public func telemetryError(message: NSString, stack: NSString, kind: NSString, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { - DatadogSDKWrapper.shared.telemetryError(id: "datadog_react_native:\(String(describing: kind)):\(message)", message: message as String, kind: kind as String, stack: stack as String) + DdTelemetry.telemetryError(id: "datadog_react_native:\(String(describing: kind)):\(message)", message: message as String, kind: kind as String, stack: stack as String) resolve(nil) } - + @objc public func consumeWebviewEvent(message: NSString, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { do{ try DatadogSDKWrapper.shared.sendWebviewMessage(body: message) } catch { - DatadogSDKWrapper.shared.telemetryError(id: "datadog_react_native:\(error.localizedDescription)", message: "The message being sent was:\(message)" as String, kind: "WebViewEventBridgeError" as String, stack: String(describing: error) as String) + DdTelemetry.telemetryError(id: "datadog_react_native:\(error.localizedDescription)", message: "The message being sent was:\(message)" as String, kind: "WebViewEventBridgeError" as String, stack: String(describing: error) as String) } + resolve(nil) } @objc public func clearAllData(resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { - DatadogSDKWrapper.shared.clearAllData() + Datadog.clearAllData() resolve(nil) } func overrideReactNativeTelemetry(rnConfiguration: DdSdkConfiguration) -> Void { - DatadogSDKWrapper.shared.overrideTelemetryConfiguration( + DdTelemetry.overrideTelemetryConfiguration( initializationType: rnConfiguration.configurationForTelemetry?.initializationType as? String, reactNativeVersion: rnConfiguration.configurationForTelemetry?.reactNativeVersion as? String, reactVersion: rnConfiguration.configurationForTelemetry?.reactVersion as? String, diff --git a/packages/core/ios/Sources/DdSdkNativeInitialization.swift b/packages/core/ios/Sources/DdSdkNativeInitialization.swift index d68d89d06..7b0f1be19 100644 --- a/packages/core/ios/Sources/DdSdkNativeInitialization.swift +++ b/packages/core/ios/Sources/DdSdkNativeInitialization.swift @@ -30,12 +30,13 @@ public class DdSdkNativeInitialization: NSObject { } internal func initialize(sdkConfiguration: DdSdkConfiguration) { - // TODO: see if this `if` is still needed - if DatadogSDKWrapper.shared.isInitialized() { - // Initializing the SDK twice results in Global.rum and - // Global.sharedTracer to be set to no-op instances + if Datadog.isInitialized(instanceName: CoreRegistry.defaultInstanceName) { + // Initializing the SDK twice results in Global.rum and Global.sharedTracer to be set to no-op instances consolePrint("Datadog SDK is already initialized, skipping initialization.", .debug) - DatadogSDKWrapper.shared.telemetryDebug(id: "datadog_react_native: RN SDK was already initialized in native", message: "RN SDK was already initialized in native") + DdTelemetry.telemetryDebug( + id: "datadog_react_native: RN SDK was already initialized in native", + message: "RN SDK was already initialized in native" + ) RUMMonitor.shared().currentSessionID { sessionId in guard let id = sessionId else { return } @@ -78,19 +79,17 @@ public class DdSdkNativeInitialization: NSObject { func enableFeatures(sdkConfiguration: DdSdkConfiguration) { let rumConfig = buildRUMConfiguration(configuration: sdkConfiguration) - DatadogSDKWrapper.shared.enableRUM(with: rumConfig) + RUM.enable(with: rumConfig) let logsConfig = buildLogsConfiguration(configuration: sdkConfiguration) - DatadogSDKWrapper.shared.enableLogs(with: logsConfig) + Logs.enable(with: logsConfig) let traceConfig = buildTraceConfiguration(configuration: sdkConfiguration) - DatadogSDKWrapper.shared.enableTrace(with: traceConfig) + Trace.enable(with: traceConfig) if sdkConfiguration.nativeCrashReportEnabled ?? false { - DatadogSDKWrapper.shared.enableCrashReporting() + CrashReporting.enable() } - - DatadogSDKWrapper.shared.enableWebviewTracking() } func buildSDKConfiguration(configuration: DdSdkConfiguration, defaultAppVersion: String = getDefaultAppVersion()) -> Datadog.Configuration { diff --git a/packages/core/ios/Sources/DdTelemetry.swift b/packages/core/ios/Sources/DdTelemetry.swift new file mode 100644 index 000000000..cb1896db8 --- /dev/null +++ b/packages/core/ios/Sources/DdTelemetry.swift @@ -0,0 +1,49 @@ + +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2025 Datadog, Inc. + */ +import DatadogCore +import DatadogInternal + +public class DdTelemetry { + public static func sendTelemetryLog(message: String, attributes: [String: any Encodable], config: [String: any Encodable]) { + let id = (config["onlyOnce"] as? Bool) == true ? message : UUID().uuidString + CoreRegistry.default.telemetry.debug(id: id, message: message, attributes: attributes) + } + + public static func telemetryDebug(id: String, message: String) { + return Datadog._internal.telemetry.debug(id: id, message: message) + } + + public static func telemetryError(id: String, message: String, kind: String?, stack: String?) { + return Datadog._internal.telemetry.error(id: id, message: message, kind: kind, stack: stack) + } + + public static func overrideTelemetryConfiguration( + initializationType: String? = nil, + reactNativeVersion: String? = nil, + reactVersion: String? = nil, + trackCrossPlatformLongTasks: Bool? = nil, + trackErrors: Bool? = nil, + trackInteractions: Bool? = nil, + trackLongTask: Bool? = nil, + trackNativeErrors: Bool? = nil, + trackNativeLongTasks: Bool? = nil, + trackNetworkRequests: Bool? = nil + ) { + CoreRegistry.default.telemetry.configuration( + initializationType: initializationType, + reactNativeVersion: reactNativeVersion, + reactVersion: reactVersion, + trackCrossPlatformLongTasks: trackCrossPlatformLongTasks, + trackErrors: trackErrors, + trackLongTask: trackLongTask, + trackNativeErrors: trackNativeErrors, + trackNativeLongTasks: trackNativeLongTasks, + trackNetworkRequests: trackNetworkRequests, + trackUserInteractions: trackInteractions + ) + } +} diff --git a/packages/core/ios/Tests/DatadogSdkWrapperTests.swift b/packages/core/ios/Tests/DatadogSdkWrapperTests.swift index 4811b4c1d..a445b2bbe 100644 --- a/packages/core/ios/Tests/DatadogSdkWrapperTests.swift +++ b/packages/core/ios/Tests/DatadogSdkWrapperTests.swift @@ -8,22 +8,23 @@ import XCTest @testable import DatadogSDKReactNative import DatadogTrace import DatadogInternal - +import DatadogRUM +import DatadogLogs internal class DatadogSdkWrapperTests: XCTestCase { override func setUp() { super.setUp() - DatadogSDKWrapper.shared.setCoreInstance(core: nil) - DatadogSDKWrapper.shared.onCoreInitializedListeners = [] + DatadogSDKWrapper.shared.onSdkInitializedListeners = [] } - func testItSetsCoreUsedForFeatures() { + func testOverrideCoreRegistryDefault() { let coreMock = MockDatadogCore() - DatadogSDKWrapper.shared.setCoreInstance(core: coreMock) + CoreRegistry.register(default: coreMock) + defer { CoreRegistry.unregisterDefault() } - DatadogSDKWrapper.shared.enableTrace(with: .init()) - DatadogSDKWrapper.shared.enableRUM(with: .init(applicationID: "app-id")) - DatadogSDKWrapper.shared.enableLogs(with: .init()) + Trace.enable(with: .init()) + RUM.enable(with: .init(applicationID: "app-id")) + Logs.enable(with: .init()) XCTAssertNotNil(coreMock.features["tracing"]) XCTAssertNotNil(coreMock.features["rum"]) diff --git a/packages/core/ios/Tests/DdSdkTests.swift b/packages/core/ios/Tests/DdSdkTests.swift index edbbba4dd..bcc3252a8 100644 --- a/packages/core/ios/Tests/DdSdkTests.swift +++ b/packages/core/ios/Tests/DdSdkTests.swift @@ -34,8 +34,7 @@ class DdSdkTests: XCTestCase { private func mockReject(args _: String?, arg _: String?, err _: Error?) {} override func tearDown() { - DatadogSDKWrapper.shared.setCoreInstance(core: nil) - DatadogSDKWrapper.shared.onCoreInitializedListeners = [] + DatadogSDKWrapper.shared.onSdkInitializedListeners = [] Datadog.internalFlushAndDeinitialize() } @@ -84,11 +83,10 @@ class DdSdkTests: XCTestCase { let bridge = DispatchQueueMock() let mockJSRefreshRateMonitor = MockJSRefreshRateMonitor() let mockListener = MockOnCoreInitializedListener() - DatadogSDKWrapper.shared.addOnCoreInitializedListener(listener: mockListener.listener) + DatadogSDKWrapper.shared.addOnSdkInitializedListener(listener: mockListener.listener) - let expectation = self.expectation(description: "Core is set when promise resolves") + let expectation = self.expectation(description: "Listener is called when promise resolves") func mockPromiseResolve(_: Any?) { - XCTAssertNotNil(mockListener.core) expectation.fulfill() } @@ -276,9 +274,9 @@ class DdSdkTests: XCTestCase { } func testSDKInitializationWithOnInitializedCallback() { - var coreFromCallback: DatadogCoreProtocol? = nil - DatadogSDKWrapper.shared.addOnCoreInitializedListener(listener: { core in - coreFromCallback = core + var isInitialized = false + DatadogSDKWrapper.shared.addOnSdkInitializedListener(listener: { + isInitialized = Datadog.isInitialized() }) DdSdkImplementation( @@ -293,14 +291,16 @@ class DdSdkTests: XCTestCase { reject: mockReject ) - XCTAssertNotNil(coreFromCallback) + XCTAssertTrue(isInitialized) } func testEnableAllFeatures() { let core = MockDatadogCore() + CoreRegistry.register(default: core) + defer { CoreRegistry.unregisterDefault() } + let configuration: DdSdkConfiguration = .mockAny() - DatadogSDKWrapper.shared.setCoreInstance(core: core) DdSdkNativeInitialization().enableFeatures( sdkConfiguration: configuration ) @@ -479,9 +479,11 @@ class DdSdkTests: XCTestCase { func testBuildConfigurationWithCrashReport() { let core = MockDatadogCore() + CoreRegistry.register(default: core) + defer { CoreRegistry.unregisterDefault() } + let configuration: DdSdkConfiguration = .mockAny(nativeCrashReportEnabled: true) - DatadogSDKWrapper.shared.setCoreInstance(core: core) DdSdkNativeInitialization().enableFeatures( sdkConfiguration: configuration ) @@ -1233,6 +1235,9 @@ class DdSdkTests: XCTestCase { func testConfigurationTelemetryOverride() throws { let core = MockDatadogCore() + CoreRegistry.register(default: core) + defer { CoreRegistry.unregisterDefault() } + let configuration: DdSdkConfiguration = .mockAny( nativeCrashReportEnabled: false, nativeLongTaskThresholdMs: 0.0, @@ -1244,7 +1249,6 @@ class DdSdkTests: XCTestCase { ] ) - DatadogSDKWrapper.shared.setCoreInstance(core: core) DdSdkImplementation().overrideReactNativeTelemetry(rnConfiguration: configuration) XCTAssertEqual(core.configuration?.initializationType, "LEGACY") @@ -1313,11 +1317,12 @@ class DdSdkTests: XCTestCase { XCTAssertTrue(bridge.isSameQueue(queue: mockJSRefreshRateMonitor.jsQueue!)) } - func testCallsOnCoreInitializedListeners() throws { + func testCallsOnSdkInitializedListeners() throws { let bridge = DispatchQueueMock() let mockJSRefreshRateMonitor = MockJSRefreshRateMonitor() let mockListener = MockOnCoreInitializedListener() - DatadogSDKWrapper.shared.addOnCoreInitializedListener(listener: mockListener.listener) + + DatadogSDKWrapper.shared.addOnSdkInitializedListener(listener: mockListener.listener) DdSdkImplementation( mainDispatchQueue: DispatchQueueMock(), @@ -1331,23 +1336,7 @@ class DdSdkTests: XCTestCase { reject: mockReject ) - XCTAssertNotNil(mockListener.core) - } - - func testConsumeWebviewEvent() throws { - let configuration: DdSdkConfiguration = .mockAny() - let core = MockDatadogCore() - - DatadogSDKWrapper.shared.setCoreInstance(core: core) - DdSdkNativeInitialization().enableFeatures( - sdkConfiguration: configuration - ) - - DdSdkImplementation().consumeWebviewEvent( - message: "{\"eventType\":\"rum\",\"event\":{\"blabla\":\"custom message\"}}", - resolve: mockResolve, reject: mockReject) - - XCTAssertNotNil(core.baggages["browser-rum-event"]) + XCTAssertTrue(mockListener.called) } func testInitialResourceThreshold() { @@ -1601,9 +1590,8 @@ extension DdSdkImplementation { } class MockOnCoreInitializedListener { - var core: DatadogCoreProtocol? - - func listener(core: DatadogCoreProtocol) { - self.core = core + var called = false + func listener() { + self.called = true } } diff --git a/packages/react-native-session-replay/ios/Sources/DdSessionReplayImplementation.swift b/packages/react-native-session-replay/ios/Sources/DdSessionReplayImplementation.swift index 45cfc2582..0fc9c6637 100644 --- a/packages/react-native-session-replay/ios/Sources/DdSessionReplayImplementation.swift +++ b/packages/react-native-session-replay/ios/Sources/DdSessionReplayImplementation.swift @@ -6,6 +6,7 @@ import Foundation @_spi(Internal) import DatadogSessionReplay +import DatadogCore import DatadogInternal import DatadogSDKReactNative import React @@ -66,8 +67,6 @@ public class DdSessionReplayImplementation: NSObject { customEndpoint: customEndpointURL ) -// let bundle = Bundle(for: DdSessionReplayImplementation.self) - var svgMap: [String: SVGData] = [:] if let bundle = Bundle.ddSessionReplayResources, @@ -92,38 +91,21 @@ public class DdSessionReplayImplementation: NSObject { fabricWrapper: fabricWrapper ) ]) - - if let core = DatadogSDKWrapper.shared.getCoreInstance() { - sessionReplay.enable( - with: sessionReplayConfiguration, - in: core - ) - } else { - consolePrint("Core instance was not found when initializing Session Replay.", .critical) - } + + sessionReplay.enable(with: sessionReplayConfiguration, in: CoreRegistry.default) resolve(nil) } @objc public func startRecording(resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) -> Void { - if let core = DatadogSDKWrapper.shared.getCoreInstance() { - sessionReplay.startRecording(in: core) - } else { - consolePrint("Core instance was not found when calling startRecording in Session Replay.", .critical) - } - + sessionReplay.startRecording(in: CoreRegistry.default) resolve(nil) } @objc public func stopRecording(resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) -> Void { - if let core = DatadogSDKWrapper.shared.getCoreInstance() { - sessionReplay.stopRecording(in: core) - } else { - consolePrint("Core instance was not found when calling stopRecording in Session Replay.", .critical) - } - + sessionReplay.stopRecording(in: CoreRegistry.default) resolve(nil) } diff --git a/packages/react-native-session-replay/ios/Tests/DdSessionReplayTests.swift b/packages/react-native-session-replay/ios/Tests/DdSessionReplayTests.swift index aa9102850..067a11df0 100644 --- a/packages/react-native-session-replay/ios/Tests/DdSessionReplayTests.swift +++ b/packages/react-native-session-replay/ios/Tests/DdSessionReplayTests.swift @@ -35,7 +35,11 @@ internal class DdSessionReplayTests: XCTestCase { override func setUp() { super.setUp() let mockDatadogCore = MockDatadogCore() - DatadogSDKWrapper.shared.setCoreInstance(core: mockDatadogCore) + CoreRegistry.register(default: mockDatadogCore) + } + + override func tearDown() { + CoreRegistry.unregisterDefault() } func testEnablesSessionReplayWithZeroReplaySampleRate() { diff --git a/packages/react-native-webview/ios/Sources/RCTDatadogWebViewTracking.swift b/packages/react-native-webview/ios/Sources/RCTDatadogWebViewTracking.swift index 97fd835b5..45f11e452 100644 --- a/packages/react-native-webview/ios/Sources/RCTDatadogWebViewTracking.swift +++ b/packages/react-native-webview/ios/Sources/RCTDatadogWebViewTracking.swift @@ -8,26 +8,27 @@ import WebKit import DatadogWebViewTracking import DatadogSDKReactNative import DatadogCore +import DatadogInternal @objc public class RCTDatadogWebViewTracking: NSObject { var webView: RCTDatadogWebView? = nil var allowedHosts: Set = Set() - var coreListener: OnCoreInitializedListener? + var onSdkInitializedListener: OnSdkInitializedListener? public override init() { super.init() - self.coreListener = { [weak self] (core: DatadogCoreProtocol) in + self.onSdkInitializedListener = { [weak self] in guard let strongSelf = self, let webView = strongSelf.webView else { return } strongSelf.enableWebViewTracking( webView: webView, allowedHosts: strongSelf.allowedHosts, - core: core + core: CoreRegistry.default ) } } - + /** Enables tracking on the given WebView. @@ -42,15 +43,16 @@ import DatadogCore guard !webView.isTrackingEnabled else { return } - if let core = DatadogSDKWrapper.shared.getCoreInstance() { - enableWebViewTracking(webView: webView, allowedHosts: allowedHosts, core: core) - } else if let coreListener = self.coreListener { - DatadogSDKWrapper.shared.addOnCoreInitializedListener(listener: coreListener) + if CoreRegistry.isRegistered(instanceName: CoreRegistry.defaultInstanceName) { + enableWebViewTracking(webView: webView, allowedHosts: allowedHosts, core: CoreRegistry.default) + } else if let onSdkInitializedListener = self.onSdkInitializedListener { + DatadogSDKWrapper.shared.addOnSdkInitializedListener(listener: onSdkInitializedListener) } else { // TODO: Report initialization problem } } + private func enableWebViewTracking( webView: RCTDatadogWebView, allowedHosts: Set, diff --git a/packages/react-native-webview/ios/Tests/DatadogSDKReactNativeWebViewTests.swift b/packages/react-native-webview/ios/Tests/DatadogSDKReactNativeWebViewTests.swift index 7ea36bda8..20f1b84fa 100644 --- a/packages/react-native-webview/ios/Tests/DatadogSDKReactNativeWebViewTests.swift +++ b/packages/react-native-webview/ios/Tests/DatadogSDKReactNativeWebViewTests.swift @@ -17,7 +17,11 @@ internal class DatadogSDKReactNativeWebViewTests: XCTestCase { override func setUp() { super.setUp() let mockDatadogCore = MockDatadogCore() - DatadogSDKWrapper.shared.setCoreInstance(core: mockDatadogCore) + CoreRegistry.register(default: mockDatadogCore) + } + + override func tearDown() { + CoreRegistry.unregisterDefault() } func testDatadogWebViewManagerReturnsDatadogWebView() { @@ -41,9 +45,10 @@ internal class DatadogSDKReactNativeWebViewTests: XCTestCase { XCTAssertFalse(view.isTrackingEnabled) } - func testDatadogWebViewTrackingIsDisabledIfCoreIsNotReady() { + func testDatadogWebViewTrackingIsDisabledIfSdkIsNotInitialized() { // Given - DatadogSDKWrapper.shared.setCoreInstance(core: nil) + CoreRegistry.unregisterDefault() + let viewManager = RCTDatadogWebViewManager() let allowedHosts = NSArray(objects: "example1.com", "example2.com") @@ -82,7 +87,7 @@ internal class DatadogSDKReactNativeWebViewTests: XCTestCase { view.addSubview(WKWebView()) - DatadogSDKWrapper.shared.setCoreInstance(core: nil) + CoreRegistry.unregisterDefault() // Given let selector = NSSelectorFromString("setupDatadogWebView:view:") @@ -92,7 +97,6 @@ internal class DatadogSDKReactNativeWebViewTests: XCTestCase { XCTAssertFalse(view.isTrackingEnabled) // When - DatadogSDKWrapper.shared.setCoreInstance(core: MockDatadogCore()) DatadogSDKWrapper.shared.callInitialize() let expectation = self.expectation(description: "WebView tracking is enabled through the listener.") From 0317ded5b6599fd1198e1842e22ec8f1495ccd41 Mon Sep 17 00:00:00 2001 From: Sergio Barrio Date: Mon, 8 Sep 2025 15:27:17 +0200 Subject: [PATCH 033/410] Bump native SDK dependencies to 3.0.0 --- benchmarks/android/app/build.gradle | 2 +- benchmarks/ios/Podfile.lock | 74 +++++++------- bump-native-dd-sdk.sh | 2 +- example-new-architecture/ios/Podfile.lock | 70 +++++++------- example/ios/Podfile.lock | 96 +++++++++---------- packages/core/DatadogSDKReactNative.podspec | 12 +-- packages/core/android/build.gradle | 10 +- ...DatadogSDKReactNativeSessionReplay.podspec | 2 +- .../android/build.gradle | 4 +- .../DatadogSDKReactNativeWebView.podspec | 4 +- .../react-native-webview/android/build.gradle | 2 +- 11 files changed, 139 insertions(+), 139 deletions(-) diff --git a/benchmarks/android/app/build.gradle b/benchmarks/android/app/build.gradle index 86d26791f..7dc4b3707 100644 --- a/benchmarks/android/app/build.gradle +++ b/benchmarks/android/app/build.gradle @@ -129,5 +129,5 @@ dependencies { // Benchmark tools from dd-sdk-android are used for vitals recording // Remember to bump thid alongside the main dd-sdk-android dependencies - implementation("com.datadoghq:dd-sdk-android-benchmark-internal:2.25.0") + implementation("com.datadoghq:dd-sdk-android-benchmark-internal:3.0.0") } diff --git a/benchmarks/ios/Podfile.lock b/benchmarks/ios/Podfile.lock index 71cd768ba..c04c9d92d 100644 --- a/benchmarks/ios/Podfile.lock +++ b/benchmarks/ios/Podfile.lock @@ -1,22 +1,22 @@ PODS: - boost (1.84.0) - - DatadogCore (2.30.2): - - DatadogInternal (= 2.30.2) - - DatadogCrashReporting (2.30.2): - - DatadogInternal (= 2.30.2) + - DatadogCore (3.0.0): + - DatadogInternal (= 3.0.0) + - DatadogCrashReporting (3.0.0): + - DatadogInternal (= 3.0.0) - PLCrashReporter (~> 1.12.0) - - DatadogInternal (2.30.2) - - DatadogLogs (2.30.2): - - DatadogInternal (= 2.30.2) - - DatadogRUM (2.30.2): - - DatadogInternal (= 2.30.2) - - DatadogSDKReactNative (2.13.0): - - DatadogCore (= 2.30.2) - - DatadogCrashReporting (= 2.30.2) - - DatadogLogs (= 2.30.2) - - DatadogRUM (= 2.30.2) - - DatadogTrace (= 2.30.2) - - DatadogWebViewTracking (= 2.30.2) + - DatadogInternal (3.0.0) + - DatadogLogs (3.0.0): + - DatadogInternal (= 3.0.0) + - DatadogRUM (3.0.0): + - DatadogInternal (= 3.0.0) + - DatadogSDKReactNative (2.12.1): + - DatadogCore (= 3.0.0) + - DatadogCrashReporting (= 3.0.0) + - DatadogLogs (= 3.0.0) + - DatadogRUM (= 3.0.0) + - DatadogTrace (= 3.0.0) + - DatadogWebViewTracking (= 3.0.0) - DoubleConversion - glog - hermes-engine @@ -39,7 +39,7 @@ PODS: - Yoga - DatadogSDKReactNativeSessionReplay (2.13.0): - DatadogSDKReactNative - - DatadogSessionReplay (= 2.30.2) + - DatadogSessionReplay (= 3.0.0) - DoubleConversion - glog - hermes-engine @@ -60,10 +60,10 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - DatadogSDKReactNativeWebView (2.13.0): - - DatadogInternal (= 2.30.2) + - DatadogSDKReactNativeWebView (2.12.1): + - DatadogInternal (= 3.0.0) - DatadogSDKReactNative - - DatadogWebViewTracking (= 2.30.2) + - DatadogWebViewTracking (= 3.0.0) - DoubleConversion - glog - hermes-engine @@ -84,13 +84,13 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - DatadogSessionReplay (2.30.2): - - DatadogInternal (= 2.30.2) - - DatadogTrace (2.30.2): - - DatadogInternal (= 2.30.2) + - DatadogSessionReplay (3.0.0): + - DatadogInternal (= 3.0.0) + - DatadogTrace (3.0.0): + - DatadogInternal (= 3.0.0) - OpenTelemetrySwiftApi (= 1.13.1) - - DatadogWebViewTracking (2.30.2): - - DatadogInternal (= 2.30.2) + - DatadogWebViewTracking (3.0.0): + - DatadogInternal (= 3.0.0) - DoubleConversion (1.1.6) - fast_float (6.1.4) - FBLazyVector (0.78.2) @@ -2070,17 +2070,17 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: boost: 7e761d76ca2ce687f7cc98e698152abd03a18f90 - DatadogCore: 8e50ad6cb68343f701707f7eeca16e0acb52ed8c - DatadogCrashReporting: 34763d4276d4fce286c7e8729cfcd2ac04dca43b - DatadogInternal: bd8672d506a7e67936ed5f7ca612169e49029b44 - DatadogLogs: d683aa9e0c9339f5ae679ead70bbdbe41cdc32f6 - DatadogRUM: 8b794aa458e6323ea9b1cef3f820fd3d092cbe27 - DatadogSDKReactNative: 6da208fd44e9eef09a17eb54826f65a58731186b - DatadogSDKReactNativeSessionReplay: 96194d4014b4bee544b75589ef7c5f0954a24aae - DatadogSDKReactNativeWebView: e9b6c83bbd6eea15a97bad45ed007e7b24f0da63 - DatadogSessionReplay: 56a91d799fe34967c5ae79a222364e37d67020f5 - DatadogTrace: 3ba194791267efa09634234749111cac95abd3e5 - DatadogWebViewTracking: 8287d5ad06e992de5e46dd72a17e05c7513344be + DatadogCore: 0c39ba4ef3dee02071f235f2fb56af7e29f4ceb0 + DatadogCrashReporting: 604e65e524cb2fc22cda39fd9c9ea5fd3bddf24e + DatadogInternal: 442673a7fb5329299b489b91ed705aa2085380f7 + DatadogLogs: 0146a0e3140ba62fd42729593c0910d692aea5fd + DatadogRUM: f97fbd0290ecec5bc952fb03a6a1ae068649243f + DatadogSDKReactNative: 241bf982c16ceff03d94a58e6d005e55c47b99c6 + DatadogSDKReactNativeSessionReplay: bba36092686e3183e97c1a0c7f4ca8142582ae28 + DatadogSDKReactNativeWebView: 6da060df20e235abac533e582d9fc2b3a5070840 + DatadogSessionReplay: 0357b911c6ab42c77c93b29761ecc20d9282e971 + DatadogTrace: d714e7c456e612b7f171483d08086a59aa1c4213 + DatadogWebViewTracking: fb9591c09fca82b143f3843a7164a7e782175c53 DoubleConversion: cb417026b2400c8f53ae97020b2be961b59470cb fast_float: 06eeec4fe712a76acc9376682e4808b05ce978b6 FBLazyVector: e32d34492c519a2194ec9d7f5e7a79d11b73f91c diff --git a/bump-native-dd-sdk.sh b/bump-native-dd-sdk.sh index 8545802db..67d99a0e7 100755 --- a/bump-native-dd-sdk.sh +++ b/bump-native-dd-sdk.sh @@ -23,7 +23,7 @@ podspec_files=( "packages/react-native-webview/DatadogSDKReactNativeWebView.podspec" ) -ios_pattern="('Datadog[^']+', '~> )[0-9.]+'" +ios_pattern="('Datadog[^']+', ')[0-9.]+'" android_pattern='(com\.datadoghq:dd-sdk-android-[^:"]+):[0-9.]+' if [[ "$sdk" == "ios" ]]; then diff --git a/example-new-architecture/ios/Podfile.lock b/example-new-architecture/ios/Podfile.lock index 960a656be..281513566 100644 --- a/example-new-architecture/ios/Podfile.lock +++ b/example-new-architecture/ios/Podfile.lock @@ -1,22 +1,22 @@ PODS: - boost (1.84.0) - - DatadogCore (2.30.2): - - DatadogInternal (= 2.30.2) - - DatadogCrashReporting (2.30.2): - - DatadogInternal (= 2.30.2) + - DatadogCore (3.0.0): + - DatadogInternal (= 3.0.0) + - DatadogCrashReporting (3.0.0): + - DatadogInternal (= 3.0.0) - PLCrashReporter (~> 1.12.0) - - DatadogInternal (2.30.2) - - DatadogLogs (2.30.2): - - DatadogInternal (= 2.30.2) - - DatadogRUM (2.30.2): - - DatadogInternal (= 2.30.2) - - DatadogSDKReactNative (2.13.0): - - DatadogCore (= 2.30.2) - - DatadogCrashReporting (= 2.30.2) - - DatadogLogs (= 2.30.2) - - DatadogRUM (= 2.30.2) - - DatadogTrace (= 2.30.2) - - DatadogWebViewTracking (= 2.30.2) + - DatadogInternal (3.0.0) + - DatadogLogs (3.0.0): + - DatadogInternal (= 3.0.0) + - DatadogRUM (3.0.0): + - DatadogInternal (= 3.0.0) + - DatadogSDKReactNative (2.12.1): + - DatadogCore (= 3.0.0) + - DatadogCrashReporting (= 3.0.0) + - DatadogLogs (= 3.0.0) + - DatadogRUM (= 3.0.0) + - DatadogTrace (= 3.0.0) + - DatadogWebViewTracking (= 3.0.0) - DoubleConversion - glog - hermes-engine @@ -37,13 +37,13 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - DatadogSDKReactNative/Tests (2.13.0): - - DatadogCore (= 2.30.2) - - DatadogCrashReporting (= 2.30.2) - - DatadogLogs (= 2.30.2) - - DatadogRUM (= 2.30.2) - - DatadogTrace (= 2.30.2) - - DatadogWebViewTracking (= 2.30.2) + - DatadogSDKReactNative/Tests (2.12.1): + - DatadogCore (= 3.0.0) + - DatadogCrashReporting (= 3.0.0) + - DatadogLogs (= 3.0.0) + - DatadogRUM (= 3.0.0) + - DatadogTrace (= 3.0.0) + - DatadogWebViewTracking (= 3.0.0) - DoubleConversion - glog - hermes-engine @@ -64,11 +64,11 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - DatadogTrace (2.30.2): - - DatadogInternal (= 2.30.2) + - DatadogTrace (3.0.0): + - DatadogInternal (= 3.0.0) - OpenTelemetrySwiftApi (= 1.13.1) - - DatadogWebViewTracking (2.30.2): - - DatadogInternal (= 2.30.2) + - DatadogWebViewTracking (3.0.0): + - DatadogInternal (= 3.0.0) - DoubleConversion (1.1.6) - fast_float (6.1.4) - FBLazyVector (0.76.9) @@ -1850,14 +1850,14 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: boost: 1dca942403ed9342f98334bf4c3621f011aa7946 - DatadogCore: 8e50ad6cb68343f701707f7eeca16e0acb52ed8c - DatadogCrashReporting: 34763d4276d4fce286c7e8729cfcd2ac04dca43b - DatadogInternal: bd8672d506a7e67936ed5f7ca612169e49029b44 - DatadogLogs: d683aa9e0c9339f5ae679ead70bbdbe41cdc32f6 - DatadogRUM: 8b794aa458e6323ea9b1cef3f820fd3d092cbe27 - DatadogSDKReactNative: 259a7851629a58cc42f3e3e7871bc041cc82209b - DatadogTrace: 3ba194791267efa09634234749111cac95abd3e5 - DatadogWebViewTracking: 8287d5ad06e992de5e46dd72a17e05c7513344be + DatadogCore: 0c39ba4ef3dee02071f235f2fb56af7e29f4ceb0 + DatadogCrashReporting: 604e65e524cb2fc22cda39fd9c9ea5fd3bddf24e + DatadogInternal: 442673a7fb5329299b489b91ed705aa2085380f7 + DatadogLogs: 0146a0e3140ba62fd42729593c0910d692aea5fd + DatadogRUM: f97fbd0290ecec5bc952fb03a6a1ae068649243f + DatadogSDKReactNative: 0a80aa75958d595a99be54d2838db53eda7a08af + DatadogTrace: d714e7c456e612b7f171483d08086a59aa1c4213 + DatadogWebViewTracking: fb9591c09fca82b143f3843a7164a7e782175c53 DoubleConversion: f16ae600a246532c4020132d54af21d0ddb2a385 fast_float: 06eeec4fe712a76acc9376682e4808b05ce978b6 FBLazyVector: 7605ea4810e0e10ae4815292433c09bf4324ba45 diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 691bbdb62..91eb63ae0 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -1,34 +1,34 @@ PODS: - boost (1.84.0) - - DatadogCore (2.30.2): - - DatadogInternal (= 2.30.2) - - DatadogCrashReporting (2.30.2): - - DatadogInternal (= 2.30.2) + - DatadogCore (3.0.0): + - DatadogInternal (= 3.0.0) + - DatadogCrashReporting (3.0.0): + - DatadogInternal (= 3.0.0) - PLCrashReporter (~> 1.12.0) - - DatadogInternal (2.30.2) - - DatadogLogs (2.30.2): - - DatadogInternal (= 2.30.2) - - DatadogRUM (2.30.2): - - DatadogInternal (= 2.30.2) - - DatadogSDKReactNative (2.13.0): - - DatadogCore (= 2.30.2) - - DatadogCrashReporting (= 2.30.2) - - DatadogLogs (= 2.30.2) - - DatadogRUM (= 2.30.2) - - DatadogTrace (= 2.30.2) - - DatadogWebViewTracking (= 2.30.2) + - DatadogInternal (3.0.0) + - DatadogLogs (3.0.0): + - DatadogInternal (= 3.0.0) + - DatadogRUM (3.0.0): + - DatadogInternal (= 3.0.0) + - DatadogSDKReactNative (2.12.1): + - DatadogCore (= 3.0.0) + - DatadogCrashReporting (= 3.0.0) + - DatadogLogs (= 3.0.0) + - DatadogRUM (= 3.0.0) + - DatadogTrace (= 3.0.0) + - DatadogWebViewTracking (= 3.0.0) - React-Core - - DatadogSDKReactNative/Tests (2.13.0): - - DatadogCore (= 2.30.2) - - DatadogCrashReporting (= 2.30.2) - - DatadogLogs (= 2.30.2) - - DatadogRUM (= 2.30.2) - - DatadogTrace (= 2.30.2) - - DatadogWebViewTracking (= 2.30.2) + - DatadogSDKReactNative/Tests (2.12.1): + - DatadogCore (= 3.0.0) + - DatadogCrashReporting (= 3.0.0) + - DatadogLogs (= 3.0.0) + - DatadogRUM (= 3.0.0) + - DatadogTrace (= 3.0.0) + - DatadogWebViewTracking (= 3.0.0) - React-Core - DatadogSDKReactNativeSessionReplay (2.13.0): - DatadogSDKReactNative - - DatadogSessionReplay (= 2.30.2) + - DatadogSessionReplay (= 3.0.0) - DoubleConversion - glog - hermes-engine @@ -51,7 +51,7 @@ PODS: - Yoga - DatadogSDKReactNativeSessionReplay/Tests (2.13.0): - DatadogSDKReactNative - - DatadogSessionReplay (= 2.30.2) + - DatadogSessionReplay (= 3.0.0) - DoubleConversion - glog - hermes-engine @@ -73,25 +73,25 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - DatadogSDKReactNativeWebView (2.13.0): - - DatadogInternal (= 2.30.2) + - DatadogSDKReactNativeWebView (2.12.1): + - DatadogInternal (= 3.0.0) - DatadogSDKReactNative - - DatadogWebViewTracking (= 2.30.2) + - DatadogWebViewTracking (= 3.0.0) - React-Core - - DatadogSDKReactNativeWebView/Tests (2.13.0): - - DatadogInternal (= 2.30.2) + - DatadogSDKReactNativeWebView/Tests (2.12.1): + - DatadogInternal (= 3.0.0) - DatadogSDKReactNative - - DatadogWebViewTracking (= 2.30.2) + - DatadogWebViewTracking (= 3.0.0) - React-Core - react-native-webview - React-RCTText - - DatadogSessionReplay (2.30.2): - - DatadogInternal (= 2.30.2) - - DatadogTrace (2.30.2): - - DatadogInternal (= 2.30.2) + - DatadogSessionReplay (3.0.0): + - DatadogInternal (= 3.0.0) + - DatadogTrace (3.0.0): + - DatadogInternal (= 3.0.0) - OpenTelemetrySwiftApi (= 1.13.1) - - DatadogWebViewTracking (2.30.2): - - DatadogInternal (= 2.30.2) + - DatadogWebViewTracking (3.0.0): + - DatadogInternal (= 3.0.0) - DoubleConversion (1.1.6) - fast_float (6.1.4) - FBLazyVector (0.76.9) @@ -1988,17 +1988,17 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: boost: 1dca942403ed9342f98334bf4c3621f011aa7946 - DatadogCore: 8e50ad6cb68343f701707f7eeca16e0acb52ed8c - DatadogCrashReporting: 34763d4276d4fce286c7e8729cfcd2ac04dca43b - DatadogInternal: bd8672d506a7e67936ed5f7ca612169e49029b44 - DatadogLogs: d683aa9e0c9339f5ae679ead70bbdbe41cdc32f6 - DatadogRUM: 8b794aa458e6323ea9b1cef3f820fd3d092cbe27 - DatadogSDKReactNative: 881e179daeb0af9b43a53c5a56a9a5084bdf4566 - DatadogSDKReactNativeSessionReplay: 9e75e392ebd1adc6bfc131def6fe82f098afd32b - DatadogSDKReactNativeWebView: 082e0b8a07d7eea11ce014b9f04410053ea3e179 - DatadogSessionReplay: 56a91d799fe34967c5ae79a222364e37d67020f5 - DatadogTrace: 3ba194791267efa09634234749111cac95abd3e5 - DatadogWebViewTracking: 8287d5ad06e992de5e46dd72a17e05c7513344be + DatadogCore: 0c39ba4ef3dee02071f235f2fb56af7e29f4ceb0 + DatadogCrashReporting: 604e65e524cb2fc22cda39fd9c9ea5fd3bddf24e + DatadogInternal: 442673a7fb5329299b489b91ed705aa2085380f7 + DatadogLogs: 0146a0e3140ba62fd42729593c0910d692aea5fd + DatadogRUM: f97fbd0290ecec5bc952fb03a6a1ae068649243f + DatadogSDKReactNative: e430b3f4d7adb0fac07b61e2fb65ceacdaf82b3e + DatadogSDKReactNativeSessionReplay: 4b2a3d166a79581f18522795b40141c34cf3685d + DatadogSDKReactNativeWebView: 35dc2b9736e1aaa82b366bf6b8a8a959a9b088c5 + DatadogSessionReplay: 0357b911c6ab42c77c93b29761ecc20d9282e971 + DatadogTrace: d714e7c456e612b7f171483d08086a59aa1c4213 + DatadogWebViewTracking: fb9591c09fca82b143f3843a7164a7e782175c53 DoubleConversion: f16ae600a246532c4020132d54af21d0ddb2a385 fast_float: 06eeec4fe712a76acc9376682e4808b05ce978b6 FBLazyVector: 7605ea4810e0e10ae4815292433c09bf4324ba45 diff --git a/packages/core/DatadogSDKReactNative.podspec b/packages/core/DatadogSDKReactNative.podspec index ff3b91232..a2f41ddd1 100644 --- a/packages/core/DatadogSDKReactNative.podspec +++ b/packages/core/DatadogSDKReactNative.podspec @@ -19,14 +19,14 @@ Pod::Spec.new do |s| s.dependency "React-Core" # /!\ Remember to keep the versions in sync with DatadogSDKReactNativeSessionReplay.podspec - s.dependency 'DatadogCore', '2.30.2' - s.dependency 'DatadogLogs', '2.30.2' - s.dependency 'DatadogTrace', '2.30.2' - s.dependency 'DatadogRUM', '2.30.2' - s.dependency 'DatadogCrashReporting', '2.30.2' + s.dependency 'DatadogCore', '3.0.0' + s.dependency 'DatadogLogs', '3.0.0' + s.dependency 'DatadogTrace', '3.0.0' + s.dependency 'DatadogRUM', '3.0.0' + s.dependency 'DatadogCrashReporting', '3.0.0' # DatadogWebViewTracking is not available for tvOS - s.ios.dependency 'DatadogWebViewTracking', '2.30.2' + s.ios.dependency 'DatadogWebViewTracking', '3.0.0' s.test_spec 'Tests' do |test_spec| test_spec.source_files = 'ios/Tests/**/*.{swift,json}' diff --git a/packages/core/android/build.gradle b/packages/core/android/build.gradle index 0396ae323..a8d09d4d1 100644 --- a/packages/core/android/build.gradle +++ b/packages/core/android/build.gradle @@ -201,16 +201,16 @@ dependencies { // This breaks builds if the React Native target is below 0.76.0. as it relies on Gradle 8.5.0. // To avoid this, we enforce 1.0.0-beta01 on RN < 0.76.0 if (reactNativeMinorVersion < 76) { - implementation("com.datadoghq:dd-sdk-android-rum:2.26.2") { + implementation("com.datadoghq:dd-sdk-android-rum:3.0.0") { exclude group: "androidx.metrics", module: "metrics-performance" } implementation "androidx.metrics:metrics-performance:1.0.0-beta01" } else { - implementation "com.datadoghq:dd-sdk-android-rum:2.26.2" + implementation "com.datadoghq:dd-sdk-android-rum:3.0.0" } - implementation "com.datadoghq:dd-sdk-android-logs:2.26.2" - implementation "com.datadoghq:dd-sdk-android-trace:2.26.2" - implementation "com.datadoghq:dd-sdk-android-webview:2.26.2" + implementation "com.datadoghq:dd-sdk-android-logs:3.0.0" + implementation "com.datadoghq:dd-sdk-android-trace:3.0.0" + implementation "com.datadoghq:dd-sdk-android-webview:3.0.0" implementation "com.google.code.gson:gson:2.10.0" testImplementation "org.junit.platform:junit-platform-launcher:1.6.2" testImplementation "org.junit.jupiter:junit-jupiter-api:5.6.2" diff --git a/packages/react-native-session-replay/DatadogSDKReactNativeSessionReplay.podspec b/packages/react-native-session-replay/DatadogSDKReactNativeSessionReplay.podspec index 915fc4129..3a2d5ff48 100644 --- a/packages/react-native-session-replay/DatadogSDKReactNativeSessionReplay.podspec +++ b/packages/react-native-session-replay/DatadogSDKReactNativeSessionReplay.podspec @@ -23,7 +23,7 @@ Pod::Spec.new do |s| s.dependency "React-Core" # /!\ Remember to keep the version in sync with DatadogSDKReactNative.podspec - s.dependency 'DatadogSessionReplay', '2.30.2' + s.dependency 'DatadogSessionReplay', '3.0.0' s.dependency 'DatadogSDKReactNative' s.test_spec 'Tests' do |test_spec| diff --git a/packages/react-native-session-replay/android/build.gradle b/packages/react-native-session-replay/android/build.gradle index afb5f5c09..6f8f35e52 100644 --- a/packages/react-native-session-replay/android/build.gradle +++ b/packages/react-native-session-replay/android/build.gradle @@ -214,8 +214,8 @@ dependencies { api "com.facebook.react:react-android:$reactNativeVersion" } implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" - implementation "com.datadoghq:dd-sdk-android-session-replay:2.26.2" - implementation "com.datadoghq:dd-sdk-android-internal:2.26.2" + implementation "com.datadoghq:dd-sdk-android-session-replay:3.0.0" + implementation "com.datadoghq:dd-sdk-android-internal:3.0.0" implementation project(path: ':datadog_mobile-react-native') testImplementation "org.junit.platform:junit-platform-launcher:1.6.2" diff --git a/packages/react-native-webview/DatadogSDKReactNativeWebView.podspec b/packages/react-native-webview/DatadogSDKReactNativeWebView.podspec index 3d4668bb0..26e160bbc 100644 --- a/packages/react-native-webview/DatadogSDKReactNativeWebView.podspec +++ b/packages/react-native-webview/DatadogSDKReactNativeWebView.podspec @@ -23,8 +23,8 @@ Pod::Spec.new do |s| end # /!\ Remember to keep the version in sync with DatadogSDKReactNative.podspec - s.dependency 'DatadogWebViewTracking', '2.30.2' - s.dependency 'DatadogInternal', '2.30.2' + s.dependency 'DatadogWebViewTracking', '3.0.0' + s.dependency 'DatadogInternal', '3.0.0' s.dependency 'DatadogSDKReactNative' s.test_spec 'Tests' do |test_spec| diff --git a/packages/react-native-webview/android/build.gradle b/packages/react-native-webview/android/build.gradle index 973b3ec5f..098eae019 100644 --- a/packages/react-native-webview/android/build.gradle +++ b/packages/react-native-webview/android/build.gradle @@ -190,7 +190,7 @@ dependencies { implementation "com.facebook.react:react-android:$reactNativeVersion" } - implementation "com.datadoghq:dd-sdk-android-webview:2.26.2" + implementation "com.datadoghq:dd-sdk-android-webview:3.0.0" implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation project(path: ':datadog_mobile-react-native') From 5470c6dba0bca59a58c69613ece5d06599c6452d Mon Sep 17 00:00:00 2001 From: Sergio Barrio Date: Fri, 12 Sep 2025 14:43:25 +0200 Subject: [PATCH 034/410] Remove setUser --- packages/codepush/__mocks__/react-native.ts | 3 - packages/core/__mocks__/react-native.ts | 3 - .../datadog/reactnative/DatadogSDKWrapper.kt | 10 -- .../com/datadog/reactnative/DatadogWrapper.kt | 17 -- .../reactnative/DdSdkImplementation.kt | 17 +- .../kotlin/com/datadog/reactnative/DdSdk.kt | 14 +- .../kotlin/com/datadog/reactnative/DdSdk.kt | 11 -- .../com/datadog/reactnative/DdSdkTest.kt | 158 ------------------ packages/core/ios/Sources/DdSdk.mm | 11 -- .../ios/Sources/DdSdkImplementation.swift | 15 +- packages/core/ios/Tests/DdSdkTests.swift | 132 --------------- packages/core/ios/Tests/MockRUMMonitor.swift | 16 ++ packages/core/jest/mock.js | 3 - packages/core/src/DdSdkReactNative.tsx | 25 +-- .../src/__tests__/DdSdkReactNative.test.tsx | 16 -- packages/core/src/logs/eventMapper.ts | 6 +- .../core/src/sdk/EventMappers/EventMapper.ts | 2 +- .../UserInfoSingleton/UserInfoSingleton.ts | 4 +- .../__tests__/UserInfoSingleton.test.ts | 6 +- .../core/src/sdk/UserInfoSingleton/types.ts | 5 +- packages/core/src/specs/NativeDdSdk.ts | 7 - packages/core/src/types.tsx | 10 +- .../__mocks__/react-native.ts | 3 - 23 files changed, 42 insertions(+), 452 deletions(-) diff --git a/packages/codepush/__mocks__/react-native.ts b/packages/codepush/__mocks__/react-native.ts index b35df4e31..046ced2f6 100644 --- a/packages/codepush/__mocks__/react-native.ts +++ b/packages/codepush/__mocks__/react-native.ts @@ -18,9 +18,6 @@ actualRN.NativeModules.DdSdk = { initialize: jest.fn().mockImplementation( () => new Promise(resolve => resolve()) ) as jest.MockedFunction, - setUser: jest.fn().mockImplementation( - () => new Promise(resolve => resolve()) - ) as jest.MockedFunction, setAttributes: jest.fn().mockImplementation( () => new Promise(resolve => resolve()) ) as jest.MockedFunction, diff --git a/packages/core/__mocks__/react-native.ts b/packages/core/__mocks__/react-native.ts index 9507ec33f..260fe68a7 100644 --- a/packages/core/__mocks__/react-native.ts +++ b/packages/core/__mocks__/react-native.ts @@ -18,9 +18,6 @@ actualRN.NativeModules.DdSdk = { initialize: jest.fn().mockImplementation( () => new Promise(resolve => resolve()) ) as jest.MockedFunction, - setUser: jest.fn().mockImplementation( - () => new Promise(resolve => resolve()) - ) as jest.MockedFunction, setUserInfo: jest.fn().mockImplementation( () => new Promise(resolve => resolve()) ) as jest.MockedFunction, diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogSDKWrapper.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogSDKWrapper.kt index da0841ac4..198061d14 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogSDKWrapper.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogSDKWrapper.kt @@ -71,16 +71,6 @@ internal class DatadogSDKWrapper : DatadogWrapper { DatadogSDKWrapperStorage.notifyOnInitializedListeners(core as InternalSdkCore) } - @Deprecated("Use setUserInfo instead; the user ID is now required.") - override fun setUser( - id: String?, - name: String?, - email: String?, - extraInfo: Map - ) { - Datadog.setUserInfo(id, name, email, extraInfo) - } - override fun setUserInfo( id: String, name: String?, diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogWrapper.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogWrapper.kt index 19b25e587..3ae3e6266 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogWrapper.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogWrapper.kt @@ -62,23 +62,6 @@ interface DatadogWrapper { consent: TrackingConsent ) - /** - * Sets the user information. - * - * @param id (nullable) a unique user identifier (relevant to your business domain) - * @param name (nullable) the user name or alias - * @param email (nullable) the user email - * @param extraInfo additional information. An extra information can be - * nested up to 8 levels deep. Keys using more than 8 levels will be sanitized by SDK. - */ - @Deprecated("Use setUserInfo instead; the user ID is now required.") - fun setUser( - id: String?, - name: String?, - email: String?, - extraInfo: Map - ) - /** * Sets the user information. * diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt index b04a2ddf3..7a5c6848c 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt @@ -78,21 +78,6 @@ class DdSdkImplementation( promise.resolve(null) } - /** - * Set the user information. - * @param user The user object (use builtin attributes: 'id', 'email', 'name', and/or any custom - * attribute). - */ - @Deprecated("Use setUserInfo instead; the user ID is now required.") - fun setUser(user: ReadableMap, promise: Promise) { - val extraInfo = user.toHashMap().toMutableMap() - val id = extraInfo.remove("id")?.toString() - val name = extraInfo.remove("name")?.toString() - val email = extraInfo.remove("email")?.toString() - datadog.setUser(id, name, email, extraInfo) - promise.resolve(null) - } - /** * Set the user information. * @param userInfo The user object (use builtin attributes: 'id', 'email', 'name', and any custom @@ -110,7 +95,7 @@ class DdSdkImplementation( if (id != null) { datadog.setUserInfo(id, name, email, extraInfo) } else { - datadog.setUser(null, name, email, extraInfo) + // TO DO - Log warning? } promise.resolve(null) diff --git a/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdSdk.kt b/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdSdk.kt index 5bc470947..cfafffffe 100644 --- a/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdSdk.kt +++ b/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdSdk.kt @@ -51,19 +51,7 @@ class DdSdk( /** * Set the user information. - * @param user The user object (use builtin attributes: 'id', 'email', 'name', and/or any custom - * attribute). - */ - @Deprecated("Use setUserInfo instead; the user ID is now required.") - @ReactMethod - override fun setUser(user: ReadableMap, promise: Promise) { - implementation.setUser(user, promise) - } - - /** - * Set the user information. - * @param user The user object (use builtin attributes: 'id', 'email', 'name', and any custom - * attribute inside 'extraInfo'). + * @param user The user object (use builtin attributes: 'id', 'email', 'name', and any custom * attribute inside 'extraInfo'). */ @ReactMethod override fun setUserInfo(user: ReadableMap, promise: Promise) { diff --git a/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt b/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt index 17acd6d20..97acb2ebf 100644 --- a/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt +++ b/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt @@ -75,17 +75,6 @@ class DdSdk( implementation.setAttributes(attributes, promise) } - /** - * Set the user information. - * @param user The user object (use builtin attributes: 'id', 'email', 'name', and/or any custom - * attribute). - */ - @Deprecated("Use setUserInfo instead; the user ID is now required.") - @ReactMethod - fun setUser(user: ReadableMap, promise: Promise) { - implementation.setUser(user, promise) - } - /** * Set the user information. * @param user The user object (use builtin attributes: 'id', 'email', 'name', and any custom * attribute inside 'extraInfo'). diff --git a/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt b/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt index a39485dae..8797bcc64 100644 --- a/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt +++ b/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt @@ -2810,164 +2810,6 @@ internal class DdSdkTest { // region misc - @Test - fun `𝕄 set native user info 𝕎 setUser()`( - @MapForgery( - key = AdvancedForgery(string = [StringForgery(StringForgeryType.NUMERICAL)]), - value = AdvancedForgery(string = [StringForgery(StringForgeryType.ASCII)]) - ) extraInfo: Map - ) { - // When - testedBridgeSdk.setUser(extraInfo.toReadableMap(), mockPromise) - - // Then - argumentCaptor> { - verify(mockDatadog) - .setUser( - isNull(), - isNull(), - isNull(), - capture() - ) - - assertThat(firstValue) - .containsAllEntriesOf(extraInfo) - .hasSize(extraInfo.size) - } - } - - @Test - fun `𝕄 set native user info 𝕎 setUser() {with id}`( - @StringForgery id: String, - @MapForgery( - key = AdvancedForgery(string = [StringForgery(StringForgeryType.NUMERICAL)]), - value = AdvancedForgery(string = [StringForgery(StringForgeryType.ASCII)]) - ) extraInfo: Map - ) { - // Given - val user = extraInfo.toMutableMap().also { - it.put("id", id) - } - - // When - testedBridgeSdk.setUser(user.toReadableMap(), mockPromise) - - // Then - argumentCaptor> { - verify(mockDatadog) - .setUser( - eq(id), - isNull(), - isNull(), - capture() - ) - - assertThat(firstValue) - .containsAllEntriesOf(extraInfo) - .hasSize(extraInfo.size) - } - } - - @Test - fun `𝕄 set native user info 𝕎 setUser() {with name}`( - @StringForgery name: String, - @MapForgery( - key = AdvancedForgery(string = [StringForgery(StringForgeryType.NUMERICAL)]), - value = AdvancedForgery(string = [StringForgery(StringForgeryType.ASCII)]) - ) extraInfo: Map - ) { - // Given - val user = extraInfo.toMutableMap().also { - it.put("name", name) - } - - // When - testedBridgeSdk.setUser(user.toReadableMap(), mockPromise) - - // Then - argumentCaptor> { - verify(mockDatadog) - .setUser( - isNull(), - eq(name), - isNull(), - capture() - ) - - assertThat(firstValue) - .containsAllEntriesOf(extraInfo) - .hasSize(extraInfo.size) - } - } - - @Test - fun `𝕄 set native user info 𝕎 setUser() {with email}`( - @StringForgery(regex = "\\w+@\\w+\\.[a-z]{3}") email: String, - @MapForgery( - key = AdvancedForgery(string = [StringForgery(StringForgeryType.NUMERICAL)]), - value = AdvancedForgery(string = [StringForgery(StringForgeryType.ASCII)]) - ) extraInfo: Map - ) { - // Given - val user = extraInfo.toMutableMap().also { - it.put("email", email) - } - - // When - testedBridgeSdk.setUser(user.toReadableMap(), mockPromise) - - // Then - argumentCaptor> { - verify(mockDatadog) - .setUser( - isNull(), - isNull(), - eq(email), - capture() - ) - - assertThat(firstValue) - .containsAllEntriesOf(extraInfo) - .hasSize(extraInfo.size) - } - } - - @Test - fun `𝕄 set native user info 𝕎 setUser() {with id, name and email}`( - @StringForgery id: String, - @StringForgery name: String, - @StringForgery(regex = "\\w+@\\w+\\.[a-z]{3}") email: String, - @MapForgery( - key = AdvancedForgery(string = [StringForgery(StringForgeryType.NUMERICAL)]), - value = AdvancedForgery(string = [StringForgery(StringForgeryType.ASCII)]) - ) extraInfo: Map - ) { - // Given - val user = extraInfo.toMutableMap().also { - it.put("id", id) - it.put("name", name) - it.put("email", email) - } - - // When - testedBridgeSdk.setUser(user.toReadableMap(), mockPromise) - - // Then - argumentCaptor> { - verify(mockDatadog) - .setUser( - eq(id), - eq(name), - eq(email), - capture() - ) - - assertThat(firstValue) - .containsAllEntriesOf(extraInfo) - .hasSize(extraInfo.size) - } - } - @Test fun `𝕄 set native user info 𝕎 setUserInfo() {with id}`( @StringForgery id: String diff --git a/packages/core/ios/Sources/DdSdk.mm b/packages/core/ios/Sources/DdSdk.mm index 663da0a79..3ead770ac 100644 --- a/packages/core/ios/Sources/DdSdk.mm +++ b/packages/core/ios/Sources/DdSdk.mm @@ -37,13 +37,6 @@ + (void)initFromNative { [self setAttributes:attributes resolve:resolve reject:reject]; } -RCT_REMAP_METHOD(setUser, withUser:(NSDictionary*)user - withResolver:(RCTPromiseResolveBlock)resolve - withRejecter:(RCTPromiseRejectBlock)reject) -{ - [self setUser:user resolve:resolve reject:reject]; -} - RCT_REMAP_METHOD(setUserInfo, withUserInfo:(NSDictionary*)userInfo withResolver:(RCTPromiseResolveBlock)resolve withRejecter:(RCTPromiseRejectBlock)reject) @@ -144,10 +137,6 @@ - (void)setTrackingConsent:(NSString *)trackingConsent resolve:(RCTPromiseResolv [self.ddSdkImplementation setTrackingConsentWithTrackingConsent:trackingConsent resolve:resolve reject:reject]; } -- (void)setUser:(NSDictionary *)user resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { - [self.ddSdkImplementation setUserWithUser:user resolve:resolve reject:reject]; -} - - (void)setUserInfo:(NSDictionary *)userInfo resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { [self.ddSdkImplementation setUserInfoWithUserInfo:userInfo resolve:resolve reject:reject]; } diff --git a/packages/core/ios/Sources/DdSdkImplementation.swift b/packages/core/ios/Sources/DdSdkImplementation.swift index 2f66f868c..5a4bbf9a5 100644 --- a/packages/core/ios/Sources/DdSdkImplementation.swift +++ b/packages/core/ios/Sources/DdSdkImplementation.swift @@ -81,18 +81,6 @@ public class DdSdkImplementation: NSObject { resolve(nil) } - @objc - public func setUser(user: NSDictionary, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { - var castedUser = castAttributesToSwift(user) - let id = castedUser.removeValue(forKey: "id") as? String - let name = castedUser.removeValue(forKey: "name") as? String - let email = castedUser.removeValue(forKey: "email") as? String - let extraInfo: [String: Encodable] = castedUser // everything what's left is an `extraInfo` - - Datadog.setUserInfo(id: id, name: name, email: email, extraInfo: extraInfo) - resolve(nil) - } - @objc public func setUserInfo(userInfo: NSDictionary, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { let castedUserInfo = castAttributesToSwift(userInfo) @@ -109,8 +97,9 @@ public class DdSdkImplementation: NSObject { if let validId = id { Datadog.setUserInfo(id: validId, name: name, email: email, extraInfo: extraInfo) } else { - Datadog.setUserInfo(name: name, email: email, extraInfo: extraInfo) + // TO DO - log warning message? } + resolve(nil) } diff --git a/packages/core/ios/Tests/DdSdkTests.swift b/packages/core/ios/Tests/DdSdkTests.swift index bcc3252a8..6be5e0994 100644 --- a/packages/core/ios/Tests/DdSdkTests.swift +++ b/packages/core/ios/Tests/DdSdkTests.swift @@ -535,85 +535,6 @@ class DdSdkTests: XCTestCase { XCTAssertEqual(ddConfig.trackFrustrations, false) } - func testSetUser() throws { - let bridge = DdSdkImplementation( - mainDispatchQueue: DispatchQueueMock(), - jsDispatchQueue: DispatchQueueMock(), - jsRefreshRateMonitor: JSRefreshRateMonitor(), - RUMMonitorProvider: { MockRUMMonitor() }, - RUMMonitorInternalProvider: { nil } - ) - bridge.initialize( - configuration: .mockAny(), - resolve: mockResolve, - reject: mockReject - ) - - bridge.setUser( - user: NSDictionary( - dictionary: [ - "id": "id_123", - "name": "John Doe", - "email": "john@doe.com", - "extra-info-1": 123, - "extra-info-2": "abc", - "extra-info-3": true, - ] - ), - resolve: mockResolve, - reject: mockReject - ) - - let ddContext = try XCTUnwrap(CoreRegistry.default as? DatadogCore).contextProvider.read() - let userInfo = try XCTUnwrap(ddContext.userInfo) - - XCTAssertEqual(userInfo.id, "id_123") - XCTAssertEqual(userInfo.name, "John Doe") - XCTAssertEqual(userInfo.email, "john@doe.com") - XCTAssertEqual(userInfo.extraInfo["extra-info-1"] as? Int64, 123) - XCTAssertEqual(userInfo.extraInfo["extra-info-2"] as? String, "abc") - XCTAssertEqual(userInfo.extraInfo["extra-info-3"] as? Bool, true) - } - - func testSetUserOptionalId() throws { - let bridge = DdSdkImplementation( - mainDispatchQueue: DispatchQueueMock(), - jsDispatchQueue: DispatchQueueMock(), - jsRefreshRateMonitor: JSRefreshRateMonitor(), - RUMMonitorProvider: { MockRUMMonitor() }, - RUMMonitorInternalProvider: { nil } - ) - bridge.initialize( - configuration: .mockAny(), - resolve: mockResolve, - reject: mockReject - ) - - bridge.setUser( - user: NSDictionary( - dictionary: [ - "name": "John Doe", - "email": "john@doe.com", - "extra-info-1": 123, - "extra-info-2": "abc", - "extra-info-3": true, - ] - ), - resolve: mockResolve, - reject: mockReject - ) - - let ddContext = try XCTUnwrap(CoreRegistry.default as? DatadogCore).contextProvider.read() - let userInfo = try XCTUnwrap(ddContext.userInfo) - - XCTAssertEqual(userInfo.id, nil) - XCTAssertEqual(userInfo.name, "John Doe") - XCTAssertEqual(userInfo.email, "john@doe.com") - XCTAssertEqual(userInfo.extraInfo["extra-info-1"] as? Int64, 123) - XCTAssertEqual(userInfo.extraInfo["extra-info-2"] as? String, "abc") - XCTAssertEqual(userInfo.extraInfo["extra-info-3"] as? Bool, true) - } - func testSetUserInfo() throws { let bridge = DdSdkImplementation( mainDispatchQueue: DispatchQueueMock(), @@ -668,59 +589,6 @@ class DdSdkTests: XCTestCase { } } - func testSetUserInfoOptionalId() throws { - let bridge = DdSdkImplementation( - mainDispatchQueue: DispatchQueueMock(), - jsDispatchQueue: DispatchQueueMock(), - jsRefreshRateMonitor: JSRefreshRateMonitor(), - RUMMonitorProvider: { MockRUMMonitor() }, - RUMMonitorInternalProvider: { nil } - ) - bridge.initialize( - configuration: .mockAny(), - resolve: mockResolve, - reject: mockReject - ) - - bridge.setUserInfo( - userInfo: NSDictionary( - dictionary: [ - "name": "John Doe", - "email": "john@doe.com", - "extraInfo": [ - "extra-info-1": 123, - "extra-info-2": "abc", - "extra-info-3": true, - "extra-info-4": [ - "nested-extra-info-1": 456 - ], - ], - ] - ), - resolve: mockResolve, - reject: mockReject - ) - - let ddContext = try XCTUnwrap(CoreRegistry.default as? DatadogCore).contextProvider.read() - let userInfo = try XCTUnwrap(ddContext.userInfo) - - XCTAssertEqual(userInfo.id, nil) - XCTAssertEqual(userInfo.name, "John Doe") - XCTAssertEqual(userInfo.email, "john@doe.com") - XCTAssertEqual(userInfo.extraInfo["extra-info-1"] as? Int64, 123) - XCTAssertEqual(userInfo.extraInfo["extra-info-2"] as? String, "abc") - XCTAssertEqual(userInfo.extraInfo["extra-info-3"] as? Bool, true) - - if let extraInfo4Encodable = userInfo.extraInfo["extra-info-4"] - as? DatadogSDKReactNative.AnyEncodable, - let extraInfo4Dict = extraInfo4Encodable.value as? [String: Int] - { - XCTAssertEqual(extraInfo4Dict, ["nested-extra-info-1": 456]) - } else { - XCTFail("extra-info-4 is not of expected type or value") - } - } - func testAddUserExtraInfo() throws { let bridge = DdSdkImplementation( mainDispatchQueue: DispatchQueueMock(), diff --git a/packages/core/ios/Tests/MockRUMMonitor.swift b/packages/core/ios/Tests/MockRUMMonitor.swift index fe17e7748..f0fa03364 100644 --- a/packages/core/ios/Tests/MockRUMMonitor.swift +++ b/packages/core/ios/Tests/MockRUMMonitor.swift @@ -10,6 +10,22 @@ @testable import DatadogSDKReactNative internal class MockRUMMonitor: RUMMonitorProtocol { + func addViewAttribute(forKey key: DatadogInternal.AttributeKey, value: any DatadogInternal.AttributeValue) { + // not implemented + } + + func addViewAttributes(_ attributes: [DatadogInternal.AttributeKey : any DatadogInternal.AttributeValue]) { + // not implemented + } + + func removeViewAttribute(forKey key: DatadogInternal.AttributeKey) { + // not implemented + } + + func removeViewAttributes(forKeys keys: [DatadogInternal.AttributeKey]) { + // not implemented + } + func currentSessionID(completion: @escaping (String?) -> Void) { // not implemented } diff --git a/packages/core/jest/mock.js b/packages/core/jest/mock.js index 9c08f5335..a8161295a 100644 --- a/packages/core/jest/mock.js +++ b/packages/core/jest/mock.js @@ -27,9 +27,6 @@ module.exports = { .fn() .mockImplementation(() => new Promise(resolve => resolve())), isInitialized: jest.fn().mockImplementation(() => true), - setUser: jest - .fn() - .mockImplementation(() => new Promise(resolve => resolve())), setUserInfo: jest .fn() .mockImplementation(() => new Promise(resolve => resolve())), diff --git a/packages/core/src/DdSdkReactNative.tsx b/packages/core/src/DdSdkReactNative.tsx index aa7e2ec36..668ae09f3 100644 --- a/packages/core/src/DdSdkReactNative.tsx +++ b/packages/core/src/DdSdkReactNative.tsx @@ -38,7 +38,6 @@ import { DdSdk } from './sdk/DdSdk'; import { FileBasedConfiguration } from './sdk/FileBasedConfiguration/FileBasedConfiguration'; import { GlobalState } from './sdk/GlobalState/GlobalState'; import { UserInfoSingleton } from './sdk/UserInfoSingleton/UserInfoSingleton'; -import type { UserInfo } from './sdk/UserInfoSingleton/types'; import { DdSdkConfiguration } from './types'; import { adaptLongTaskThreshold } from './utils/longTasksUtils'; import { version as sdkVersion } from './version'; @@ -192,22 +191,6 @@ export class DdSdkReactNative { AttributesSingleton.getInstance().setAttributes(attributes); }; - /** - * Set the user information. - * @deprecated UserInfo id property is now mandatory (please user setUserInfo instead) - * @param user: The user object (use builtin attributes: 'id', 'email', 'name', and/or any custom attribute). - * @returns a Promise. - */ - // eslint-disable-next-line @typescript-eslint/ban-types - static setUser = async (user: UserInfo): Promise => { - InternalLog.log( - `Setting user ${JSON.stringify(user)}`, - SdkVerbosity.DEBUG - ); - await DdSdk.setUser(user); - UserInfoSingleton.getInstance().setUserInfo(user); - }; - /** * Sets the user information. * @param id: A mandatory unique user identifier (relevant to your business domain). @@ -245,6 +228,14 @@ export class DdSdkReactNative { ); const userInfo = UserInfoSingleton.getInstance().getUserInfo(); + if (!userInfo) { + InternalLog.log( + 'Skipped adding User Extra Info: User Info is currently undefined. A user ID must be set before adding extra info. Please call setUserInfo() first.', + SdkVerbosity.WARN + ); + + return; + } const updatedUserInfo = { ...userInfo, extraInfo: { diff --git a/packages/core/src/__tests__/DdSdkReactNative.test.tsx b/packages/core/src/__tests__/DdSdkReactNative.test.tsx index 18bf060ce..f9405aa51 100644 --- a/packages/core/src/__tests__/DdSdkReactNative.test.tsx +++ b/packages/core/src/__tests__/DdSdkReactNative.test.tsx @@ -63,7 +63,6 @@ beforeEach(async () => { DdSdkReactNative['wasAutoInstrumented'] = false; NativeModules.DdSdk.initialize.mockClear(); NativeModules.DdSdk.setAttributes.mockClear(); - NativeModules.DdSdk.setUser.mockClear(); NativeModules.DdSdk.setTrackingConsent.mockClear(); NativeModules.DdSdk.onRUMSessionStarted.mockClear(); @@ -1064,21 +1063,6 @@ describe('DdSdkReactNative', () => { }); }); - describe('setUser', () => { - it('calls SDK method when setUser, and sets the user in UserProvider', async () => { - // GIVEN - const user = { id: 'id', foo: 'bar' }; - - // WHEN - await DdSdkReactNative.setUser(user); - - // THEN - expect(DdSdk.setUser).toHaveBeenCalledTimes(1); - expect(DdSdk.setUser).toHaveBeenCalledWith(user); - expect(UserInfoSingleton.getInstance().getUserInfo()).toEqual(user); - }); - }); - describe('setUserInfo', () => { it('calls SDK method when setUserInfo, and sets the user in UserProvider', async () => { // GIVEN diff --git a/packages/core/src/logs/eventMapper.ts b/packages/core/src/logs/eventMapper.ts index eb7b5f22c..939e882a5 100644 --- a/packages/core/src/logs/eventMapper.ts +++ b/packages/core/src/logs/eventMapper.ts @@ -31,13 +31,15 @@ export const formatRawLogToNativeEvent = ( export const formatRawLogToLogEvent = ( rawLog: RawLog | RawLogWithError, additionalInformation: { - userInfo: UserInfo; + userInfo?: UserInfo; attributes: Attributes; } ): LogEvent => { + const userInfo = additionalInformation?.userInfo; + return { ...rawLog, - userInfo: additionalInformation.userInfo, + ...(userInfo !== undefined ? { userInfo } : {}), attributes: additionalInformation.attributes }; }; diff --git a/packages/core/src/sdk/EventMappers/EventMapper.ts b/packages/core/src/sdk/EventMappers/EventMapper.ts index beafcf420..9ca252d72 100644 --- a/packages/core/src/sdk/EventMappers/EventMapper.ts +++ b/packages/core/src/sdk/EventMappers/EventMapper.ts @@ -15,7 +15,7 @@ import type { UserInfo } from '../UserInfoSingleton/types'; import { deepClone } from './utils/deepClone'; export type AdditionalEventDataForMapper = { - userInfo: UserInfo; + userInfo?: UserInfo; attributes: Attributes; }; diff --git a/packages/core/src/sdk/UserInfoSingleton/UserInfoSingleton.ts b/packages/core/src/sdk/UserInfoSingleton/UserInfoSingleton.ts index c3862aabc..26392d794 100644 --- a/packages/core/src/sdk/UserInfoSingleton/UserInfoSingleton.ts +++ b/packages/core/src/sdk/UserInfoSingleton/UserInfoSingleton.ts @@ -7,13 +7,13 @@ import type { UserInfo } from './types'; class UserInfoProvider { - private userInfo: UserInfo = {}; + private userInfo: UserInfo | undefined = undefined; setUserInfo = (userInfo: UserInfo) => { this.userInfo = userInfo; }; - getUserInfo = (): UserInfo => { + getUserInfo = (): UserInfo | undefined => { return this.userInfo; }; } diff --git a/packages/core/src/sdk/UserInfoSingleton/__tests__/UserInfoSingleton.test.ts b/packages/core/src/sdk/UserInfoSingleton/__tests__/UserInfoSingleton.test.ts index 78a722c01..1f7ae84e7 100644 --- a/packages/core/src/sdk/UserInfoSingleton/__tests__/UserInfoSingleton.test.ts +++ b/packages/core/src/sdk/UserInfoSingleton/__tests__/UserInfoSingleton.test.ts @@ -9,6 +9,7 @@ import { UserInfoSingleton } from '../UserInfoSingleton'; describe('UserInfoSingleton', () => { it('sets, returns and resets the user info', () => { UserInfoSingleton.getInstance().setUserInfo({ + id: 'test', email: 'user@mail.com', extraInfo: { loggedIn: true @@ -16,6 +17,7 @@ describe('UserInfoSingleton', () => { }); expect(UserInfoSingleton.getInstance().getUserInfo()).toEqual({ + id: 'test', email: 'user@mail.com', extraInfo: { loggedIn: true @@ -24,6 +26,8 @@ describe('UserInfoSingleton', () => { UserInfoSingleton.reset(); - expect(UserInfoSingleton.getInstance().getUserInfo()).toEqual({}); + expect(UserInfoSingleton.getInstance().getUserInfo()).toEqual( + undefined + ); }); }); diff --git a/packages/core/src/sdk/UserInfoSingleton/types.ts b/packages/core/src/sdk/UserInfoSingleton/types.ts index 97a03ae7f..dd14eb150 100644 --- a/packages/core/src/sdk/UserInfoSingleton/types.ts +++ b/packages/core/src/sdk/UserInfoSingleton/types.ts @@ -5,11 +5,8 @@ */ export type UserInfo = { - readonly id?: string /** @deprecated To be made mandatory when removing DdSdkReactnative.setUser */; + readonly id: string; readonly name?: string; readonly email?: string; readonly extraInfo?: Record; - readonly [ - key: string - ]: unknown /** @deprecated To be removed alongside DdSdkReactnative.setUser */; }; diff --git a/packages/core/src/specs/NativeDdSdk.ts b/packages/core/src/specs/NativeDdSdk.ts index 6f1ce82a5..bbf2572ee 100644 --- a/packages/core/src/specs/NativeDdSdk.ts +++ b/packages/core/src/specs/NativeDdSdk.ts @@ -31,13 +31,6 @@ export interface Spec extends TurboModule { */ setAttributes(attributes: Object): Promise; - /** - * Set the user information. - * @deprecated: Use setUserInfo instead - * @param user: The user object (use builtin attributes: 'id', 'email', 'name', and/or any custom attribute). - */ - setUser(user: Object): Promise; - /** * Set the user information. * @param user: The user object (use builtin attributes: 'id', 'email', 'name', and any custom attribute under extraInfo). diff --git a/packages/core/src/types.tsx b/packages/core/src/types.tsx index e1c5096fb..fe1a5895c 100644 --- a/packages/core/src/types.tsx +++ b/packages/core/src/types.tsx @@ -5,7 +5,6 @@ */ import type { BatchProcessingLevel } from './DdSdkReactNativeConfiguration'; -import type { UserInfo as UserInfoSingleton } from './sdk/UserInfoSingleton/types'; declare global { // eslint-disable-next-line no-var, vars-on-top @@ -90,13 +89,6 @@ export type DdSdkType = { */ setAttributes(attributes: object): Promise; - /** - * Sets the user information. - * @deprecated UserInfo id property is now mandatory (please user setUserInfo instead) - * @param user: The user object (use builtin attributes: 'id', 'email', 'name', and/or any custom attribute). - */ - setUser(user: object): Promise; - /** * Sets the user information. * @param id: A unique user identifier (relevant to your business domain) @@ -173,7 +165,7 @@ export type LogEvent = { readonly source?: ErrorSource; // readonly date: number; // TODO: RUMM-2446 & RUMM-2447 readonly status: LogStatus; - readonly userInfo: UserInfoSingleton; + readonly userInfo?: UserInfo; readonly attributes?: object; }; diff --git a/packages/react-native-apollo-client/__mocks__/react-native.ts b/packages/react-native-apollo-client/__mocks__/react-native.ts index b35df4e31..046ced2f6 100644 --- a/packages/react-native-apollo-client/__mocks__/react-native.ts +++ b/packages/react-native-apollo-client/__mocks__/react-native.ts @@ -18,9 +18,6 @@ actualRN.NativeModules.DdSdk = { initialize: jest.fn().mockImplementation( () => new Promise(resolve => resolve()) ) as jest.MockedFunction, - setUser: jest.fn().mockImplementation( - () => new Promise(resolve => resolve()) - ) as jest.MockedFunction, setAttributes: jest.fn().mockImplementation( () => new Promise(resolve => resolve()) ) as jest.MockedFunction, From 37a43fa19ae4ad465c38f5092ac2fb5a781653d2 Mon Sep 17 00:00:00 2001 From: Sergio Barrio Date: Fri, 12 Sep 2025 17:09:36 +0200 Subject: [PATCH 035/410] Update Tracer imports for Android to remove opentracing dependencies --- .../reactnative/DdTraceImplementation.kt | 39 ++++++++++--------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdTraceImplementation.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdTraceImplementation.kt index 901ec3f6a..3ed77eb75 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdTraceImplementation.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdTraceImplementation.kt @@ -6,30 +6,28 @@ package com.datadog.reactnative -import com.datadog.android.trace.AndroidTracer -import com.datadog.android.trace.Trace -import com.datadog.android.trace.TraceConfiguration +import com.datadog.android.Datadog +import com.datadog.android.trace.DatadogTracing import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReadableMap -import io.opentracing.Scope -import io.opentracing.Span -import io.opentracing.Tracer -import io.opentracing.util.GlobalTracer +import com.datadog.android.trace.api.scope.DatadogScope +import com.datadog.android.trace.api.span.DatadogSpan +import com.datadog.android.trace.api.tracer.DatadogTracer +import com.datadog.android.trace.GlobalDatadogTracer import java.util.concurrent.TimeUnit /** * The entry point to use Datadog's Trace feature. */ class DdTraceImplementation( - private val tracerProvider: () -> Tracer = { - val tracer = AndroidTracer.Builder().build() - GlobalTracer.registerIfAbsent(tracer) - - GlobalTracer.get() + private val tracerProvider: () -> DatadogTracer = { + val tracer = DatadogTracing.newTracerBuilder(Datadog.getInstance()).build() + GlobalDatadogTracer.registerIfAbsent(tracer) + GlobalDatadogTracer.get() } ) { - private val spanMap: MutableMap = mutableMapOf() - private val scopeMap: MutableMap = mutableMapOf() + private val spanMap: MutableMap = mutableMapOf() + private val scopeMap: MutableMap = mutableMapOf() // lazy here is on purpose. The thing is that this class will be instantiated even // before Sdk.initialize is called, but Tracer can be created only after SDK is initialized. @@ -47,15 +45,18 @@ class DdTraceImplementation( .start() // This is required for traces to be able to be bundled with logs. - val scope = tracer.scopeManager().activate(span) - + val scope = tracer.activateSpan(span) val spanContext = span.context() span.setTags(context.toHashMap()) span.setTags(GlobalState.globalAttributes) - val spanId = spanContext.toSpanId() + val spanId = spanContext.spanId.toString() + spanMap[spanId] = span - scopeMap[spanId] = scope + if (scope != null) { + scopeMap[spanId] = scope + } + promise.resolve(spanId) } @@ -82,7 +83,7 @@ class DdTraceImplementation( promise.resolve(null) } - private fun Span.setTags(tags: Map) { + private fun DatadogSpan.setTags(tags: Map) { for ((key, value) in tags) { when (value) { is Boolean -> setTag(key, value) From e201337241f7ab4c9e54172bd10d6a4a0ef7cda2 Mon Sep 17 00:00:00 2001 From: Sergio Barrio Date: Thu, 18 Sep 2025 10:13:53 +0200 Subject: [PATCH 036/410] Fix android tests --- .../com/datadog/reactnative/DdTraceTest.kt | 84 ++++++++++--------- .../com/datadog/tools/unit/MockRumMonitor.kt | 10 +-- 2 files changed, 47 insertions(+), 47 deletions(-) diff --git a/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdTraceTest.kt b/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdTraceTest.kt index 16d459a57..8c22f88e1 100644 --- a/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdTraceTest.kt +++ b/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdTraceTest.kt @@ -6,6 +6,12 @@ package com.datadog.reactnative +import com.datadog.android.trace.api.scope.DatadogScope +import com.datadog.android.trace.api.span.DatadogSpan +import com.datadog.android.trace.api.span.DatadogSpanBuilder +import com.datadog.android.trace.api.span.DatadogSpanContext +import com.datadog.android.trace.api.trace.DatadogTraceId +import com.datadog.android.trace.api.tracer.DatadogTracer import com.datadog.tools.unit.toReadableMap import com.facebook.react.bridge.Promise import fr.xgouchet.elmyr.annotation.AdvancedForgery @@ -15,11 +21,6 @@ import fr.xgouchet.elmyr.annotation.MapForgery import fr.xgouchet.elmyr.annotation.StringForgery import fr.xgouchet.elmyr.annotation.StringForgeryType import fr.xgouchet.elmyr.junit5.ForgeExtension -import io.opentracing.Scope -import io.opentracing.ScopeManager -import io.opentracing.Span -import io.opentracing.SpanContext -import io.opentracing.Tracer import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Assumptions.assumeTrue @@ -51,22 +52,19 @@ internal class DdTraceTest { lateinit var testedTrace: DdTraceImplementation @Mock - lateinit var mockTracer: Tracer + lateinit var mockTracer: DatadogTracer @Mock - lateinit var mockSpanBuilder: Tracer.SpanBuilder + lateinit var mockSpanBuilder: DatadogSpanBuilder @Mock - lateinit var mockSpanContext: SpanContext + lateinit var mockSpanContext: DatadogSpanContext @Mock - lateinit var mockScopeManager: ScopeManager + lateinit var mockSpan: DatadogSpan @Mock - lateinit var mockSpan: Span - - @Mock - lateinit var mockScope: Scope + lateinit var mockScope: DatadogScope @StringForgery lateinit var fakeOperation: String @@ -74,11 +72,11 @@ internal class DdTraceTest { @DoubleForgery(1000000000000.0, 2000000000000.0) var fakeTimestamp: Double = 0.0 - @StringForgery(type = StringForgeryType.HEXADECIMAL) - lateinit var fakeSpanId: String + @LongForgery(100L, 2000L) + var fakeSpanId: Long = 0 - @StringForgery(type = StringForgeryType.HEXADECIMAL) - lateinit var fakeTraceId: String + @Mock + lateinit var fakeTraceId: DatadogTraceId @MapForgery( key = AdvancedForgery(string = [StringForgery()]), @@ -102,7 +100,6 @@ internal class DdTraceTest { @BeforeEach fun `set up`() { whenever(mockTracer.buildSpan(fakeOperation)) doReturn mockSpanBuilder - whenever(mockTracer.scopeManager()) doReturn mockScopeManager whenever( mockSpanBuilder.withStartTimestamp( fakeTimestamp.toLong() * 1000 @@ -110,9 +107,9 @@ internal class DdTraceTest { ) doReturn mockSpanBuilder whenever(mockSpanBuilder.start()) doReturn mockSpan whenever(mockSpan.context()) doReturn mockSpanContext - whenever(mockSpanContext.toSpanId()) doReturn fakeSpanId - whenever(mockSpanContext.toTraceId()) doReturn fakeTraceId - whenever(mockScopeManager.activate(mockSpan)) doReturn mockScope + whenever(mockSpanContext.spanId) doReturn fakeSpanId + whenever(mockSpanContext.traceId) doReturn fakeTraceId + whenever(mockTracer.activateSpan(mockSpan)) doReturn mockScope testedTrace = DdTraceImplementation(tracerProvider = { mockTracer }) } @@ -133,7 +130,7 @@ internal class DdTraceTest { ) // Then - assertThat(lastResolvedValue).isEqualTo(fakeSpanId) + assertThat(lastResolvedValue.toString()).isEqualTo(fakeSpanId.toString()) } @Test @@ -154,18 +151,20 @@ internal class DdTraceTest { fakeTimestamp, mockPromise ) - val id = lastResolvedValue - testedTrace.finishSpan(id as String, fakeContext.toReadableMap(), endTimestamp, mockPromise) + val id = lastResolvedValue.toString() + testedTrace.finishSpan(id, fakeContext.toReadableMap(), endTimestamp, mockPromise) // Then - assertThat(id).isEqualTo(fakeSpanId) + assertThat(id).isEqualTo(fakeSpanId.toString()) verify(mockSpan).finish(endTimestamp.toLong() * 1000) } @Test fun `M do nothing W startSpan() + finishSpan() with unknown id`( @LongForgery(100L, 2000L) duration: Long, - @StringForgery(type = StringForgeryType.HEXADECIMAL) otherSpanId: String + @StringForgery(type = StringForgeryType.HEXADECIMAL) + @LongForgery(100L, 2000L) + otherSpanId: Long ) { // Given assumeTrue(otherSpanId != fakeSpanId) @@ -178,11 +177,16 @@ internal class DdTraceTest { fakeTimestamp, mockPromise ) - val id = lastResolvedValue - testedTrace.finishSpan(otherSpanId, fakeContext.toReadableMap(), endTimestamp, mockPromise) + val id = lastResolvedValue.toString() + testedTrace.finishSpan( + otherSpanId.toString(), + fakeContext.toReadableMap(), + endTimestamp, + mockPromise + ) // Then - assertThat(id).isEqualTo(fakeSpanId) + assertThat(id).isEqualTo(fakeSpanId.toString()) verify(mockSpan, never()).finish(any()) } @@ -200,7 +204,7 @@ internal class DdTraceTest { fakeTimestamp, mockPromise ) - val id = lastResolvedValue + val id = lastResolvedValue.toString() testedTrace.finishSpan( id as String, emptyMap().toReadableMap(), @@ -209,7 +213,7 @@ internal class DdTraceTest { ) // Then - assertThat(id).isEqualTo(fakeSpanId) + assertThat(id).isEqualTo(fakeSpanId.toString()) verify(mockSpan).context() verify(mockSpan).finish(endTimestamp.toLong() * 1000) fakeContext.forEach { @@ -232,11 +236,11 @@ internal class DdTraceTest { fakeTimestamp, mockPromise ) - val id = lastResolvedValue - testedTrace.finishSpan(id as String, fakeContext.toReadableMap(), endTimestamp, mockPromise) + val id = lastResolvedValue.toString() + testedTrace.finishSpan(id, fakeContext.toReadableMap(), endTimestamp, mockPromise) // Then - assertThat(id).isEqualTo(fakeSpanId) + assertThat(id).isEqualTo(fakeSpanId.toString()) verify(mockSpan).context() verify(mockSpan).finish(endTimestamp.toLong() * 1000) fakeContext.forEach { @@ -262,16 +266,16 @@ internal class DdTraceTest { fakeTimestamp, mockPromise ) - val id = lastResolvedValue + val id = lastResolvedValue.toString() testedTrace.finishSpan( - id as String, + id, emptyMap().toReadableMap(), endTimestamp, mockPromise ) // Then - assertThat(id).isEqualTo(fakeSpanId) + assertThat(id).isEqualTo(fakeSpanId.toString()) verify(mockSpan).context() verify(mockSpan).finish(endTimestamp.toLong() * 1000) fakeContext.forEach { @@ -298,14 +302,14 @@ internal class DdTraceTest { fakeTimestamp, mockPromise ) - val id = lastResolvedValue + val id = lastResolvedValue.toString() fakeGlobalState.forEach { (k, v) -> GlobalState.addAttribute(k, v) } - testedTrace.finishSpan(id as String, fakeContext.toReadableMap(), endTimestamp, mockPromise) + testedTrace.finishSpan(id, fakeContext.toReadableMap(), endTimestamp, mockPromise) // Then - assertThat(id).isEqualTo(fakeSpanId) + assertThat(id).isEqualTo(fakeSpanId.toString()) verify(mockSpan).context() verify(mockSpan).finish(endTimestamp.toLong() * 1000) expectedAttributes.forEach { diff --git a/packages/core/android/src/test/kotlin/com/datadog/tools/unit/MockRumMonitor.kt b/packages/core/android/src/test/kotlin/com/datadog/tools/unit/MockRumMonitor.kt index a2e79d630..7c5585edd 100644 --- a/packages/core/android/src/test/kotlin/com/datadog/tools/unit/MockRumMonitor.kt +++ b/packages/core/android/src/test/kotlin/com/datadog/tools/unit/MockRumMonitor.kt @@ -29,6 +29,8 @@ class MockRumMonitor : RumMonitor { override fun addAttribute(key: String, value: Any?) {} + override fun addViewAttributes(attributes: Map) {} + override fun addError( message: String, source: RumErrorSource, @@ -61,6 +63,7 @@ class MockRumMonitor : RumMonitor { override fun getCurrentSessionId(callback: (String?) -> Unit) {} override fun removeAttribute(key: String) {} + override fun removeViewAttributes(attributes: Collection) {} override fun startAction( type: RumActionType, @@ -75,13 +78,6 @@ class MockRumMonitor : RumMonitor { attributes: Map ) {} - override fun startResource( - key: String, - method: String, - url: String, - attributes: Map - ) {} - override fun startView( key: Any, name: String, From 90c79addf2e087e2c0509a38745f3086a0d88dbf Mon Sep 17 00:00:00 2001 From: Sergio Barrio Date: Thu, 18 Sep 2025 10:50:27 +0200 Subject: [PATCH 037/410] Fix iOS tests --- packages/core/ios/Tests/DdSdkTests.swift | 2 +- packages/core/ios/Tests/RUMMocks.swift | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/core/ios/Tests/DdSdkTests.swift b/packages/core/ios/Tests/DdSdkTests.swift index 6be5e0994..555ce4549 100644 --- a/packages/core/ios/Tests/DdSdkTests.swift +++ b/packages/core/ios/Tests/DdSdkTests.swift @@ -738,7 +738,7 @@ class DdSdkTests: XCTestCase { XCTAssertEqual(actualFirstPartyHosts, expectedFirstPartyHosts) XCTAssertEqual(actualTracingSamplingRate, 66) - XCTAssertEqual(actualTraceContextInjection, .all) + XCTAssertEqual(actualTraceContextInjection, .sampled) } func testBuildTelemetrySampleRate() { diff --git a/packages/core/ios/Tests/RUMMocks.swift b/packages/core/ios/Tests/RUMMocks.swift index 6a6fd94e7..01d0f9d0d 100644 --- a/packages/core/ios/Tests/RUMMocks.swift +++ b/packages/core/ios/Tests/RUMMocks.swift @@ -213,14 +213,14 @@ extension RUMActionID: RandomMockable { } } -extension RUMDevice.RUMDeviceType: RandomMockable { - static func mockRandom() -> RUMDevice.RUMDeviceType { +extension Device.DeviceType: RandomMockable { + static func mockRandom() -> Device.DeviceType { return [.mobile, .desktop, .tablet, .tv, .gamingConsole, .bot, .other].randomElement()! } } -extension RUMDevice: RandomMockable { - static func mockRandom() -> RUMDevice { +extension Device: RandomMockable { + static func mockRandom() -> Device { return .init( architecture: .mockRandom(), brand: .mockRandom(), @@ -231,8 +231,8 @@ extension RUMDevice: RandomMockable { } } -extension RUMOperatingSystem: RandomMockable { - static func mockRandom() -> RUMOperatingSystem { +extension OperatingSystem: RandomMockable { + static func mockRandom() -> OperatingSystem { return .init( build: .mockRandom(length: 5), name: .mockRandom(length: 5), From 5c8f15e6b34bb3271addc7a777e9c64fe2834d81 Mon Sep 17 00:00:00 2001 From: Sergio Barrio Date: Fri, 19 Sep 2025 11:45:00 +0200 Subject: [PATCH 038/410] Bump Native SDKs to 3.1.0 --- benchmarks/android/app/build.gradle | 2 +- benchmarks/ios/Podfile.lock | 70 +++++++-------- example-new-architecture/ios/Podfile.lock | 66 +++++++------- example/ios/Podfile.lock | 88 +++++++++---------- packages/core/DatadogSDKReactNative.podspec | 12 +-- packages/core/android/build.gradle | 10 +-- .../com/datadog/tools/unit/MockRumMonitor.kt | 23 +++++ packages/core/ios/Tests/DdLogsTests.swift | 2 + ...DatadogSDKReactNativeSessionReplay.podspec | 2 +- .../android/build.gradle | 4 +- .../DatadogSDKReactNativeWebView.podspec | 4 +- .../react-native-webview/android/build.gradle | 2 +- 12 files changed, 155 insertions(+), 130 deletions(-) diff --git a/benchmarks/android/app/build.gradle b/benchmarks/android/app/build.gradle index 7dc4b3707..04c240bd0 100644 --- a/benchmarks/android/app/build.gradle +++ b/benchmarks/android/app/build.gradle @@ -129,5 +129,5 @@ dependencies { // Benchmark tools from dd-sdk-android are used for vitals recording // Remember to bump thid alongside the main dd-sdk-android dependencies - implementation("com.datadoghq:dd-sdk-android-benchmark-internal:3.0.0") + implementation("com.datadoghq:dd-sdk-android-benchmark-internal:3.1.0") } diff --git a/benchmarks/ios/Podfile.lock b/benchmarks/ios/Podfile.lock index c04c9d92d..45c15e56f 100644 --- a/benchmarks/ios/Podfile.lock +++ b/benchmarks/ios/Podfile.lock @@ -1,22 +1,22 @@ PODS: - boost (1.84.0) - - DatadogCore (3.0.0): - - DatadogInternal (= 3.0.0) - - DatadogCrashReporting (3.0.0): - - DatadogInternal (= 3.0.0) + - DatadogCore (3.1.0): + - DatadogInternal (= 3.1.0) + - DatadogCrashReporting (3.1.0): + - DatadogInternal (= 3.1.0) - PLCrashReporter (~> 1.12.0) - - DatadogInternal (3.0.0) - - DatadogLogs (3.0.0): - - DatadogInternal (= 3.0.0) - - DatadogRUM (3.0.0): - - DatadogInternal (= 3.0.0) + - DatadogInternal (3.1.0) + - DatadogLogs (3.1.0): + - DatadogInternal (= 3.1.0) + - DatadogRUM (3.1.0): + - DatadogInternal (= 3.1.0) - DatadogSDKReactNative (2.12.1): - - DatadogCore (= 3.0.0) - - DatadogCrashReporting (= 3.0.0) - - DatadogLogs (= 3.0.0) - - DatadogRUM (= 3.0.0) - - DatadogTrace (= 3.0.0) - - DatadogWebViewTracking (= 3.0.0) + - DatadogCore (= 3.1.0) + - DatadogCrashReporting (= 3.1.0) + - DatadogLogs (= 3.1.0) + - DatadogRUM (= 3.1.0) + - DatadogTrace (= 3.1.0) + - DatadogWebViewTracking (= 3.1.0) - DoubleConversion - glog - hermes-engine @@ -39,7 +39,7 @@ PODS: - Yoga - DatadogSDKReactNativeSessionReplay (2.13.0): - DatadogSDKReactNative - - DatadogSessionReplay (= 3.0.0) + - DatadogSessionReplay (= 3.1.0) - DoubleConversion - glog - hermes-engine @@ -61,9 +61,9 @@ PODS: - ReactCommon/turbomodule/core - Yoga - DatadogSDKReactNativeWebView (2.12.1): - - DatadogInternal (= 3.0.0) + - DatadogInternal (= 3.1.0) - DatadogSDKReactNative - - DatadogWebViewTracking (= 3.0.0) + - DatadogWebViewTracking (= 3.1.0) - DoubleConversion - glog - hermes-engine @@ -84,13 +84,13 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - DatadogSessionReplay (3.0.0): - - DatadogInternal (= 3.0.0) - - DatadogTrace (3.0.0): - - DatadogInternal (= 3.0.0) + - DatadogSessionReplay (3.1.0): + - DatadogInternal (= 3.1.0) + - DatadogTrace (3.1.0): + - DatadogInternal (= 3.1.0) - OpenTelemetrySwiftApi (= 1.13.1) - - DatadogWebViewTracking (3.0.0): - - DatadogInternal (= 3.0.0) + - DatadogWebViewTracking (3.1.0): + - DatadogInternal (= 3.1.0) - DoubleConversion (1.1.6) - fast_float (6.1.4) - FBLazyVector (0.78.2) @@ -2070,17 +2070,17 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: boost: 7e761d76ca2ce687f7cc98e698152abd03a18f90 - DatadogCore: 0c39ba4ef3dee02071f235f2fb56af7e29f4ceb0 - DatadogCrashReporting: 604e65e524cb2fc22cda39fd9c9ea5fd3bddf24e - DatadogInternal: 442673a7fb5329299b489b91ed705aa2085380f7 - DatadogLogs: 0146a0e3140ba62fd42729593c0910d692aea5fd - DatadogRUM: f97fbd0290ecec5bc952fb03a6a1ae068649243f - DatadogSDKReactNative: 241bf982c16ceff03d94a58e6d005e55c47b99c6 - DatadogSDKReactNativeSessionReplay: bba36092686e3183e97c1a0c7f4ca8142582ae28 - DatadogSDKReactNativeWebView: 6da060df20e235abac533e582d9fc2b3a5070840 - DatadogSessionReplay: 0357b911c6ab42c77c93b29761ecc20d9282e971 - DatadogTrace: d714e7c456e612b7f171483d08086a59aa1c4213 - DatadogWebViewTracking: fb9591c09fca82b143f3843a7164a7e782175c53 + DatadogCore: d2f51c7fb4308cf3c25e55e2e7242e5d558ee71d + DatadogCrashReporting: f636f1d1c534572c0b0abcdc59df244c884d825d + DatadogInternal: 7837b2ce3d525d429682532eeda697b181299fdc + DatadogLogs: 250894b5a99da5b924a019049c0d0326823cdbd6 + DatadogRUM: 0d2a60e1abb8aacfb8827ef84f6d5deb4d5026c8 + DatadogSDKReactNative: 8e0f39de38621d4d7ed961a74d8a216fd3a38321 + DatadogSDKReactNativeSessionReplay: f9288c8e981dcc65d1f727b01421ee9a7601e75f + DatadogSDKReactNativeWebView: 993527f6c5d38e0fcc4804a6a60c334dd199dc5b + DatadogSessionReplay: 6bc71888e2b41dd0de3325f06f0c0b3cee0e6df4 + DatadogTrace: f59e933074cd285ad7e9f5af991f8fe04b095991 + DatadogWebViewTracking: 9bc92b4147aeed47eb1911451f651094aa6dd6c1 DoubleConversion: cb417026b2400c8f53ae97020b2be961b59470cb fast_float: 06eeec4fe712a76acc9376682e4808b05ce978b6 FBLazyVector: e32d34492c519a2194ec9d7f5e7a79d11b73f91c diff --git a/example-new-architecture/ios/Podfile.lock b/example-new-architecture/ios/Podfile.lock index 281513566..c0cd90bf6 100644 --- a/example-new-architecture/ios/Podfile.lock +++ b/example-new-architecture/ios/Podfile.lock @@ -1,22 +1,22 @@ PODS: - boost (1.84.0) - - DatadogCore (3.0.0): - - DatadogInternal (= 3.0.0) - - DatadogCrashReporting (3.0.0): - - DatadogInternal (= 3.0.0) + - DatadogCore (3.1.0): + - DatadogInternal (= 3.1.0) + - DatadogCrashReporting (3.1.0): + - DatadogInternal (= 3.1.0) - PLCrashReporter (~> 1.12.0) - - DatadogInternal (3.0.0) - - DatadogLogs (3.0.0): - - DatadogInternal (= 3.0.0) - - DatadogRUM (3.0.0): - - DatadogInternal (= 3.0.0) + - DatadogInternal (3.1.0) + - DatadogLogs (3.1.0): + - DatadogInternal (= 3.1.0) + - DatadogRUM (3.1.0): + - DatadogInternal (= 3.1.0) - DatadogSDKReactNative (2.12.1): - - DatadogCore (= 3.0.0) - - DatadogCrashReporting (= 3.0.0) - - DatadogLogs (= 3.0.0) - - DatadogRUM (= 3.0.0) - - DatadogTrace (= 3.0.0) - - DatadogWebViewTracking (= 3.0.0) + - DatadogCore (= 3.1.0) + - DatadogCrashReporting (= 3.1.0) + - DatadogLogs (= 3.1.0) + - DatadogRUM (= 3.1.0) + - DatadogTrace (= 3.1.0) + - DatadogWebViewTracking (= 3.1.0) - DoubleConversion - glog - hermes-engine @@ -38,12 +38,12 @@ PODS: - ReactCommon/turbomodule/core - Yoga - DatadogSDKReactNative/Tests (2.12.1): - - DatadogCore (= 3.0.0) - - DatadogCrashReporting (= 3.0.0) - - DatadogLogs (= 3.0.0) - - DatadogRUM (= 3.0.0) - - DatadogTrace (= 3.0.0) - - DatadogWebViewTracking (= 3.0.0) + - DatadogCore (= 3.1.0) + - DatadogCrashReporting (= 3.1.0) + - DatadogLogs (= 3.1.0) + - DatadogRUM (= 3.1.0) + - DatadogTrace (= 3.1.0) + - DatadogWebViewTracking (= 3.1.0) - DoubleConversion - glog - hermes-engine @@ -64,11 +64,11 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - DatadogTrace (3.0.0): - - DatadogInternal (= 3.0.0) + - DatadogTrace (3.1.0): + - DatadogInternal (= 3.1.0) - OpenTelemetrySwiftApi (= 1.13.1) - - DatadogWebViewTracking (3.0.0): - - DatadogInternal (= 3.0.0) + - DatadogWebViewTracking (3.1.0): + - DatadogInternal (= 3.1.0) - DoubleConversion (1.1.6) - fast_float (6.1.4) - FBLazyVector (0.76.9) @@ -1850,14 +1850,14 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: boost: 1dca942403ed9342f98334bf4c3621f011aa7946 - DatadogCore: 0c39ba4ef3dee02071f235f2fb56af7e29f4ceb0 - DatadogCrashReporting: 604e65e524cb2fc22cda39fd9c9ea5fd3bddf24e - DatadogInternal: 442673a7fb5329299b489b91ed705aa2085380f7 - DatadogLogs: 0146a0e3140ba62fd42729593c0910d692aea5fd - DatadogRUM: f97fbd0290ecec5bc952fb03a6a1ae068649243f - DatadogSDKReactNative: 0a80aa75958d595a99be54d2838db53eda7a08af - DatadogTrace: d714e7c456e612b7f171483d08086a59aa1c4213 - DatadogWebViewTracking: fb9591c09fca82b143f3843a7164a7e782175c53 + DatadogCore: d2f51c7fb4308cf3c25e55e2e7242e5d558ee71d + DatadogCrashReporting: f636f1d1c534572c0b0abcdc59df244c884d825d + DatadogInternal: 7837b2ce3d525d429682532eeda697b181299fdc + DatadogLogs: 250894b5a99da5b924a019049c0d0326823cdbd6 + DatadogRUM: 0d2a60e1abb8aacfb8827ef84f6d5deb4d5026c8 + DatadogSDKReactNative: 069ea9876220b2d09b0f4b180ce571b1b6ecbb35 + DatadogTrace: f59e933074cd285ad7e9f5af991f8fe04b095991 + DatadogWebViewTracking: 9bc92b4147aeed47eb1911451f651094aa6dd6c1 DoubleConversion: f16ae600a246532c4020132d54af21d0ddb2a385 fast_float: 06eeec4fe712a76acc9376682e4808b05ce978b6 FBLazyVector: 7605ea4810e0e10ae4815292433c09bf4324ba45 diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 91eb63ae0..feb34e2a3 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -1,34 +1,34 @@ PODS: - boost (1.84.0) - - DatadogCore (3.0.0): - - DatadogInternal (= 3.0.0) - - DatadogCrashReporting (3.0.0): - - DatadogInternal (= 3.0.0) + - DatadogCore (3.1.0): + - DatadogInternal (= 3.1.0) + - DatadogCrashReporting (3.1.0): + - DatadogInternal (= 3.1.0) - PLCrashReporter (~> 1.12.0) - - DatadogInternal (3.0.0) - - DatadogLogs (3.0.0): - - DatadogInternal (= 3.0.0) - - DatadogRUM (3.0.0): - - DatadogInternal (= 3.0.0) + - DatadogInternal (3.1.0) + - DatadogLogs (3.1.0): + - DatadogInternal (= 3.1.0) + - DatadogRUM (3.1.0): + - DatadogInternal (= 3.1.0) - DatadogSDKReactNative (2.12.1): - - DatadogCore (= 3.0.0) - - DatadogCrashReporting (= 3.0.0) - - DatadogLogs (= 3.0.0) - - DatadogRUM (= 3.0.0) - - DatadogTrace (= 3.0.0) - - DatadogWebViewTracking (= 3.0.0) + - DatadogCore (= 3.1.0) + - DatadogCrashReporting (= 3.1.0) + - DatadogLogs (= 3.1.0) + - DatadogRUM (= 3.1.0) + - DatadogTrace (= 3.1.0) + - DatadogWebViewTracking (= 3.1.0) - React-Core - DatadogSDKReactNative/Tests (2.12.1): - - DatadogCore (= 3.0.0) - - DatadogCrashReporting (= 3.0.0) - - DatadogLogs (= 3.0.0) - - DatadogRUM (= 3.0.0) - - DatadogTrace (= 3.0.0) - - DatadogWebViewTracking (= 3.0.0) + - DatadogCore (= 3.1.0) + - DatadogCrashReporting (= 3.1.0) + - DatadogLogs (= 3.1.0) + - DatadogRUM (= 3.1.0) + - DatadogTrace (= 3.1.0) + - DatadogWebViewTracking (= 3.1.0) - React-Core - DatadogSDKReactNativeSessionReplay (2.13.0): - DatadogSDKReactNative - - DatadogSessionReplay (= 3.0.0) + - DatadogSessionReplay (= 3.1.0) - DoubleConversion - glog - hermes-engine @@ -51,7 +51,7 @@ PODS: - Yoga - DatadogSDKReactNativeSessionReplay/Tests (2.13.0): - DatadogSDKReactNative - - DatadogSessionReplay (= 3.0.0) + - DatadogSessionReplay (= 3.1.0) - DoubleConversion - glog - hermes-engine @@ -74,24 +74,24 @@ PODS: - ReactCommon/turbomodule/core - Yoga - DatadogSDKReactNativeWebView (2.12.1): - - DatadogInternal (= 3.0.0) + - DatadogInternal (= 3.1.0) - DatadogSDKReactNative - - DatadogWebViewTracking (= 3.0.0) + - DatadogWebViewTracking (= 3.1.0) - React-Core - DatadogSDKReactNativeWebView/Tests (2.12.1): - - DatadogInternal (= 3.0.0) + - DatadogInternal (= 3.1.0) - DatadogSDKReactNative - - DatadogWebViewTracking (= 3.0.0) + - DatadogWebViewTracking (= 3.1.0) - React-Core - react-native-webview - React-RCTText - - DatadogSessionReplay (3.0.0): - - DatadogInternal (= 3.0.0) - - DatadogTrace (3.0.0): - - DatadogInternal (= 3.0.0) + - DatadogSessionReplay (3.1.0): + - DatadogInternal (= 3.1.0) + - DatadogTrace (3.1.0): + - DatadogInternal (= 3.1.0) - OpenTelemetrySwiftApi (= 1.13.1) - - DatadogWebViewTracking (3.0.0): - - DatadogInternal (= 3.0.0) + - DatadogWebViewTracking (3.1.0): + - DatadogInternal (= 3.1.0) - DoubleConversion (1.1.6) - fast_float (6.1.4) - FBLazyVector (0.76.9) @@ -1988,17 +1988,17 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: boost: 1dca942403ed9342f98334bf4c3621f011aa7946 - DatadogCore: 0c39ba4ef3dee02071f235f2fb56af7e29f4ceb0 - DatadogCrashReporting: 604e65e524cb2fc22cda39fd9c9ea5fd3bddf24e - DatadogInternal: 442673a7fb5329299b489b91ed705aa2085380f7 - DatadogLogs: 0146a0e3140ba62fd42729593c0910d692aea5fd - DatadogRUM: f97fbd0290ecec5bc952fb03a6a1ae068649243f - DatadogSDKReactNative: e430b3f4d7adb0fac07b61e2fb65ceacdaf82b3e - DatadogSDKReactNativeSessionReplay: 4b2a3d166a79581f18522795b40141c34cf3685d - DatadogSDKReactNativeWebView: 35dc2b9736e1aaa82b366bf6b8a8a959a9b088c5 - DatadogSessionReplay: 0357b911c6ab42c77c93b29761ecc20d9282e971 - DatadogTrace: d714e7c456e612b7f171483d08086a59aa1c4213 - DatadogWebViewTracking: fb9591c09fca82b143f3843a7164a7e782175c53 + DatadogCore: d2f51c7fb4308cf3c25e55e2e7242e5d558ee71d + DatadogCrashReporting: f636f1d1c534572c0b0abcdc59df244c884d825d + DatadogInternal: 7837b2ce3d525d429682532eeda697b181299fdc + DatadogLogs: 250894b5a99da5b924a019049c0d0326823cdbd6 + DatadogRUM: 0d2a60e1abb8aacfb8827ef84f6d5deb4d5026c8 + DatadogSDKReactNative: af351a4e1ce08124c290c52de94b0062a166cc67 + DatadogSDKReactNativeSessionReplay: dcbd55d9d0f2b86026996a8b7ec9654922d5dfe1 + DatadogSDKReactNativeWebView: 096ac87eb753b6a217b93441983264b9837c3b7e + DatadogSessionReplay: 6bc71888e2b41dd0de3325f06f0c0b3cee0e6df4 + DatadogTrace: f59e933074cd285ad7e9f5af991f8fe04b095991 + DatadogWebViewTracking: 9bc92b4147aeed47eb1911451f651094aa6dd6c1 DoubleConversion: f16ae600a246532c4020132d54af21d0ddb2a385 fast_float: 06eeec4fe712a76acc9376682e4808b05ce978b6 FBLazyVector: 7605ea4810e0e10ae4815292433c09bf4324ba45 diff --git a/packages/core/DatadogSDKReactNative.podspec b/packages/core/DatadogSDKReactNative.podspec index a2f41ddd1..c0d235304 100644 --- a/packages/core/DatadogSDKReactNative.podspec +++ b/packages/core/DatadogSDKReactNative.podspec @@ -19,14 +19,14 @@ Pod::Spec.new do |s| s.dependency "React-Core" # /!\ Remember to keep the versions in sync with DatadogSDKReactNativeSessionReplay.podspec - s.dependency 'DatadogCore', '3.0.0' - s.dependency 'DatadogLogs', '3.0.0' - s.dependency 'DatadogTrace', '3.0.0' - s.dependency 'DatadogRUM', '3.0.0' - s.dependency 'DatadogCrashReporting', '3.0.0' + s.dependency 'DatadogCore', '3.1.0' + s.dependency 'DatadogLogs', '3.1.0' + s.dependency 'DatadogTrace', '3.1.0' + s.dependency 'DatadogRUM', '3.1.0' + s.dependency 'DatadogCrashReporting', '3.1.0' # DatadogWebViewTracking is not available for tvOS - s.ios.dependency 'DatadogWebViewTracking', '3.0.0' + s.ios.dependency 'DatadogWebViewTracking', '3.1.0' s.test_spec 'Tests' do |test_spec| test_spec.source_files = 'ios/Tests/**/*.{swift,json}' diff --git a/packages/core/android/build.gradle b/packages/core/android/build.gradle index a8d09d4d1..59bb75cc3 100644 --- a/packages/core/android/build.gradle +++ b/packages/core/android/build.gradle @@ -201,16 +201,16 @@ dependencies { // This breaks builds if the React Native target is below 0.76.0. as it relies on Gradle 8.5.0. // To avoid this, we enforce 1.0.0-beta01 on RN < 0.76.0 if (reactNativeMinorVersion < 76) { - implementation("com.datadoghq:dd-sdk-android-rum:3.0.0") { + implementation("com.datadoghq:dd-sdk-android-rum:3.1.0") { exclude group: "androidx.metrics", module: "metrics-performance" } implementation "androidx.metrics:metrics-performance:1.0.0-beta01" } else { - implementation "com.datadoghq:dd-sdk-android-rum:3.0.0" + implementation "com.datadoghq:dd-sdk-android-rum:3.1.0" } - implementation "com.datadoghq:dd-sdk-android-logs:3.0.0" - implementation "com.datadoghq:dd-sdk-android-trace:3.0.0" - implementation "com.datadoghq:dd-sdk-android-webview:3.0.0" + implementation "com.datadoghq:dd-sdk-android-logs:3.1.0" + implementation "com.datadoghq:dd-sdk-android-trace:3.1.0" + implementation "com.datadoghq:dd-sdk-android-webview:3.1.0" implementation "com.google.code.gson:gson:2.10.0" testImplementation "org.junit.platform:junit-platform-launcher:1.6.2" testImplementation "org.junit.jupiter:junit-jupiter-api:5.6.2" diff --git a/packages/core/android/src/test/kotlin/com/datadog/tools/unit/MockRumMonitor.kt b/packages/core/android/src/test/kotlin/com/datadog/tools/unit/MockRumMonitor.kt index 7c5585edd..702cc2533 100644 --- a/packages/core/android/src/test/kotlin/com/datadog/tools/unit/MockRumMonitor.kt +++ b/packages/core/android/src/test/kotlin/com/datadog/tools/unit/MockRumMonitor.kt @@ -13,6 +13,7 @@ import com.datadog.android.rum.RumMonitor import com.datadog.android.rum.RumResourceKind import com.datadog.android.rum.RumResourceMethod import com.datadog.android.rum._RumInternalProxy +import com.datadog.android.rum.featureoperations.FailureReason class MockRumMonitor : RumMonitor { override var debug = false @@ -123,4 +124,26 @@ class MockRumMonitor : RumMonitor { key: Any, attributes: Map ) {} + + @ExperimentalRumApi + override fun startFeatureOperation( + name: String, + operationKey: String?, + attributes: Map + ) {} + + @ExperimentalRumApi + override fun succeedFeatureOperation( + name: String, + operationKey: String?, + attributes: Map + ) {} + + @ExperimentalRumApi + override fun failFeatureOperation( + name: String, + operationKey: String?, + failureReason: FailureReason, + attributes: Map + ) {} } diff --git a/packages/core/ios/Tests/DdLogsTests.swift b/packages/core/ios/Tests/DdLogsTests.swift index 2d9fdebac..60640e807 100644 --- a/packages/core/ios/Tests/DdLogsTests.swift +++ b/packages/core/ios/Tests/DdLogsTests.swift @@ -463,6 +463,8 @@ private class MockNativeLogger: LoggerProtocol { } extension MockNativeLogger: InternalLoggerProtocol { + func critical(message: String, error: (any Error)?, attributes: [String : any Encodable]?, completionHandler: @escaping DatadogInternal.CompletionHandler) {} + func log(level: DatadogLogs.LogLevel, message: String, errorKind: String?, errorMessage: String?, stackTrace: String?, attributes: [String : Encodable]?) { receivedMethodCalls.append(MethodCall( kind: MockNativeLogger.MethodCall.Kind(from: level), diff --git a/packages/react-native-session-replay/DatadogSDKReactNativeSessionReplay.podspec b/packages/react-native-session-replay/DatadogSDKReactNativeSessionReplay.podspec index 3a2d5ff48..6a5d0b78f 100644 --- a/packages/react-native-session-replay/DatadogSDKReactNativeSessionReplay.podspec +++ b/packages/react-native-session-replay/DatadogSDKReactNativeSessionReplay.podspec @@ -23,7 +23,7 @@ Pod::Spec.new do |s| s.dependency "React-Core" # /!\ Remember to keep the version in sync with DatadogSDKReactNative.podspec - s.dependency 'DatadogSessionReplay', '3.0.0' + s.dependency 'DatadogSessionReplay', '3.1.0' s.dependency 'DatadogSDKReactNative' s.test_spec 'Tests' do |test_spec| diff --git a/packages/react-native-session-replay/android/build.gradle b/packages/react-native-session-replay/android/build.gradle index 6f8f35e52..f21f71cbb 100644 --- a/packages/react-native-session-replay/android/build.gradle +++ b/packages/react-native-session-replay/android/build.gradle @@ -214,8 +214,8 @@ dependencies { api "com.facebook.react:react-android:$reactNativeVersion" } implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" - implementation "com.datadoghq:dd-sdk-android-session-replay:3.0.0" - implementation "com.datadoghq:dd-sdk-android-internal:3.0.0" + implementation "com.datadoghq:dd-sdk-android-session-replay:3.1.0" + implementation "com.datadoghq:dd-sdk-android-internal:3.1.0" implementation project(path: ':datadog_mobile-react-native') testImplementation "org.junit.platform:junit-platform-launcher:1.6.2" diff --git a/packages/react-native-webview/DatadogSDKReactNativeWebView.podspec b/packages/react-native-webview/DatadogSDKReactNativeWebView.podspec index 26e160bbc..080a853d8 100644 --- a/packages/react-native-webview/DatadogSDKReactNativeWebView.podspec +++ b/packages/react-native-webview/DatadogSDKReactNativeWebView.podspec @@ -23,8 +23,8 @@ Pod::Spec.new do |s| end # /!\ Remember to keep the version in sync with DatadogSDKReactNative.podspec - s.dependency 'DatadogWebViewTracking', '3.0.0' - s.dependency 'DatadogInternal', '3.0.0' + s.dependency 'DatadogWebViewTracking', '3.1.0' + s.dependency 'DatadogInternal', '3.1.0' s.dependency 'DatadogSDKReactNative' s.test_spec 'Tests' do |test_spec| diff --git a/packages/react-native-webview/android/build.gradle b/packages/react-native-webview/android/build.gradle index 098eae019..87ca1b7e7 100644 --- a/packages/react-native-webview/android/build.gradle +++ b/packages/react-native-webview/android/build.gradle @@ -190,7 +190,7 @@ dependencies { implementation "com.facebook.react:react-android:$reactNativeVersion" } - implementation "com.datadoghq:dd-sdk-android-webview:3.0.0" + implementation "com.datadoghq:dd-sdk-android-webview:3.1.0" implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation project(path: ':datadog_mobile-react-native') From 2f6002a4c1830610ecaee32463a2b439dd269a04 Mon Sep 17 00:00:00 2001 From: Sergio Barrio Date: Fri, 19 Sep 2025 14:33:51 +0200 Subject: [PATCH 039/410] Fix internaltTestingTools tests --- .../DdInternalTestingImplementation.kt | 45 ++++++++------- .../DdInternalTestingImplementationTest.kt | 55 ++++++++++++------- 2 files changed, 60 insertions(+), 40 deletions(-) diff --git a/packages/internal-testing-tools/android/src/main/kotlin/com/datadog/reactnative/internaltesting/DdInternalTestingImplementation.kt b/packages/internal-testing-tools/android/src/main/kotlin/com/datadog/reactnative/internaltesting/DdInternalTestingImplementation.kt index b33bef6de..0d548aa72 100644 --- a/packages/internal-testing-tools/android/src/main/kotlin/com/datadog/reactnative/internaltesting/DdInternalTestingImplementation.kt +++ b/packages/internal-testing-tools/android/src/main/kotlin/com/datadog/reactnative/internaltesting/DdInternalTestingImplementation.kt @@ -6,11 +6,13 @@ package com.datadog.reactnative.internaltesting +import androidx.annotation.WorkerThread import com.datadog.android.api.InternalLogger import com.datadog.android.Datadog import com.datadog.android.api.context.DatadogContext import com.datadog.android.api.context.NetworkInfo import com.datadog.android.api.context.TimeInfo +import com.datadog.android.api.feature.EventWriteScope import com.datadog.android.api.feature.Feature import com.datadog.android.api.feature.FeatureScope import com.datadog.android.api.storage.EventBatchWriter @@ -112,53 +114,54 @@ internal class FeatureScopeInterceptor( private val featureScope: FeatureScope, private val core: InternalSdkCore, ) : FeatureScope by featureScope { - private val eventsBatchInterceptor = EventBatchInterceptor() + private val eventWriteScopeInterceptor = EventWriteScopeInterceptor() fun eventsWritten(): List { - return eventsBatchInterceptor.events + return eventWriteScopeInterceptor.events } fun clearData() { - eventsBatchInterceptor.clearData() + eventWriteScopeInterceptor.clearData() } // region FeatureScope override fun withWriteContext( - forceNewBatch: Boolean, - callback: (DatadogContext, EventBatchWriter) -> Unit + withFeatureContexts: Set, + callback: (datadogContext: DatadogContext, write: EventWriteScope) -> Unit ) { - featureScope.withWriteContext(forceNewBatch, callback) + featureScope.withWriteContext(withFeatureContexts, callback) core.getDatadogContext()?.let { - callback(it, eventsBatchInterceptor) + callback(it, eventWriteScopeInterceptor) } } // endregion } - -internal class EventBatchInterceptor: EventBatchWriter { +internal class EventWriteScopeInterceptor : EventWriteScope { internal val events = mutableListOf() - override fun currentMetadata(): ByteArray? { - return null - } - fun clearData() { events.clear() } - override fun write( - event: RawBatchEvent, - batchMetadata: ByteArray?, - eventType: EventType - ): Boolean { - val eventContent = String(event.data) + private val writer = object : EventBatchWriter { + override fun currentMetadata(): ByteArray? = null - events += eventContent + override fun write( + event: RawBatchEvent, + batchMetadata: ByteArray?, + eventType: EventType + ): Boolean { + events += String(event.data) + return true + } + } - return true + override fun invoke(p1: (EventBatchWriter) -> Unit) { + p1(writer) } } + diff --git a/packages/internal-testing-tools/android/src/test/kotlin/com/datadog/reactnative/internaltesting/DdInternalTestingImplementationTest.kt b/packages/internal-testing-tools/android/src/test/kotlin/com/datadog/reactnative/internaltesting/DdInternalTestingImplementationTest.kt index 4a6938f9b..d25db9274 100644 --- a/packages/internal-testing-tools/android/src/test/kotlin/com/datadog/reactnative/internaltesting/DdInternalTestingImplementationTest.kt +++ b/packages/internal-testing-tools/android/src/test/kotlin/com/datadog/reactnative/internaltesting/DdInternalTestingImplementationTest.kt @@ -10,9 +10,9 @@ import android.content.Context import com.datadog.android.Datadog import com.datadog.android.api.SdkCore import com.datadog.android.api.context.DatadogContext +import com.datadog.android.api.feature.EventWriteScope import com.datadog.android.api.feature.Feature import com.datadog.android.api.feature.FeatureScope -import com.datadog.android.api.storage.EventBatchWriter import com.datadog.android.api.storage.EventType import com.datadog.android.api.storage.RawBatchEvent import com.datadog.android.api.storage.datastore.DataStoreHandler @@ -85,23 +85,27 @@ internal class DdInternalTestingImplementationTest { wrappedCore.registerFeature(mockFeature) requireNotNull(wrappedCore.getFeature(mockFeature.name)) - .withWriteContext { _, eventBatchWriter -> - eventBatchWriter.write( - RawBatchEvent(data = "mock event for test".toByteArray()), - batchMetadata = null, - eventType = EventType.DEFAULT + .withWriteContext { _, writeScope -> + writeScope { + val rawBatchEvent = + RawBatchEvent(data = "mock event for test".toByteArray()) + it.write( + rawBatchEvent, + batchMetadata = null, + eventType = EventType.DEFAULT + ) + } + + // Then + assertThat( + wrappedCore.featureScopes[mockFeature.name] + ?.eventsWritten() + ?.first() ) + .isEqualTo( + "mock event for test" + ) } - - // Then - assertThat( - wrappedCore.featureScopes[mockFeature.name] - ?.eventsWritten() - ?.first() - ) - .isEqualTo( - "mock event for test" - ) } } } @@ -116,10 +120,23 @@ internal class MockFeatureScope(private val feature: Feature) : FeatureScope { return feature as T } + override fun withContext( + withFeatureContexts: Set, + callback: (datadogContext: DatadogContext) -> Unit + ) { + } + override fun withWriteContext( - forceNewBatch: Boolean, - callback: (DatadogContext, EventBatchWriter) -> Unit - ) {} + withFeatureContexts: Set, + callback: (datadogContext: DatadogContext, write: EventWriteScope) -> Unit + ) { + } + + override fun getWriteContextSync( + withFeatureContexts: Set + ): Pair? { + return TODO("Provide the return value") + } } internal class MockFeature(override val name: String) : Feature { From a9bcdba87d45f06f7bcb15dbae17a8eb6831182f Mon Sep 17 00:00:00 2001 From: Sergio Barrio Date: Fri, 5 Sep 2025 17:47:29 +0200 Subject: [PATCH 040/410] Use native sdk's core instance instead of the one inside RN SDK wrapper --- .../com/datadog/reactnative/DdSdkNativeInitialization.kt | 2 ++ .../src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt | 6 +----- .../src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt | 7 +------ 3 files changed, 4 insertions(+), 11 deletions(-) diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkNativeInitialization.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkNativeInitialization.kt index 4388ad5f6..ee55d08fe 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkNativeInitialization.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkNativeInitialization.kt @@ -65,7 +65,9 @@ class DdSdkNativeInitialization internal constructor( datadog.initialize(appContext, sdkConfiguration, trackingConsent) Rum.enable(rumConfiguration, Datadog.getInstance()) + Logs.enable(logsConfiguration, Datadog.getInstance()) + Trace.enable(traceConfiguration, Datadog.getInstance()) } diff --git a/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt b/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt index 97acb2ebf..3c86e28b4 100644 --- a/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt +++ b/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt @@ -21,11 +21,7 @@ class DdSdk( ddTelemetry: DdTelemetry = DdTelemetry() ) : ReactContextBaseJavaModule(reactContext) { - private val implementation = DdSdkImplementation( - reactContext, - datadog = datadogWrapper, - ddTelemetry - ) + private val implementation = DdSdkImplementation(reactContext, datadog = datadogWrapper, ddTelemetry) private var lifecycleEventListener: LifecycleEventListener? = null override fun getName(): String = DdSdkImplementation.NAME diff --git a/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt b/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt index 8797bcc64..c9f91738f 100644 --- a/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt +++ b/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt @@ -163,12 +163,7 @@ internal class DdSdkTest { answer.getArgument(0).run() true } - testedBridgeSdk = DdSdkImplementation( - mockReactContext, - mockDatadog, - mockDdTelemetry, - TestUiThreadExecutor() - ) + testedBridgeSdk = DdSdkImplementation(mockReactContext, mockDatadog, mockDdTelemetry, TestUiThreadExecutor()) DatadogSDKWrapperStorage.onInitializedListeners.clear() } From 10460c21eaca326ef48d1e18ffb3a2433887e6ac Mon Sep 17 00:00:00 2001 From: Marco Saia Date: Mon, 22 Sep 2025 16:20:27 +0200 Subject: [PATCH 041/410] Fixed internal testing tools and unit tests --- .../com/datadog/reactnative/DdSdkNativeInitialization.kt | 2 -- .../src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt | 6 +++++- .../src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt | 7 ++++++- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkNativeInitialization.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkNativeInitialization.kt index ee55d08fe..4388ad5f6 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkNativeInitialization.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkNativeInitialization.kt @@ -65,9 +65,7 @@ class DdSdkNativeInitialization internal constructor( datadog.initialize(appContext, sdkConfiguration, trackingConsent) Rum.enable(rumConfiguration, Datadog.getInstance()) - Logs.enable(logsConfiguration, Datadog.getInstance()) - Trace.enable(traceConfiguration, Datadog.getInstance()) } diff --git a/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt b/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt index 3c86e28b4..97acb2ebf 100644 --- a/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt +++ b/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt @@ -21,7 +21,11 @@ class DdSdk( ddTelemetry: DdTelemetry = DdTelemetry() ) : ReactContextBaseJavaModule(reactContext) { - private val implementation = DdSdkImplementation(reactContext, datadog = datadogWrapper, ddTelemetry) + private val implementation = DdSdkImplementation( + reactContext, + datadog = datadogWrapper, + ddTelemetry + ) private var lifecycleEventListener: LifecycleEventListener? = null override fun getName(): String = DdSdkImplementation.NAME diff --git a/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt b/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt index c9f91738f..8797bcc64 100644 --- a/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt +++ b/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt @@ -163,7 +163,12 @@ internal class DdSdkTest { answer.getArgument(0).run() true } - testedBridgeSdk = DdSdkImplementation(mockReactContext, mockDatadog, mockDdTelemetry, TestUiThreadExecutor()) + testedBridgeSdk = DdSdkImplementation( + mockReactContext, + mockDatadog, + mockDdTelemetry, + TestUiThreadExecutor() + ) DatadogSDKWrapperStorage.onInitializedListeners.clear() } From 94a50bbf8944382a0bd5c0f6dded229f9e4da8ff Mon Sep 17 00:00:00 2001 From: Sergio Barrio Date: Fri, 19 Sep 2025 17:37:31 +0200 Subject: [PATCH 042/410] Expose clearUserInfo API --- packages/core/__mocks__/react-native.ts | 3 + .../datadog/reactnative/DatadogSDKWrapper.kt | 4 ++ .../com/datadog/reactnative/DatadogWrapper.kt | 5 ++ .../reactnative/DdSdkImplementation.kt | 10 ++- .../kotlin/com/datadog/reactnative/DdSdk.kt | 8 +++ .../kotlin/com/datadog/reactnative/DdSdk.kt | 8 +++ .../com/datadog/reactnative/DdSdkTest.kt | 11 +++ packages/core/ios/Sources/DdSdk.mm | 12 +++- .../ios/Sources/DdSdkImplementation.swift | 8 ++- packages/core/ios/Tests/DdSdkTests.swift | 67 +++++++++++++++++++ packages/core/jest/mock.js | 3 + packages/core/src/DdSdkReactNative.tsx | 10 +++ .../src/__tests__/DdSdkReactNative.test.tsx | 26 +++++++ .../UserInfoSingleton/UserInfoSingleton.ts | 4 ++ .../__tests__/UserInfoSingleton.test.ts | 55 ++++++++++++--- packages/core/src/specs/NativeDdSdk.ts | 5 ++ packages/core/src/types.tsx | 5 ++ 17 files changed, 230 insertions(+), 14 deletions(-) diff --git a/packages/core/__mocks__/react-native.ts b/packages/core/__mocks__/react-native.ts index 260fe68a7..0e85e65ae 100644 --- a/packages/core/__mocks__/react-native.ts +++ b/packages/core/__mocks__/react-native.ts @@ -24,6 +24,9 @@ actualRN.NativeModules.DdSdk = { addUserExtraInfo: jest.fn().mockImplementation( () => new Promise(resolve => resolve()) ) as jest.MockedFunction, + clearUserInfo: jest.fn().mockImplementation( + () => new Promise(resolve => resolve()) + ) as jest.MockedFunction, setAttributes: jest.fn().mockImplementation( () => new Promise(resolve => resolve()) ) as jest.MockedFunction, diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogSDKWrapper.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogSDKWrapper.kt index 198061d14..f781687eb 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogSDKWrapper.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogSDKWrapper.kt @@ -86,6 +86,10 @@ internal class DatadogSDKWrapper : DatadogWrapper { Datadog.addUserProperties(extraInfo) } + override fun clearUserInfo() { + Datadog.clearUserInfo() + } + override fun addRumGlobalAttributes(attributes: Map) { val rumMonitor = this.getRumMonitor() for (attribute in attributes) { diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogWrapper.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogWrapper.kt index 3ae3e6266..49d606b35 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogWrapper.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogWrapper.kt @@ -86,6 +86,11 @@ interface DatadogWrapper { extraInfo: Map ) + /** + * Clears the user information. + */ + fun clearUserInfo() + /** * Adds global attributes. * diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt index 7a5c6848c..7adcf7438 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt @@ -102,7 +102,7 @@ class DdSdkImplementation( } /** - * Sets the user information. + * Sets the user extra information. * @param userExtraInfo: The additional information. (To set the id, name or email please user setUserInfo). */ fun addUserExtraInfo( @@ -114,6 +114,14 @@ class DdSdkImplementation( promise.resolve(null) } + /** + * Clears the user information. + */ + fun clearUserInfo(promise: Promise) { + datadog.clearUserInfo() + promise.resolve(null) + } + /** * Set the tracking consent regarding the data collection. * @param trackingConsent Consent, which can take one of the following values: 'pending', diff --git a/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdSdk.kt b/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdSdk.kt index cfafffffe..4e4668a3e 100644 --- a/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdSdk.kt +++ b/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdSdk.kt @@ -67,6 +67,14 @@ class DdSdk( implementation.addUserExtraInfo(extraInfo, promise) } + /** + * Clears the user information. + */ + @ReactMethod + override fun clearUserInfo(promise: Promise) { + implementation.clearUserInfo(promise) + } + /** * Set the tracking consent regarding the data collection. * @param trackingConsent Consent, which can take one of the following values: 'pending', diff --git a/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt b/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt index 97acb2ebf..0ebdd37fb 100644 --- a/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt +++ b/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt @@ -93,6 +93,14 @@ class DdSdk( implementation.addUserExtraInfo(extraInfo, promise) } + /** + * Clears the user information. + */ + @ReactMethod + fun clearUserInfo(promise: Promise) { + implementation.clearUserInfo(promise) + } + /** * Set the tracking consent regarding the data collection. * @param trackingConsent Consent, which can take one of the following values: 'pending', diff --git a/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt b/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt index 8797bcc64..f917bb847 100644 --- a/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt +++ b/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt @@ -2954,6 +2954,17 @@ internal class DdSdkTest { } } + @Test + fun `𝕄 clear user info 𝕎 clearUserInfo()`() { + // When + testedBridgeSdk.clearUserInfo(mockPromise) + + // Then + argumentCaptor> { + verify(mockDatadog).clearUserInfo() + } + } + @Test fun `𝕄 set RUM attributes 𝕎 setAttributes`( @MapForgery( diff --git a/packages/core/ios/Sources/DdSdk.mm b/packages/core/ios/Sources/DdSdk.mm index 3ead770ac..98a228f76 100644 --- a/packages/core/ios/Sources/DdSdk.mm +++ b/packages/core/ios/Sources/DdSdk.mm @@ -51,6 +51,12 @@ + (void)initFromNative { [self addUserExtraInfo:extraInfo resolve:resolve reject:reject]; } +RCT_EXPORT_METHOD(clearUserInfo:(RCTPromiseResolveBlock)resolve + withRejecter:(RCTPromiseRejectBlock)reject) +{ + [self clearUserInfo:resolve reject:reject]; +} + RCT_REMAP_METHOD(setTrackingConsent, withTrackingConsent:(NSString*)trackingConsent withResolver:(RCTPromiseResolveBlock)resolve withRejecter:(RCTPromiseRejectBlock)reject) @@ -81,7 +87,7 @@ + (void)initFromNative { [self consumeWebviewEvent:message resolve:resolve reject:reject]; } -RCT_REMAP_METHOD(clearAllData, withResolver:(RCTPromiseResolveBlock)resolve +RCT_EXPORT_METHOD(clearAllData:(RCTPromiseResolveBlock)resolve withRejecter:(RCTPromiseRejectBlock)reject) { [self clearAllData:resolve reject:reject]; @@ -141,6 +147,10 @@ - (void)setUserInfo:(NSDictionary *)userInfo resolve:(RCTPromiseResolveBlock)res [self.ddSdkImplementation setUserInfoWithUserInfo:userInfo resolve:resolve reject:reject]; } +- (void)clearUserInfo:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + [self.ddSdkImplementation clearUserInfoWithResolve:resolve reject:reject]; +} + -(void)addUserExtraInfo:(NSDictionary *)extraInfo resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { [self.ddSdkImplementation addUserExtraInfoWithExtraInfo:extraInfo resolve:resolve reject:reject]; } diff --git a/packages/core/ios/Sources/DdSdkImplementation.swift b/packages/core/ios/Sources/DdSdkImplementation.swift index 5a4bbf9a5..9d057d158 100644 --- a/packages/core/ios/Sources/DdSdkImplementation.swift +++ b/packages/core/ios/Sources/DdSdkImplementation.swift @@ -102,7 +102,7 @@ public class DdSdkImplementation: NSObject { resolve(nil) } - + @objc public func addUserExtraInfo(extraInfo: NSDictionary, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { let castedExtraInfo = castAttributesToSwift(extraInfo) @@ -111,6 +111,12 @@ public class DdSdkImplementation: NSObject { resolve(nil) } + @objc + public func clearUserInfo(resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { + Datadog.clearUserInfo() + resolve(nil) + } + @objc public func setTrackingConsent(trackingConsent: NSString, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { Datadog.set(trackingConsent: (trackingConsent as NSString?).asTrackingConsent()) diff --git a/packages/core/ios/Tests/DdSdkTests.swift b/packages/core/ios/Tests/DdSdkTests.swift index 555ce4549..efbf57b96 100644 --- a/packages/core/ios/Tests/DdSdkTests.swift +++ b/packages/core/ios/Tests/DdSdkTests.swift @@ -651,6 +651,73 @@ class DdSdkTests: XCTestCase { XCTFail("extra-info-4 is not of expected type or value") } } + + func testClearUserInfo() throws { + let bridge = DdSdkImplementation( + mainDispatchQueue: DispatchQueueMock(), + jsDispatchQueue: DispatchQueueMock(), + jsRefreshRateMonitor: JSRefreshRateMonitor(), + RUMMonitorProvider: { MockRUMMonitor() }, + RUMMonitorInternalProvider: { nil } + ) + bridge.initialize( + configuration: .mockAny(), + resolve: mockResolve, + reject: mockReject + ) + + bridge.setUserInfo( + userInfo: NSDictionary( + dictionary: [ + "id": "id_123", + "name": "John Doe", + "email": "john@doe.com", + "extraInfo": [ + "extra-info-1": 123, + "extra-info-2": "abc", + "extra-info-3": true, + "extra-info-4": [ + "nested-extra-info-1": 456 + ], + ], + ] + ), + resolve: mockResolve, + reject: mockReject + ) + + var ddContext = try XCTUnwrap(CoreRegistry.default as? DatadogCore).contextProvider.read() + var userInfo = try XCTUnwrap(ddContext.userInfo) + + XCTAssertEqual(userInfo.id, "id_123") + XCTAssertEqual(userInfo.name, "John Doe") + XCTAssertEqual(userInfo.email, "john@doe.com") + XCTAssertEqual(userInfo.extraInfo["extra-info-1"] as? Int64, 123) + XCTAssertEqual(userInfo.extraInfo["extra-info-2"] as? String, "abc") + XCTAssertEqual(userInfo.extraInfo["extra-info-3"] as? Bool, true) + + if let extraInfo4Encodable = userInfo.extraInfo["extra-info-4"] + as? DatadogSDKReactNative.AnyEncodable, + let extraInfo4Dict = extraInfo4Encodable.value as? [String: Int] + { + XCTAssertEqual(extraInfo4Dict, ["nested-extra-info-1": 456]) + } else { + XCTFail("extra-info-4 is not of expected type or value") + } + + bridge.clearUserInfo(resolve: mockResolve, reject: mockReject) + + ddContext = try XCTUnwrap(CoreRegistry.default as? DatadogCore).contextProvider.read() + userInfo = try XCTUnwrap(ddContext.userInfo) + + XCTAssertEqual(userInfo.id, nil) + XCTAssertEqual(userInfo.name, nil) + XCTAssertEqual(userInfo.email, nil) + XCTAssertEqual(userInfo.extraInfo["extra-info-1"] as? Int64, nil) + XCTAssertEqual(userInfo.extraInfo["extra-info-2"] as? String, nil) + XCTAssertEqual(userInfo.extraInfo["extra-info-3"] as? Bool, nil) + XCTAssertEqual(userInfo.extraInfo["extra-info-4"] as? [String: Int], nil) + } func testSettingAttributes() { let rumMonitorMock = MockRUMMonitor() diff --git a/packages/core/jest/mock.js b/packages/core/jest/mock.js index a8161295a..8e154c4cd 100644 --- a/packages/core/jest/mock.js +++ b/packages/core/jest/mock.js @@ -33,6 +33,9 @@ module.exports = { addUserExtraInfo: jest .fn() .mockImplementation(() => new Promise(resolve => resolve())), + clearUserInfo: jest + .fn() + .mockImplementation(() => new Promise(resolve => resolve())), setAttributes: jest .fn() .mockImplementation(() => new Promise(resolve => resolve())), diff --git a/packages/core/src/DdSdkReactNative.tsx b/packages/core/src/DdSdkReactNative.tsx index 668ae09f3..1838542df 100644 --- a/packages/core/src/DdSdkReactNative.tsx +++ b/packages/core/src/DdSdkReactNative.tsx @@ -214,6 +214,16 @@ export class DdSdkReactNative { UserInfoSingleton.getInstance().setUserInfo(userInfo); }; + /** + * Clears the user information. + * @returns a Promise. + */ + static clearUserInfo = async (): Promise => { + InternalLog.log('Clearing user info', SdkVerbosity.DEBUG); + await DdSdk.clearUserInfo(); + UserInfoSingleton.getInstance().clearUserInfo(); + }; + /** * Set the user information. * @param extraUserInfo: The additional information. (To set the id, name or email please user setUserInfo). diff --git a/packages/core/src/__tests__/DdSdkReactNative.test.tsx b/packages/core/src/__tests__/DdSdkReactNative.test.tsx index f9405aa51..57ff5d0ed 100644 --- a/packages/core/src/__tests__/DdSdkReactNative.test.tsx +++ b/packages/core/src/__tests__/DdSdkReactNative.test.tsx @@ -1112,6 +1112,32 @@ describe('DdSdkReactNative', () => { }); }); + describe('clearUserInfo', () => { + it('calls SDK method when clearUserInfo, and clears the user in UserProvider', async () => { + // GIVEN + const userInfo = { + id: 'id', + name: 'name', + email: 'email', + extraInfo: { + foo: 'bar' + } + }; + + await DdSdkReactNative.setUserInfo(userInfo); + + // WHEN + await DdSdkReactNative.clearUserInfo(); + + // THEN + expect(DdSdk.clearUserInfo).toHaveBeenCalledTimes(1); + expect(DdSdk.setUserInfo).toHaveBeenCalled(); + expect(UserInfoSingleton.getInstance().getUserInfo()).toEqual( + undefined + ); + }); + }); + describe('setTrackingConsent', () => { it('calls SDK method when setTrackingConsent', async () => { // GIVEN diff --git a/packages/core/src/sdk/UserInfoSingleton/UserInfoSingleton.ts b/packages/core/src/sdk/UserInfoSingleton/UserInfoSingleton.ts index 26392d794..3ce23614b 100644 --- a/packages/core/src/sdk/UserInfoSingleton/UserInfoSingleton.ts +++ b/packages/core/src/sdk/UserInfoSingleton/UserInfoSingleton.ts @@ -16,6 +16,10 @@ class UserInfoProvider { getUserInfo = (): UserInfo | undefined => { return this.userInfo; }; + + clearUserInfo = () => { + this.userInfo = undefined; + }; } export class UserInfoSingleton { diff --git a/packages/core/src/sdk/UserInfoSingleton/__tests__/UserInfoSingleton.test.ts b/packages/core/src/sdk/UserInfoSingleton/__tests__/UserInfoSingleton.test.ts index 1f7ae84e7..f8e7276d6 100644 --- a/packages/core/src/sdk/UserInfoSingleton/__tests__/UserInfoSingleton.test.ts +++ b/packages/core/src/sdk/UserInfoSingleton/__tests__/UserInfoSingleton.test.ts @@ -7,27 +7,60 @@ import { UserInfoSingleton } from '../UserInfoSingleton'; describe('UserInfoSingleton', () => { - it('sets, returns and resets the user info', () => { + beforeEach(() => { + UserInfoSingleton.reset(); + }); + + it('returns undefined by default', () => { + expect(UserInfoSingleton.getInstance().getUserInfo()).toBeUndefined(); + }); + + it('stores and returns user info after setUserInfo', () => { + const info = { + id: 'test', + email: 'user@mail.com', + extraInfo: { loggedIn: true } + }; + + UserInfoSingleton.getInstance().setUserInfo(info); + + expect(UserInfoSingleton.getInstance().getUserInfo()).toEqual(info); + }); + + it('clears user info with clearUserInfo', () => { UserInfoSingleton.getInstance().setUserInfo({ id: 'test', email: 'user@mail.com', - extraInfo: { - loggedIn: true - } + extraInfo: { loggedIn: true } }); - expect(UserInfoSingleton.getInstance().getUserInfo()).toEqual({ + UserInfoSingleton.getInstance().clearUserInfo(); + + expect(UserInfoSingleton.getInstance().getUserInfo()).toBeUndefined(); + }); + + it('reset() replaces the provider and clears stored user info', () => { + const instanceBefore = UserInfoSingleton.getInstance(); + + UserInfoSingleton.getInstance().setUserInfo({ id: 'test', email: 'user@mail.com', - extraInfo: { - loggedIn: true - } + extraInfo: { loggedIn: true } }); UserInfoSingleton.reset(); - expect(UserInfoSingleton.getInstance().getUserInfo()).toEqual( - undefined - ); + const instanceAfter = UserInfoSingleton.getInstance(); + + expect(instanceAfter).not.toBe(instanceBefore); + + expect(instanceAfter.getUserInfo()).toBeUndefined(); + }); + + it('getInstance returns the same provider between calls (singleton behavior)', () => { + const a = UserInfoSingleton.getInstance(); + const b = UserInfoSingleton.getInstance(); + + expect(a).toBe(b); }); }); diff --git a/packages/core/src/specs/NativeDdSdk.ts b/packages/core/src/specs/NativeDdSdk.ts index bbf2572ee..a2ce1120e 100644 --- a/packages/core/src/specs/NativeDdSdk.ts +++ b/packages/core/src/specs/NativeDdSdk.ts @@ -37,6 +37,11 @@ export interface Spec extends TurboModule { */ setUserInfo(user: Object): Promise; + /** + * Clears the user information. + */ + clearUserInfo(): Promise; + /** * Add custom attributes to the current user information * @param extraInfo: The extraInfo object containing additionall custom attributes diff --git a/packages/core/src/types.tsx b/packages/core/src/types.tsx index fe1a5895c..bad19d429 100644 --- a/packages/core/src/types.tsx +++ b/packages/core/src/types.tsx @@ -98,6 +98,11 @@ export type DdSdkType = { */ setUserInfo(userInfo: UserInfo): Promise; + /** + * Clears the user information. + */ + clearUserInfo(): Promise; + /** * Add additional user information. * @param extraUserInfo: The additional information. (To set the id, name or email please user setUserInfo). From 17b2d2149dfbdcbea5f4683ce3b843df2032be2f Mon Sep 17 00:00:00 2001 From: Sergio Barrio Date: Mon, 29 Sep 2025 12:20:44 +0200 Subject: [PATCH 043/410] Update attribute API --- benchmarks/src/testSetup/monitor.ts | 5 +- example/src/ddUtils.tsx | 4 +- packages/codepush/__mocks__/react-native.ts | 13 +- packages/core/__mocks__/react-native.ts | 13 +- .../datadog/reactnative/DatadogSDKWrapper.kt | 17 ++- .../com/datadog/reactnative/DatadogWrapper.kt | 22 ++++ .../reactnative/DdSdkImplementation.kt | 51 ++++++- .../kotlin/com/datadog/reactnative/DdSdk.kt | 39 +++++- .../kotlin/com/datadog/reactnative/DdSdk.kt | 37 +++++- .../com/datadog/reactnative/DdSdkTest.kt | 118 ++++++++++++++++- packages/core/ios/Sources/AnyEncodable.swift | 21 ++- packages/core/ios/Sources/DdSdk.mm | 42 +++++- .../ios/Sources/DdSdkImplementation.swift | 31 ++++- packages/core/ios/Sources/GlobalState.swift | 2 +- packages/core/ios/Tests/DdSdkTests.swift | 124 +++++++++++++++++- packages/core/ios/Tests/MockRUMMonitor.swift | 12 +- packages/core/jest/mock.js | 11 +- packages/core/src/DdSdkReactNative.tsx | 53 +++++++- .../src/__tests__/DdSdkReactNative.test.tsx | 68 +++++++++- .../AttributesSingleton.ts | 26 +++- .../__tests__/AttributesSingleton.test.ts | 60 +++++++-- packages/core/src/specs/NativeDdSdk.ts | 23 +++- packages/core/src/types.tsx | 21 ++- .../__mocks__/react-native.ts | 4 +- 24 files changed, 742 insertions(+), 75 deletions(-) diff --git a/benchmarks/src/testSetup/monitor.ts b/benchmarks/src/testSetup/monitor.ts index c7cc23473..93ea0fccd 100644 --- a/benchmarks/src/testSetup/monitor.ts +++ b/benchmarks/src/testSetup/monitor.ts @@ -4,8 +4,7 @@ * Copyright 2016-Present Datadog, Inc. */ -import { DefaultTimeProvider, RumActionType } from "@datadog/mobile-react-native"; -import { ErrorSource } from "@datadog/mobile-react-native/lib/typescript/rum/types"; +import { DefaultTimeProvider, ErrorSource, RumActionType } from "@datadog/mobile-react-native"; import type { DdRumType, ResourceKind } from "@datadog/mobile-react-native/lib/typescript/rum/types"; import type { GestureResponderEvent } from "react-native/types"; @@ -72,4 +71,4 @@ export const Monitor: Pick { DdLogs.info('The RN Sdk was properly initialized') DdSdkReactNative.setUserInfo({id: "1337", name: "Xavier", email: "xg@example.com", extraInfo: { type: "premium" } }) - DdSdkReactNative.setAttributes({campaign: "ad-network"}) + DdSdkReactNative.addAttributes({campaign: "ad-network"}) }); } diff --git a/packages/codepush/__mocks__/react-native.ts b/packages/codepush/__mocks__/react-native.ts index 046ced2f6..0c8189840 100644 --- a/packages/codepush/__mocks__/react-native.ts +++ b/packages/codepush/__mocks__/react-native.ts @@ -18,9 +18,18 @@ actualRN.NativeModules.DdSdk = { initialize: jest.fn().mockImplementation( () => new Promise(resolve => resolve()) ) as jest.MockedFunction, - setAttributes: jest.fn().mockImplementation( + addAttribute: jest.fn().mockImplementation( () => new Promise(resolve => resolve()) - ) as jest.MockedFunction, + ) as jest.MockedFunction, + removeAttribute: jest.fn().mockImplementation( + () => new Promise(resolve => resolve()) + ) as jest.MockedFunction, + addAttributes: jest.fn().mockImplementation( + () => new Promise(resolve => resolve()) + ) as jest.MockedFunction, + removeAttributes: jest.fn().mockImplementation( + () => new Promise(resolve => resolve()) + ) as jest.MockedFunction, setTrackingConsent: jest.fn().mockImplementation( () => new Promise(resolve => resolve()) ) as jest.MockedFunction, diff --git a/packages/core/__mocks__/react-native.ts b/packages/core/__mocks__/react-native.ts index 0e85e65ae..24e3f80c7 100644 --- a/packages/core/__mocks__/react-native.ts +++ b/packages/core/__mocks__/react-native.ts @@ -27,9 +27,18 @@ actualRN.NativeModules.DdSdk = { clearUserInfo: jest.fn().mockImplementation( () => new Promise(resolve => resolve()) ) as jest.MockedFunction, - setAttributes: jest.fn().mockImplementation( + addAttribute: jest.fn().mockImplementation( () => new Promise(resolve => resolve()) - ) as jest.MockedFunction, + ) as jest.MockedFunction, + removeAttribute: jest.fn().mockImplementation( + () => new Promise(resolve => resolve()) + ) as jest.MockedFunction, + addAttributes: jest.fn().mockImplementation( + () => new Promise(resolve => resolve()) + ) as jest.MockedFunction, + removeAttributes: jest.fn().mockImplementation( + () => new Promise(resolve => resolve()) + ) as jest.MockedFunction, setTrackingConsent: jest.fn().mockImplementation( () => new Promise(resolve => resolve()) ) as jest.MockedFunction, diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogSDKWrapper.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogSDKWrapper.kt index f781687eb..06151d834 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogSDKWrapper.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogSDKWrapper.kt @@ -89,11 +89,24 @@ internal class DatadogSDKWrapper : DatadogWrapper { override fun clearUserInfo() { Datadog.clearUserInfo() } + + override fun addRumGlobalAttribute(key: String, value: Any?) { + this.getRumMonitor().addAttribute(key, value) + } + + override fun removeRumGlobalAttribute(key: String) { + this.getRumMonitor().removeAttribute(key) + } override fun addRumGlobalAttributes(attributes: Map) { - val rumMonitor = this.getRumMonitor() for (attribute in attributes) { - rumMonitor.addAttribute(attribute.key, attribute.value) + this.addRumGlobalAttribute(attribute.key, attribute.value) + } + } + + override fun removeRumGlobalAttributes(keys: Array) { + for (key in keys) { + this.removeRumGlobalAttribute(key) } } diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogWrapper.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogWrapper.kt index 49d606b35..d6395b18b 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogWrapper.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogWrapper.kt @@ -91,6 +91,21 @@ interface DatadogWrapper { */ fun clearUserInfo() + + /** Adds a global attribute. + * + * @param key: Key that identifies the attribute. + * @param value: Value linked to the attribute. + */ + fun addRumGlobalAttribute(key: String, value: Any?) + + /** + * Removes a global attribute. + * + * @param key: Key that identifies the attribute. + */ + fun removeRumGlobalAttribute(key: String) + /** * Adds global attributes. * @@ -98,6 +113,13 @@ interface DatadogWrapper { */ fun addRumGlobalAttributes(attributes: Map) + /** + * Removes global attributes. + * + * @param keys Keys linked to the attributes to be removed + */ + fun removeRumGlobalAttributes(keys: Array) + /** * Sets tracking consent. */ diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt index 7adcf7438..ed545d9e1 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt @@ -14,6 +14,7 @@ import com.datadog.android.rum.configuration.VitalsUpdateFrequency import com.facebook.react.bridge.LifecycleEventListener import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.ReadableArray import com.facebook.react.bridge.ReadableMap import java.util.Locale import java.util.concurrent.TimeUnit @@ -66,11 +67,35 @@ class DdSdkImplementation( } /** - * Sets the global context (set of attributes) attached with all future Logs, Spans and RUM + * Sets a specific attribute in the global context attached with all future Logs, Spans and RUM. + * + * @param key: Key that identifies the attribute. + * @param value: Value linked to the attribute. + */ + fun addAttribute(key: String, value: ReadableMap, promise: Promise) { + val attributeValue = value.toMap()["value"] + datadog.addRumGlobalAttribute(key, attributeValue) + GlobalState.addAttribute(key, attributeValue) + promise.resolve(null) + } + + /** + * Removes an attribute from the global context attached with all future Logs, Spans and RUM events. + * @param key: They key associated with the attribute to be removed. + */ + fun removeAttribute(key: String, promise: Promise) { + datadog.removeRumGlobalAttribute(key) + GlobalState.removeAttribute(key) + promise.resolve(null) + } + + + /** + * Adds a set of attributes to the global context that is attached with all future Logs, Spans and RUM * events. - * @param attributes The global context attributes. + * @param attributes: The global context attributes. */ - fun setAttributes(attributes: ReadableMap, promise: Promise) { + fun addAttributes(attributes: ReadableMap, promise: Promise) { datadog.addRumGlobalAttributes(attributes.toHashMap()) for ((k,v) in attributes.toHashMap()) { GlobalState.addAttribute(k, v) @@ -78,6 +103,26 @@ class DdSdkImplementation( promise.resolve(null) } + /** + * Removes a set of attributes from the global context that is attached with all future Logs, Spans and RUM + * events. + * @param keys: They keys associated with the attributes to be removed. + */ + fun removeAttributes(keys: ReadableArray, promise: Promise) { + val keysArray = mutableListOf() + for (i in 0 until keys.size()) { + val key: String = keys.getString(i) + keysArray.add(key) + } + val keysStringArray = keysArray.toTypedArray() + + datadog.removeRumGlobalAttributes(keysStringArray) + for (key in keysStringArray) { + GlobalState.removeAttribute(key) + } + promise.resolve(null) + } + /** * Set the user information. * @param userInfo The user object (use builtin attributes: 'id', 'email', 'name', and any custom diff --git a/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdSdk.kt b/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdSdk.kt index 4e4668a3e..a9d430081 100644 --- a/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdSdk.kt +++ b/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdSdk.kt @@ -12,13 +12,14 @@ import com.facebook.react.bridge.LifecycleEventListener import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.ReactMethod +import com.facebook.react.bridge.ReadableArray import com.facebook.react.bridge.ReadableMap import com.facebook.react.modules.core.DeviceEventManagerModule /** The entry point to initialize Datadog's features. */ class DdSdk( reactContext: ReactApplicationContext, - datadogWrapper: DatadogWrapper = DatadogSDKWrapper() + datadogWrapper: DatadogWrapper = DatadogSDKWrapper(), ddTelemetry: DdTelemetry = DdTelemetry() ) : NativeDdSdkSpec(reactContext) { @@ -40,13 +41,43 @@ class DdSdk( } /** - * Sets the global context (set of attributes) attached with all future Logs, Spans and RUM + * Sets a specific attribute in the global context attached with all future Logs, Spans and RUM + * + * @param key: Key that identifies the attribute. + * @param value: Value linked to the attribute. + */ + @ReactMethod + override fun addAttribute(key: String, value: ReadableMap, promise: Promise) { + implementation.addAttribute(key, value, promise) + } + + /** + * Removes an attribute from the context attached with all future Logs, Spans and RUM events. + * @param key: They key associated with the attribute to be removed. + */ + @ReactMethod + override fun removeAttribute(key: String, promise: Promise) { + implementation.removeAttribute(key, promise) + } + + /** + * Adds a set of attributes to the global context that is attached with all future Logs, Spans and RUM * events. * @param attributes The global context attributes. */ @ReactMethod - override fun setAttributes(attributes: ReadableMap, promise: Promise) { - implementation.setAttributes(attributes, promise) + override fun addAttributes(attributes: ReadableMap, promise: Promise) { + implementation.addAttributes(attributes, promise) + } + + /** + * Removes a set of attributes from the global context that is attached with all future Logs, Spans and RUM + * events. + * @param keys: They keys associated with the attributes to be removed. + */ + @ReactMethod + override fun removeAttributes(keys: ReadableArray, promise: Promise) { + implementation.removeAttributes(keys, promise) } /** diff --git a/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt b/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt index 0ebdd37fb..958ba521b 100644 --- a/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt +++ b/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt @@ -12,6 +12,7 @@ import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.ReactContextBaseJavaModule import com.facebook.react.bridge.ReactMethod +import com.facebook.react.bridge.ReadableArray import com.facebook.react.bridge.ReadableMap /** The entry point to initialize Datadog's features. */ @@ -66,13 +67,43 @@ class DdSdk( } /** - * Sets the global context (set of attributes) attached with all future Logs, Spans and RUM + * Sets a specific attribute in the global context attached with all future Logs, Spans and RUM + * + * @param key: Key that identifies the attribute. + * @param value: Value linked to the attribute. + */ + @ReactMethod + fun addAttribute(key: String, value: ReadableMap, promise: Promise) { + implementation.addAttribute(key, value, promise) + } + + /** + * Removes an attribute from the context attached with all future Logs, Spans and RUM events. + * @param key: They key associated with the attribute to be removed. + */ + @ReactMethod + fun removeAttribute(key: String, promise: Promise) { + implementation.removeAttribute(key, promise) + } + + /** + * Adds a set of attributes to the global context that is attached with all future Logs, Spans and RUM * events. * @param attributes The global context attributes. */ @ReactMethod - fun setAttributes(attributes: ReadableMap, promise: Promise) { - implementation.setAttributes(attributes, promise) + fun addAttributes(attributes: ReadableMap, promise: Promise) { + implementation.addAttributes(attributes, promise) + } + + /** + * Removes a set of attributes from the global context that is attached with all future Logs, Spans and RUM + * events. + * @param keys: They keys associated with the attributes to be removed. + */ + @ReactMethod + fun removeAttributes(keys: ReadableArray, promise: Promise) { + implementation.removeAttributes(keys, promise) } /** diff --git a/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt b/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt index f917bb847..ae6ded89c 100644 --- a/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt +++ b/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt @@ -40,6 +40,7 @@ import com.datadog.tools.unit.setStaticValue import com.datadog.tools.unit.toReadableArray import com.datadog.tools.unit.toReadableJavaOnlyMap import com.datadog.tools.unit.toReadableMap +import com.facebook.react.bridge.JavaOnlyMap import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.ReadableMap @@ -78,7 +79,6 @@ import org.mockito.kotlin.doReturn import org.mockito.kotlin.doThrow import org.mockito.kotlin.eq import org.mockito.kotlin.inOrder -import org.mockito.kotlin.isNotNull import org.mockito.kotlin.isNull import org.mockito.kotlin.mock import org.mockito.kotlin.never @@ -2966,28 +2966,96 @@ internal class DdSdkTest { } @Test - fun `𝕄 set RUM attributes 𝕎 setAttributes`( + fun `M set Rum attribute W addAttribute`( + @StringForgery(type = StringForgeryType.NUMERICAL) key: String, + @StringForgery(type = StringForgeryType.ASCII) value: String + ) { + // When + val attributeMap = JavaOnlyMap().apply { + putString("value", value) + } + testedBridgeSdk.addAttribute(key, attributeMap, mockPromise) + + // Then + verify(mockDatadog).addRumGlobalAttribute(key, value) + } + + @Test + fun `M set GlobalState attribute W addAttribute`( + @StringForgery(type = StringForgeryType.NUMERICAL) key: String, + @StringForgery(type = StringForgeryType.ASCII) value: String + ) { + // When + val attributeMap = JavaOnlyMap().apply { + putString("value", value) + } + testedBridgeSdk.addAttribute(key, attributeMap, mockPromise) + + // Then + assertThat(GlobalState.globalAttributes).containsEntry(key, value) + } + + @Test + fun `M remove Rum attribute W removeAttribute`( + @StringForgery(type = StringForgeryType.NUMERICAL) key: String, + @StringForgery(type = StringForgeryType.ASCII) value: String + ) { + // Given + val attributeMap = JavaOnlyMap().apply { + putString("value", value) + } + testedBridgeSdk.addAttribute(key, attributeMap, mockPromise) + assertThat(GlobalState.globalAttributes).containsEntry(key, value) + + // When + testedBridgeSdk.removeAttribute(key, mockPromise) + + // Then + verify(mockDatadog).removeRumGlobalAttribute(key) + } + + @Test + fun `M remove GlobalState attribute W removeAttribute`( + @StringForgery(type = StringForgeryType.NUMERICAL) key: String, + @StringForgery(type = StringForgeryType.ASCII) value: String + ) { + // Given + val attributeMap = JavaOnlyMap().apply { + putString("value", value) + } + testedBridgeSdk.addAttribute(key, attributeMap, mockPromise) + assertThat(GlobalState.globalAttributes).containsEntry(key, value) + + // When + testedBridgeSdk.removeAttribute(key, mockPromise) + + // Then + assertThat(GlobalState.globalAttributes).doesNotContainEntry(key, value) + } + + @Test + fun `𝕄 set RUM attributes 𝕎 addAttributes`( @MapForgery( key = AdvancedForgery(string = [StringForgery(StringForgeryType.NUMERICAL)]), value = AdvancedForgery(string = [StringForgery(StringForgeryType.ASCII)]) ) customAttributes: Map ) { // When - testedBridgeSdk.setAttributes(customAttributes.toReadableMap(), mockPromise) + testedBridgeSdk.addAttributes(customAttributes.toReadableMap(), mockPromise) // Then verify(mockDatadog).addRumGlobalAttributes(customAttributes) } @Test - fun `𝕄 set GlobalState attributes 𝕎 setAttributes`( + fun `𝕄 set GlobalState attributes 𝕎 addAttributes`( @MapForgery( key = AdvancedForgery(string = [StringForgery(StringForgeryType.NUMERICAL)]), value = AdvancedForgery(string = [StringForgery(StringForgeryType.ASCII)]) ) customAttributes: Map ) { // When - testedBridgeSdk.setAttributes(customAttributes.toReadableMap(), mockPromise) + testedBridgeSdk.addAttributes(customAttributes.toReadableMap(), mockPromise) // Then customAttributes.forEach { (k, v) -> @@ -2995,6 +3063,46 @@ internal class DdSdkTest { } } + @Test + fun `𝕄 remove RUM attributes 𝕎 removeAttributes`( + @MapForgery( + key = AdvancedForgery(string = [StringForgery(StringForgeryType.NUMERICAL)]), + value = AdvancedForgery(string = [StringForgery(StringForgeryType.ASCII)]) + ) customAttributes: Map + ) { + // Given + testedBridgeSdk.addAttributes(customAttributes.toReadableMap(), mockPromise) + verify(mockDatadog).addRumGlobalAttributes(customAttributes) + + // When + val keys = customAttributes.keys.toReadableArray() + testedBridgeSdk.removeAttributes(keys, mockPromise) + + // Then + verify(mockDatadog).removeRumGlobalAttributes(customAttributes.keys.toTypedArray()) + } + + @Test + fun `𝕄 remve GlobalState attributes 𝕎 removeAttributes`( + @MapForgery( + key = AdvancedForgery(string = [StringForgery(StringForgeryType.NUMERICAL)]), + value = AdvancedForgery(string = [StringForgery(StringForgeryType.ASCII)]) + ) customAttributes: Map + ) { + // Given + testedBridgeSdk.addAttributes(customAttributes.toReadableMap(), mockPromise) + verify(mockDatadog).addRumGlobalAttributes(customAttributes) + + // When + val keys = customAttributes.keys.toReadableArray() + testedBridgeSdk.removeAttributes(keys, mockPromise) + + // Then + customAttributes.forEach { (k, v) -> + assertThat(GlobalState.globalAttributes).doesNotContainEntry(k, v) + } + } + @Test fun `𝕄 build Granted consent 𝕎 buildTrackingConsent {granted}`(forge: Forge) { // When diff --git a/packages/core/ios/Sources/AnyEncodable.swift b/packages/core/ios/Sources/AnyEncodable.swift index 39821af87..7fac7bb3b 100644 --- a/packages/core/ios/Sources/AnyEncodable.swift +++ b/packages/core/ios/Sources/AnyEncodable.swift @@ -14,18 +14,25 @@ internal func castAttributesToSwift(_ attributes: [String: Any]) -> [String: Enc var casted: [String: Encodable] = [:] attributes.forEach { key, value in - if let castedValue = castByPreservingTypeInformation(attributeValue: value) { - // If possible, cast attribute by preserving its type information - casted[key] = castedValue - } else { - // Otherwise, cast by preserving its encoded value (and loosing type information) - casted[key] = castByPreservingEncodedValue(attributeValue: value) - } + casted[key] = castValueToSwift(value) } return casted } +internal func castValueToSwift(_ value: Any) -> Encodable { + var casted: Encodable + if let castedValue = castByPreservingTypeInformation(attributeValue: value) { + // If possible, cast attribute by preserving its type information + casted = castedValue + } else { + // Otherwise, cast by preserving its encoded value (and loosing type information) + casted = castByPreservingEncodedValue(attributeValue: value) + } + + return casted +} + /// Casts `Any` value to `Encodable` by preserving its type information. private func castByPreservingTypeInformation(attributeValue: Any) -> Encodable? { switch attributeValue { diff --git a/packages/core/ios/Sources/DdSdk.mm b/packages/core/ios/Sources/DdSdk.mm index 98a228f76..7129d7af1 100644 --- a/packages/core/ios/Sources/DdSdk.mm +++ b/packages/core/ios/Sources/DdSdk.mm @@ -30,11 +30,33 @@ + (void)initFromNative { [self initialize:configuration resolve:resolve reject:reject]; } -RCT_REMAP_METHOD(setAttributes, withAttributes:(NSDictionary*)attributes +RCT_EXPORT_METHOD(addAttribute:(NSString*) key + withValue:(NSDictionary*) value + withResolver:(RCTPromiseResolveBlock)resolve + withRejecter:(RCTPromiseRejectBlock)reject) +{ + [self addAttribute:key value:value resolve:resolve reject:reject]; +} + +RCT_EXPORT_METHOD(removeAttribute:(NSString*) key + withResolver:(RCTPromiseResolveBlock)resolve + withRejecter:(RCTPromiseRejectBlock)reject) +{ + [self removeAttribute:key resolve:resolve reject:reject]; +} + +RCT_REMAP_METHOD(addAttributes, withAttributes:(NSDictionary*)attributes withResolver:(RCTPromiseResolveBlock)resolve withRejecter:(RCTPromiseRejectBlock)reject) { - [self setAttributes:attributes resolve:resolve reject:reject]; + [self addAttributes:attributes resolve:resolve reject:reject]; +} + +RCT_REMAP_METHOD(removeAttributes, withKeys:(NSArray *)keys + withResolver:(RCTPromiseResolveBlock)resolve + withRejecter:(RCTPromiseRejectBlock)reject) +{ + [self removeAttributes:keys resolve:resolve reject:reject]; } RCT_REMAP_METHOD(setUserInfo, withUserInfo:(NSDictionary*)userInfo @@ -135,8 +157,20 @@ - (void)initialize:(NSDictionary *)configuration resolve:(RCTPromiseResolveBlock [self.ddSdkImplementation initializeWithConfiguration:configuration resolve:resolve reject:reject]; } -- (void)setAttributes:(NSDictionary *)attributes resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { - [self.ddSdkImplementation setAttributesWithAttributes:attributes resolve:resolve reject:reject]; +- (void)addAttribute:(NSString *)key value:(NSDictionary *)value resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + [self.ddSdkImplementation addAttributeWithKey:key value:value resolve:resolve reject:reject]; +} + +- (void)removeAttribute:(NSString *)key resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + [self.ddSdkImplementation removeAttributeWithKey:key resolve:resolve reject:reject]; +} + +- (void)addAttributes:(NSDictionary *)attributes resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + [self.ddSdkImplementation addAttributesWithAttributes:attributes resolve:resolve reject:reject]; +} + +- (void)removeAttributes:(NSArray *)keys resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + [self.ddSdkImplementation removeAttributesWithKeys:keys resolve:resolve reject:reject]; } - (void)setTrackingConsent:(NSString *)trackingConsent resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { diff --git a/packages/core/ios/Sources/DdSdkImplementation.swift b/packages/core/ios/Sources/DdSdkImplementation.swift index 9d057d158..16adb50f2 100644 --- a/packages/core/ios/Sources/DdSdkImplementation.swift +++ b/packages/core/ios/Sources/DdSdkImplementation.swift @@ -69,14 +69,43 @@ public class DdSdkImplementation: NSObject { resolve(nil) } + + @objc + public func addAttribute(key: AttributeKey, value: NSDictionary, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { + if let attributeValue = value.object(forKey: "value") { + let castedValue = castValueToSwift(attributeValue) + RUMMonitorProvider().addAttribute(forKey: key, value: castedValue) + GlobalState.addAttribute(forKey: key, value: castedValue) + } + + resolve(nil) + } + + @objc + public func removeAttribute(key: AttributeKey, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { + RUMMonitorProvider().removeAttribute(forKey: key) + GlobalState.removeAttribute(key: key) + + resolve(nil) + } @objc - public func setAttributes(attributes: NSDictionary, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { + public func addAttributes(attributes: NSDictionary, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { let castedAttributes = castAttributesToSwift(attributes) for (key, value) in castedAttributes { RUMMonitorProvider().addAttribute(forKey: key, value: value) GlobalState.addAttribute(forKey: key, value: value) } + + resolve(nil) + } + + @objc + public func removeAttributes(keys: [AttributeKey], resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { + RUMMonitorProvider().removeAttributes(forKeys: keys) + for (key) in keys { + GlobalState.removeAttribute(key: key) + } resolve(nil) } diff --git a/packages/core/ios/Sources/GlobalState.swift b/packages/core/ios/Sources/GlobalState.swift index b932803a1..a758bf0ef 100644 --- a/packages/core/ios/Sources/GlobalState.swift +++ b/packages/core/ios/Sources/GlobalState.swift @@ -15,7 +15,7 @@ internal struct GlobalState { } internal static func removeAttribute(key: String) { - GlobalState.globalAttributes.removeValue(forKey: key) + GlobalState.globalAttributes[key] = nil } } diff --git a/packages/core/ios/Tests/DdSdkTests.swift b/packages/core/ios/Tests/DdSdkTests.swift index efbf57b96..adbb57da9 100644 --- a/packages/core/ios/Tests/DdSdkTests.swift +++ b/packages/core/ios/Tests/DdSdkTests.swift @@ -651,7 +651,7 @@ class DdSdkTests: XCTestCase { XCTFail("extra-info-4 is not of expected type or value") } } - + func testClearUserInfo() throws { let bridge = DdSdkImplementation( mainDispatchQueue: DispatchQueueMock(), @@ -704,12 +704,12 @@ class DdSdkTests: XCTestCase { } else { XCTFail("extra-info-4 is not of expected type or value") } - + bridge.clearUserInfo(resolve: mockResolve, reject: mockReject) - + ddContext = try XCTUnwrap(CoreRegistry.default as? DatadogCore).contextProvider.read() userInfo = try XCTUnwrap(ddContext.userInfo) - + XCTAssertEqual(userInfo.id, nil) XCTAssertEqual(userInfo.name, nil) XCTAssertEqual(userInfo.email, nil) @@ -719,7 +719,59 @@ class DdSdkTests: XCTestCase { XCTAssertEqual(userInfo.extraInfo["extra-info-4"] as? [String: Int], nil) } - func testSettingAttributes() { + func testRemovingAttribute() { + let rumMonitorMock = MockRUMMonitor() + let bridge = DdSdkImplementation( + mainDispatchQueue: DispatchQueueMock(), + jsDispatchQueue: DispatchQueueMock(), + jsRefreshRateMonitor: JSRefreshRateMonitor(), + RUMMonitorProvider: { rumMonitorMock }, + RUMMonitorInternalProvider: { nil } + ) + + bridge.initialize( + configuration: .mockAny(), + resolve: mockResolve, + reject: mockReject + ) + + bridge.addAttributes( + attributes: NSDictionary( + dictionary: [ + "attribute-1": 123, + "attribute-2": "abc", + ] + ), + resolve: mockResolve, + reject: mockReject + ) + + XCTAssertEqual(rumMonitorMock.addedAttributes["attribute-1"] as? Int64, 123) + XCTAssertEqual(rumMonitorMock.addedAttributes["attribute-2"] as? String, "abc") + + XCTAssertEqual(GlobalState.globalAttributes["attribute-1"] as? Int64, 123) + XCTAssertEqual(GlobalState.globalAttributes["attribute-2"] as? String, "abc") + + bridge.removeAttribute(key: "attribute-1", resolve: mockResolve, reject: mockReject) + + XCTAssertEqual(rumMonitorMock.addedAttributes["attribute-1"] as? Int64, nil) + XCTAssertEqual(rumMonitorMock.addedAttributes["attribute-2"] as? String, "abc") + + XCTAssertEqual(GlobalState.globalAttributes["attribute-1"] as? Int64, nil) + XCTAssertEqual(GlobalState.globalAttributes["attribute-2"] as? String, "abc") + + bridge.removeAttribute(key: "attribute-2", resolve: mockResolve, reject: mockReject) + + XCTAssertEqual(rumMonitorMock.addedAttributes["attribute-1"] as? Int64, nil) + XCTAssertEqual(rumMonitorMock.addedAttributes["attribute-2"] as? String, nil) + + XCTAssertEqual(GlobalState.globalAttributes["attribute-1"] as? Int64, nil) + XCTAssertEqual(GlobalState.globalAttributes["attribute-2"] as? String, nil) + + GlobalState.globalAttributes.removeAll() + } + + func testAddingAttributes() { let rumMonitorMock = MockRUMMonitor() let bridge = DdSdkImplementation( mainDispatchQueue: DispatchQueueMock(), @@ -734,7 +786,7 @@ class DdSdkTests: XCTestCase { reject: mockReject ) - bridge.setAttributes( + bridge.addAttributes( attributes: NSDictionary( dictionary: [ "attribute-1": 123, @@ -757,6 +809,66 @@ class DdSdkTests: XCTestCase { GlobalState.globalAttributes.removeAll() } + func testRemovingAttributes() { + let rumMonitorMock = MockRUMMonitor() + let bridge = DdSdkImplementation( + mainDispatchQueue: DispatchQueueMock(), + jsDispatchQueue: DispatchQueueMock(), + jsRefreshRateMonitor: JSRefreshRateMonitor(), + RUMMonitorProvider: { rumMonitorMock }, + RUMMonitorInternalProvider: { nil } + ) + bridge.initialize( + configuration: .mockAny(), + resolve: mockResolve, + reject: mockReject + ) + + bridge.addAttributes( + attributes: NSDictionary( + dictionary: [ + "attribute-1": 123, + "attribute-2": "abc", + "attribute-3": true, + ] + ), + resolve: mockResolve, + reject: mockReject + ) + + XCTAssertEqual(rumMonitorMock.addedAttributes["attribute-1"] as? Int64, 123) + XCTAssertEqual(rumMonitorMock.addedAttributes["attribute-2"] as? String, "abc") + XCTAssertEqual(rumMonitorMock.addedAttributes["attribute-3"] as? Bool, true) + + XCTAssertEqual(GlobalState.globalAttributes["attribute-1"] as? Int64, 123) + XCTAssertEqual(GlobalState.globalAttributes["attribute-2"] as? String, "abc") + XCTAssertEqual(GlobalState.globalAttributes["attribute-3"] as? Bool, true) + + bridge.removeAttributes( + keys: ["attribute-1", "attribute-2"], resolve: mockResolve, reject: mockReject) + + XCTAssertEqual(rumMonitorMock.addedAttributes["attribute-1"] as? Int64, nil) + XCTAssertEqual(rumMonitorMock.addedAttributes["attribute-2"] as? String, nil) + XCTAssertEqual(rumMonitorMock.addedAttributes["attribute-3"] as? Bool, true) + + XCTAssertEqual(GlobalState.globalAttributes["attribute-1"] as? Int64, nil) + XCTAssertEqual(GlobalState.globalAttributes["attribute-2"] as? String, nil) + XCTAssertEqual(GlobalState.globalAttributes["attribute-3"] as? Bool, true) + + bridge.removeAttributes(keys: ["attribute-3"], resolve: mockResolve, reject: mockReject) + + XCTAssertEqual(rumMonitorMock.addedAttributes["attribute-1"] as? Int64, nil) + XCTAssertEqual(rumMonitorMock.addedAttributes["attribute-2"] as? String, nil) + XCTAssertEqual(rumMonitorMock.addedAttributes["attribute-3"] as? Bool, nil) + + XCTAssertEqual(GlobalState.globalAttributes["attribute-1"] as? Int64, nil) + XCTAssertEqual(GlobalState.globalAttributes["attribute-2"] as? String, nil) + XCTAssertEqual(GlobalState.globalAttributes["attribute-3"] as? Bool, nil) + + GlobalState.globalAttributes.removeAll() + + } + func testBuildLongTaskThreshold() { let configuration: DdSdkConfiguration = .mockAny(nativeLongTaskThresholdMs: 2500) diff --git a/packages/core/ios/Tests/MockRUMMonitor.swift b/packages/core/ios/Tests/MockRUMMonitor.swift index f0fa03364..3a882ed47 100644 --- a/packages/core/ios/Tests/MockRUMMonitor.swift +++ b/packages/core/ios/Tests/MockRUMMonitor.swift @@ -38,14 +38,20 @@ internal class MockRUMMonitor: RUMMonitorProtocol { addedAttributes[key] = value } - func removeAttribute(forKey key: DatadogInternal.AttributeKey) {} + func removeAttribute(forKey key: DatadogInternal.AttributeKey) { + addedAttributes.removeValue(forKey: key) + } func addAttributes(_ attributes: [DatadogInternal.AttributeKey : any DatadogInternal.AttributeValue]) { - // Not implemented + for (key, value) in attributes { + addAttribute(forKey: key, value: value) + } } func removeAttributes(forKeys keys: [DatadogInternal.AttributeKey]) { - // Not implemented + for key in keys { + removeAttribute(forKey: key) + } } var debug: Bool diff --git a/packages/core/jest/mock.js b/packages/core/jest/mock.js index 8e154c4cd..c49d13f48 100644 --- a/packages/core/jest/mock.js +++ b/packages/core/jest/mock.js @@ -36,7 +36,16 @@ module.exports = { clearUserInfo: jest .fn() .mockImplementation(() => new Promise(resolve => resolve())), - setAttributes: jest + addAttribute: jest + .fn() + .mockImplementation(() => new Promise(resolve => resolve())), + removeAttribute: jest + .fn() + .mockImplementation(() => new Promise(resolve => resolve())), + addAttributes: jest + .fn() + .mockImplementation(() => new Promise(resolve => resolve())), + removeAttributes: jest .fn() .mockImplementation(() => new Promise(resolve => resolve())), setTrackingConsent: jest diff --git a/packages/core/src/DdSdkReactNative.tsx b/packages/core/src/DdSdkReactNative.tsx index 1838542df..8360a695b 100644 --- a/packages/core/src/DdSdkReactNative.tsx +++ b/packages/core/src/DdSdkReactNative.tsx @@ -175,20 +175,61 @@ export class DdSdkReactNative { ); }; + /** + * Adds a specific attribute to the global context attached with all future Logs, Spans and RUM. + * @param key: Key that identifies the attribute. + * @param value: Value linked to the attribute. + */ + static addAttribute = async ( + key: string, + value: unknown + ): Promise => { + InternalLog.log( + `Adding attribute ${JSON.stringify(value)} for key ${key}`, + SdkVerbosity.DEBUG + ); + await DdSdk.addAttribute(key, { value }); + AttributesSingleton.getInstance().addAttribute(key, value); + }; + + /** + * Removes an attribute from the context attached with all future Logs, Spans and RUM events. + * @param key: They key associated with the attribute to be removed. + */ + static removeAttribute = async (key: string): Promise => { + InternalLog.log( + `Removing attribute for key ${key}`, + SdkVerbosity.DEBUG + ); + await DdSdk.removeAttribute(key); + AttributesSingleton.getInstance().removeAttribute(key); + }; + /** * Adds a set of attributes to the global context attached with all future Logs, Spans and RUM events. - * To remove an attribute, set it to `undefined` in a call to `setAttributes`. * @param attributes: The global context attributes. * @returns a Promise. */ - // eslint-disable-next-line @typescript-eslint/ban-types - static setAttributes = async (attributes: Attributes): Promise => { + static addAttributes = async (attributes: Attributes): Promise => { + InternalLog.log( + `Adding attributes ${JSON.stringify(attributes)}`, + SdkVerbosity.DEBUG + ); + await DdSdk.addAttributes(attributes); + AttributesSingleton.getInstance().addAttributes(attributes); + }; + + /** + * Removes a set of attributes from the context attached with all future Logs, Spans and RUM events. + * @param keys: They keys associated with the attributes to be removed. + */ + static removeAttributes = async (keys: string[]): Promise => { InternalLog.log( - `Setting attributes ${JSON.stringify(attributes)}`, + `Removing attributes for keys ${JSON.stringify(keys)}`, SdkVerbosity.DEBUG ); - await DdSdk.setAttributes(attributes); - AttributesSingleton.getInstance().setAttributes(attributes); + await DdSdk.removeAttributes(keys); + AttributesSingleton.getInstance().removeAttributes(keys); }; /** diff --git a/packages/core/src/__tests__/DdSdkReactNative.test.tsx b/packages/core/src/__tests__/DdSdkReactNative.test.tsx index 57ff5d0ed..5e6f8c447 100644 --- a/packages/core/src/__tests__/DdSdkReactNative.test.tsx +++ b/packages/core/src/__tests__/DdSdkReactNative.test.tsx @@ -62,7 +62,7 @@ beforeEach(async () => { GlobalState.instance.isInitialized = false; DdSdkReactNative['wasAutoInstrumented'] = false; NativeModules.DdSdk.initialize.mockClear(); - NativeModules.DdSdk.setAttributes.mockClear(); + NativeModules.DdSdk.addAttributes.mockClear(); NativeModules.DdSdk.setTrackingConsent.mockClear(); NativeModules.DdSdk.onRUMSessionStarted.mockClear(); @@ -1045,24 +1045,80 @@ describe('DdSdkReactNative', () => { }); }); - describe('setAttributes', () => { - it('calls SDK method when setAttributes', async () => { + describe('addAttribute', () => { + it('calls SDK method when addAttribute', async () => { + // GIVEN + const key = 'foo'; + const value = 'bar'; + + // WHEN + + await DdSdkReactNative.addAttribute(key, value); + + // THEN + expect(DdSdk.addAttribute).toHaveBeenCalledTimes(1); + expect(DdSdk.addAttribute).toHaveBeenCalledWith(key, { value }); + expect(AttributesSingleton.getInstance().getAttribute(key)).toEqual( + value + ); + }); + }); + + describe('removeAttribute', () => { + it('calls SDK method when removeAttribute', async () => { + // GIVEN + const key = 'foo'; + const value = 'bar'; + await DdSdkReactNative.addAttribute(key, value); + + // WHEN + await DdSdkReactNative.removeAttribute(key); + + // THEN + expect(DdSdk.removeAttribute).toHaveBeenCalledTimes(1); + expect(DdSdk.removeAttribute).toHaveBeenCalledWith(key); + expect(AttributesSingleton.getInstance().getAttribute(key)).toEqual( + undefined + ); + }); + }); + + describe('addAttributes', () => { + it('calls SDK method when addAttributes', async () => { // GIVEN const attributes = { foo: 'bar' }; // WHEN - await DdSdkReactNative.setAttributes(attributes); + await DdSdkReactNative.addAttributes(attributes); // THEN - expect(DdSdk.setAttributes).toHaveBeenCalledTimes(1); - expect(DdSdk.setAttributes).toHaveBeenCalledWith(attributes); + expect(DdSdk.addAttributes).toHaveBeenCalledTimes(1); + expect(DdSdk.addAttributes).toHaveBeenCalledWith(attributes); expect(AttributesSingleton.getInstance().getAttributes()).toEqual({ foo: 'bar' }); }); }); + describe('removeAttributes', () => { + it('calls SDK method when removeAttributes', async () => { + // GIVEN + const attributes = { foo: 'bar', baz: 'quux' }; + await DdSdkReactNative.addAttributes(attributes); + + // WHEN + await DdSdkReactNative.removeAttributes(['foo', 'baz']); + + // THEN + expect(DdSdk.removeAttributes).toHaveBeenCalledTimes(1); + expect(DdSdk.removeAttributes).toHaveBeenCalledWith(['foo', 'baz']); + expect(AttributesSingleton.getInstance().getAttributes()).toEqual( + {} + ); + }); + }); + describe('setUserInfo', () => { it('calls SDK method when setUserInfo, and sets the user in UserProvider', async () => { // GIVEN diff --git a/packages/core/src/sdk/AttributesSingleton/AttributesSingleton.ts b/packages/core/src/sdk/AttributesSingleton/AttributesSingleton.ts index a51bb6c99..ac92c2d32 100644 --- a/packages/core/src/sdk/AttributesSingleton/AttributesSingleton.ts +++ b/packages/core/src/sdk/AttributesSingleton/AttributesSingleton.ts @@ -9,13 +9,37 @@ import type { Attributes } from './types'; class AttributesProvider { private attributes: Attributes = {}; - setAttributes = (attributes: Attributes) => { + addAttribute = (key: string, value: unknown) => { + const newAttributes = { ...this.attributes }; + newAttributes[key] = value; + this.attributes = newAttributes; + }; + + removeAttribute = (key: string) => { + const updatedAttributes = { ...this.attributes }; + delete updatedAttributes[key]; + this.attributes = updatedAttributes; + }; + + addAttributes = (attributes: Attributes) => { this.attributes = { ...this.attributes, ...attributes }; }; + removeAttributes = (keys: string[]) => { + const updated = { ...this.attributes }; + for (const k of keys) { + delete updated[k]; + } + this.attributes = updated; + }; + + getAttribute = (key: string): unknown | undefined => { + return this.attributes[key]; + }; + getAttributes = (): Attributes => { return this.attributes; }; diff --git a/packages/core/src/sdk/AttributesSingleton/__tests__/AttributesSingleton.test.ts b/packages/core/src/sdk/AttributesSingleton/__tests__/AttributesSingleton.test.ts index 23fbe5ad7..90d1133b4 100644 --- a/packages/core/src/sdk/AttributesSingleton/__tests__/AttributesSingleton.test.ts +++ b/packages/core/src/sdk/AttributesSingleton/__tests__/AttributesSingleton.test.ts @@ -7,9 +7,12 @@ import { AttributesSingleton } from '../AttributesSingleton'; describe('AttributesSingleton', () => { - it('adds, returns and resets the user info', () => { - // Adding first attributes - AttributesSingleton.getInstance().setAttributes({ + beforeEach(() => { + AttributesSingleton.reset(); + }); + + it('adds, returns and resets the attributes', () => { + AttributesSingleton.getInstance().addAttributes({ appType: 'student', extraInfo: { loggedIn: true @@ -23,11 +26,8 @@ describe('AttributesSingleton', () => { } }); - // Removing and adding new attributes - AttributesSingleton.getInstance().setAttributes({ - appType: undefined, - newAttribute: false - }); + AttributesSingleton.getInstance().removeAttribute('appType'); + AttributesSingleton.getInstance().addAttribute('newAttribute', false); expect(AttributesSingleton.getInstance().getAttributes()).toEqual({ newAttribute: false, @@ -41,4 +41,48 @@ describe('AttributesSingleton', () => { expect(AttributesSingleton.getInstance().getAttributes()).toEqual({}); }); + + it('addAttribute sets a single key and getAttribute returns it', () => { + AttributesSingleton.getInstance().addAttribute('userId', '123'); + expect(AttributesSingleton.getInstance().getAttribute('userId')).toBe( + '123' + ); + expect(AttributesSingleton.getInstance().getAttributes()).toEqual({ + userId: '123' + }); + }); + + it('removeAttribute removes a single key and leaves others intact', () => { + AttributesSingleton.getInstance().addAttributes({ + a: 1, + b: 2 + }); + + AttributesSingleton.getInstance().removeAttribute('a'); + + expect( + AttributesSingleton.getInstance().getAttribute('a') + ).toBeUndefined(); + expect(AttributesSingleton.getInstance().getAttributes()).toEqual({ + b: 2 + }); + }); + + it('removeAttributes removes multiple keys (missing keys are ignored)', () => { + AttributesSingleton.getInstance().addAttributes({ + keyToKeep: 'yes', + keyToRemove1: true, + keyToRemove2: false + }); + + AttributesSingleton.getInstance().removeAttributes([ + 'keyToRemove1', + 'keyToRemove2', + 'keyToIgnore' + ]); + + expect(AttributesSingleton.getInstance().getAttributes()).toEqual({ + keyToKeep: 'yes' + }); + }); }); diff --git a/packages/core/src/specs/NativeDdSdk.ts b/packages/core/src/specs/NativeDdSdk.ts index a2ce1120e..70401fe3c 100644 --- a/packages/core/src/specs/NativeDdSdk.ts +++ b/packages/core/src/specs/NativeDdSdk.ts @@ -26,10 +26,29 @@ export interface Spec extends TurboModule { initialize(configuration: Object): Promise; /** - * Sets the global context (set of attributes) attached with all future Logs, Spans and RUM events. + * Adds a specific attribute to the global context attached with all future Logs, Spans and RUM. + * @param key: Key that identifies the attribute. + * @param value: Value linked to the attribute. + */ + addAttribute(key: string, value: Object): Promise; + + /** + * Removes an attribute from the context attached with all future Logs, Spans and RUM events. + * @param key: They key associated with the attribute to be removed. + */ + removeAttribute(key: string): Promise; + + /** + * Adds the global context (set of attributes) attached with all future Logs, Spans and RUM events. * @param attributes: The global context attributes. */ - setAttributes(attributes: Object): Promise; + addAttributes(attributes: Object): Promise; + + /** + * Removes a set of attributes from the context attached with all future Logs, Spans and RUM events. + * @param keys: They keys associated with the attributes to be removed. + */ + removeAttributes(keys: string[]): Promise; /** * Set the user information. diff --git a/packages/core/src/types.tsx b/packages/core/src/types.tsx index bad19d429..c8d9821cc 100644 --- a/packages/core/src/types.tsx +++ b/packages/core/src/types.tsx @@ -83,11 +83,30 @@ export type DdSdkType = { */ initialize(configuration: DdSdkConfiguration): Promise; + /** + * Sets a specific attribute in the global context attached with all future Logs, Spans and RUM + * @param key: Key that identifies the attribute. + * @param value: Value linked to the attribute. + */ + addAttribute(key: string, value: object): Promise; + + /** + * Removes an attribute from the context attached with all future Logs, Spans and RUM events. + * @param key: They key associated with the attribute to be removed. + */ + removeAttribute(key: string): Promise; + /** * Sets the global context (set of attributes) attached with all future Logs, Spans and RUM events. * @param attributes: The global context attributes. */ - setAttributes(attributes: object): Promise; + addAttributes(attributes: object): Promise; + + /** + * Removes a set of attributes from the context attached with all future Logs, Spans and RUM events. + * @param keys: They keys associated with the attributes to be removed. + */ + removeAttributes(keys: string[]): Promise; /** * Sets the user information. diff --git a/packages/react-native-apollo-client/__mocks__/react-native.ts b/packages/react-native-apollo-client/__mocks__/react-native.ts index 046ced2f6..bbac607d3 100644 --- a/packages/react-native-apollo-client/__mocks__/react-native.ts +++ b/packages/react-native-apollo-client/__mocks__/react-native.ts @@ -18,9 +18,9 @@ actualRN.NativeModules.DdSdk = { initialize: jest.fn().mockImplementation( () => new Promise(resolve => resolve()) ) as jest.MockedFunction, - setAttributes: jest.fn().mockImplementation( + addAttributes: jest.fn().mockImplementation( () => new Promise(resolve => resolve()) - ) as jest.MockedFunction, + ) as jest.MockedFunction, setTrackingConsent: jest.fn().mockImplementation( () => new Promise(resolve => resolve()) ) as jest.MockedFunction, From 5bb5b9fb54dc220efcf26939667933a36f15fa1e Mon Sep 17 00:00:00 2001 From: Sergio Barrio Date: Thu, 16 Oct 2025 11:01:21 +0200 Subject: [PATCH 044/410] JS refresh rate normalization --- .../reactnative/DdSdkImplementation.kt | 51 ++++++- .../com/datadog/reactnative/DdSdkTest.kt | 127 ++++++++++++++++++ .../ios/Sources/DdSdkImplementation.swift | 21 ++- packages/core/ios/Tests/DdSdkTests.swift | 108 +++++++++++++++ 4 files changed, 305 insertions(+), 2 deletions(-) diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt index ed545d9e1..574394a36 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt @@ -7,7 +7,10 @@ package com.datadog.reactnative import android.content.Context +import android.hardware.display.DisplayManager +import android.os.Build import android.util.Log +import android.view.Display import com.datadog.android.privacy.TrackingConsent import com.datadog.android.rum.RumPerformanceMetric import com.datadog.android.rum.configuration.VitalsUpdateFrequency @@ -19,6 +22,7 @@ import com.facebook.react.bridge.ReadableMap import java.util.Locale import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicBoolean +import kotlin.math.max /** The entry point to initialize Datadog's features. */ @Suppress("TooManyFunctions") @@ -293,9 +297,10 @@ class DdSdkImplementation( return { if (jsRefreshRateMonitoringEnabled && it > 0.0) { + val normalizedFrameTimeSeconds = normalizeFrameTime(it, appContext) datadog.getRumMonitor() ._getInternal() - ?.updatePerformanceMetric(RumPerformanceMetric.JS_FRAME_TIME, it) + ?.updatePerformanceMetric(RumPerformanceMetric.JS_FRAME_TIME, normalizedFrameTimeSeconds) } if (jsLongTasksMonitoringEnabled && it > @@ -308,6 +313,49 @@ class DdSdkImplementation( } } + /** + * Normalizes frameTime values so when are turned into FPS metrics they are normalized on a range of zero to 60fps. + * @param frameTimeSeconds: the frame time to normalize. In seconds. + * @param context: The current app context + * @param fpsBudget: The maximum fps under which the frame Time will be normalized [0-fpsBudget]. Defaults to 60Hz. + * @param deviceDisplayFps: The maximum fps supported by the device. If not provided it will be set from the value obtained from the app context. + */ + @Suppress("CyclomaticComplexMethod") + fun normalizeFrameTime( + frameTimeSeconds: Double, + context: Context, + fpsBudget: Double? = null, + deviceDisplayFps: Double? = null, + ) : Double { + val frameTimeMs = frameTimeSeconds * 1000.0 + val frameBudgetHz = fpsBudget ?: DEFAULT_REFRESH_HZ + val maxDeviceDisplayHz = deviceDisplayFps ?: getMaxDisplayRefreshRate(context) + ?: 60.0 + + val maxDeviceFrameTimeMs = 1000.0 / maxDeviceDisplayHz + val budgetFrameTimeMs = 1000.0 / frameBudgetHz + + if (listOf( + maxDeviceDisplayHz, frameTimeMs, frameBudgetHz, budgetFrameTimeMs, maxDeviceFrameTimeMs + ).any { !it.isFinite() || it <= 0.0 } + ) return 1.0 / DEFAULT_REFRESH_HZ + + + var normalizedFrameTimeMs = frameTimeMs / (maxDeviceFrameTimeMs / budgetFrameTimeMs) + + normalizedFrameTimeMs = max(normalizedFrameTimeMs, maxDeviceFrameTimeMs) + + return normalizedFrameTimeMs / 1000.0 // in seconds + } + + @Suppress("CyclomaticComplexMethod") + private fun getMaxDisplayRefreshRate(context: Context?): Double { + val dm = context?.getSystemService(Context.DISPLAY_SERVICE) as? DisplayManager ?: return 60.0 + val display: Display = dm.getDisplay(Display.DEFAULT_DISPLAY) ?: return DEFAULT_REFRESH_HZ + + return display.supportedModes.maxOf { it.refreshRate.toDouble() } + } + // endregion internal companion object { internal const val DEFAULT_APP_VERSION = "?" @@ -317,6 +365,7 @@ class DdSdkImplementation( internal const val DD_DROP_ACTION = "_dd.action.drop_action" internal const val MONITOR_JS_ERROR_MESSAGE = "Error monitoring JS refresh rate" internal const val PACKAGE_INFO_NOT_FOUND_ERROR_MESSAGE = "Error getting package info" + internal const val DEFAULT_REFRESH_HZ = 60.0 internal const val NAME = "DdSdk" } } diff --git a/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt b/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt index ae6ded89c..327d8ffc0 100644 --- a/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt +++ b/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt @@ -60,6 +60,7 @@ import java.util.Locale import java.util.stream.Stream import kotlin.time.Duration.Companion.seconds import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.data.Offset import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -3238,6 +3239,132 @@ internal class DdSdkTest { } } + @Test + fun `𝕄 normalize frameTime according to the device's refresh rate`() { + // 10 fps, 60Hz device, 60 fps budget -> 10 fps + var frameTimeSeconds = testedBridgeSdk.normalizeFrameTime( + frameTimeSeconds = 0.1, + context = mockContext, + fpsBudget = 60.0, + deviceDisplayFps = 60.0 + ) + assertThat(frameTimeSeconds).isEqualTo(0.1) + + // 30 fps, 60Hz device, 60 fps budget -> 30 fps + frameTimeSeconds = testedBridgeSdk.normalizeFrameTime( + frameTimeSeconds = 0.03, + context = mockContext, + fpsBudget = 60.0, + deviceDisplayFps = 60.0 + ) + assertThat(frameTimeSeconds).isEqualTo(0.03) + + // 60 fps, 60Hz device, 60 fps budget -> 60 fps + frameTimeSeconds = testedBridgeSdk.normalizeFrameTime( + frameTimeSeconds = 0.016, + context = mockContext, + fpsBudget = 60.0, + deviceDisplayFps = 60.0 + ) + assertThat(frameTimeSeconds).isEqualTo(0.016, Offset.offset(0.005)) + + // 60 fps, 120Hz device, 60 fps budget -> 30 fps + frameTimeSeconds = testedBridgeSdk.normalizeFrameTime( + frameTimeSeconds = 0.016, + context = mockContext, + fpsBudget = 60.0, + deviceDisplayFps = 120.0 + ) + assertThat(frameTimeSeconds).isEqualTo(0.032) + + // 120 fps, 120Hz device, 60 fps budget -> 60 fps + frameTimeSeconds = testedBridgeSdk.normalizeFrameTime( + frameTimeSeconds = 0.0083, + context = mockContext, + fpsBudget = 60.0, + deviceDisplayFps = 120.0 + ) + assertThat(frameTimeSeconds).isEqualTo(0.016, Offset.offset(0.005)) + + // 90 fps, 120Hz device, 60 fps budget -> 45 fps + frameTimeSeconds = testedBridgeSdk.normalizeFrameTime( + frameTimeSeconds = 0.0111, + context = mockContext, + fpsBudget = 60.0, + deviceDisplayFps = 120.0 + ) + assertThat(frameTimeSeconds).isEqualTo(0.0222, Offset.offset(0.001)) + + // 100 fps, 120Hz device, 60 fps budget -> 50 fps + frameTimeSeconds = testedBridgeSdk.normalizeFrameTime( + frameTimeSeconds = 0.01, + context = mockContext, + fpsBudget = 60.0, + deviceDisplayFps = 120.0 + ) + assertThat(frameTimeSeconds).isEqualTo(0.02, Offset.offset(0.001)) + + // 120 fps, 120Hz device, 120 fps budget -> 120 fps + frameTimeSeconds = testedBridgeSdk.normalizeFrameTime( + frameTimeSeconds = 0.0083, + context = mockContext, + fpsBudget = 120.0, + deviceDisplayFps = 120.0 + ) + assertThat(frameTimeSeconds).isEqualTo(0.0083, Offset.offset(0.001)) + + // 80 fps, 160Hz device, 60 fps budget -> 30 fps + frameTimeSeconds = testedBridgeSdk.normalizeFrameTime( + frameTimeSeconds = 0.0125, + context = mockContext, + fpsBudget = 60.0, + deviceDisplayFps = 160.0 + ) + assertThat(frameTimeSeconds).isEqualTo(0.033, Offset.offset(0.001)) + + // 160 fps, 160Hz device, 60 fps budget -> 60 fps + frameTimeSeconds = testedBridgeSdk.normalizeFrameTime( + frameTimeSeconds = 0.00625, + context = mockContext, + fpsBudget = 60.0, + deviceDisplayFps = 160.0 + ) + assertThat(frameTimeSeconds).isEqualTo(0.016, Offset.offset(0.001)) + + // Edge cases + frameTimeSeconds = testedBridgeSdk.normalizeFrameTime( + frameTimeSeconds = 0.0, + context = mockContext, + fpsBudget = 0.0, + deviceDisplayFps = 0.0 + ) + assertThat(frameTimeSeconds).isEqualTo(0.016, Offset.offset(0.001)) + + frameTimeSeconds = testedBridgeSdk.normalizeFrameTime( + frameTimeSeconds = 0.016, + context = mockContext, + fpsBudget = 0.0, + deviceDisplayFps = 0.0 + ) + assertThat(frameTimeSeconds).isEqualTo(0.016, Offset.offset(0.001)) + + frameTimeSeconds = testedBridgeSdk.normalizeFrameTime( + frameTimeSeconds = 0.016, + context = mockContext, + fpsBudget = 60.0, + deviceDisplayFps = 0.0 + ) + assertThat(frameTimeSeconds).isEqualTo(0.016, Offset.offset(0.001)) + + frameTimeSeconds = testedBridgeSdk.normalizeFrameTime( + frameTimeSeconds = 0.016, + context = mockContext, + fpsBudget = 0.0, + deviceDisplayFps = 60.0 + ) + assertThat(frameTimeSeconds).isEqualTo(0.016, Offset.offset(0.001)) + } + // endregion // region Internal diff --git a/packages/core/ios/Sources/DdSdkImplementation.swift b/packages/core/ios/Sources/DdSdkImplementation.swift index 16adb50f2..6e24af4e3 100644 --- a/packages/core/ios/Sources/DdSdkImplementation.swift +++ b/packages/core/ios/Sources/DdSdkImplementation.swift @@ -232,7 +232,8 @@ public class DdSdkImplementation: NSObject { // Leave JS thread ASAP to give as much time to JS engine work. sharedQueue.async { if (shouldRecordFrameTime) { - rumMonitorInternal.updatePerformanceMetric(at: now, metric: .jsFrameTimeSeconds, value: frameTime, attributes: [:]) + let normalizedFrameTimeSeconds = DdSdkImplementation.normalizeFrameTimeForDeviceRefreshRate(frameTime) + rumMonitorInternal.updatePerformanceMetric(at: now, metric: .jsFrameTimeSeconds, value: normalizedFrameTimeSeconds, attributes: [:]) } if (shouldRecordLongTask) { rumMonitorInternal.addLongTask(at: now, duration: frameTime, attributes: ["long_task.target": "javascript"]) @@ -242,5 +243,23 @@ public class DdSdkImplementation: NSObject { return frameTimeCallback } + + // Normalizes frameTime values so when they are turned into FPS metrics they are normalized on a range between 0 and fpsBudget. If fpsBudget is not provided it will default to 60hz. + public static func normalizeFrameTimeForDeviceRefreshRate(_ frameTime: Double, fpsBudget: Double? = nil, deviceDisplayFps: Double? = nil) -> Double { + let DEFAULT_REFRESH_HZ = 60.0 + let frameTimeMs: Double = frameTime * 1000.0 + let frameBudgetHz: Double = fpsBudget ?? DEFAULT_REFRESH_HZ + let maxDeviceDisplayHz = deviceDisplayFps ?? Double(UIScreen.main.maximumFramesPerSecond) + let maxDeviceFrameTimeMs = 1000.0 / maxDeviceDisplayHz + let budgetFrameTimeMs = 1000.0 / frameBudgetHz + + guard maxDeviceDisplayHz > 0, frameTimeMs.isFinite, frameTimeMs > 0, frameBudgetHz > 0, budgetFrameTimeMs.isFinite, budgetFrameTimeMs > 0, maxDeviceFrameTimeMs.isFinite, maxDeviceFrameTimeMs > 0 else { + return 1.0 / DEFAULT_REFRESH_HZ + } + + var normalizedFrameTimeMs = frameTimeMs / (maxDeviceFrameTimeMs / budgetFrameTimeMs) + normalizedFrameTimeMs = max(normalizedFrameTimeMs, maxDeviceFrameTimeMs) + return normalizedFrameTimeMs / 1000.0 // in seconds + } } diff --git a/packages/core/ios/Tests/DdSdkTests.swift b/packages/core/ios/Tests/DdSdkTests.swift index adbb57da9..4a5d13f2e 100644 --- a/packages/core/ios/Tests/DdSdkTests.swift +++ b/packages/core/ios/Tests/DdSdkTests.swift @@ -1181,6 +1181,114 @@ class DdSdkTests: XCTestCase { XCTAssertEqual(rumMonitorMock.receivedLongTasks.first?.value, 0.25) XCTAssertEqual(rumMonitorMock.lastReceivedPerformanceMetrics[.jsFrameTimeSeconds], 0.25) } + + func testFrameTimeNormalizationFromCallback() { + let mockRefreshRateMonitor = MockJSRefreshRateMonitor() + let rumMonitorMock = MockRUMMonitor() + + DdSdkImplementation( + mainDispatchQueue: DispatchQueueMock(), + jsDispatchQueue: DispatchQueueMock(), + jsRefreshRateMonitor: mockRefreshRateMonitor, + RUMMonitorProvider: { rumMonitorMock }, + RUMMonitorInternalProvider: { rumMonitorMock._internalMock } + ).initialize( + configuration: .mockAny( + longTaskThresholdMs: 200, + vitalsUpdateFrequency: "average" + ), + resolve: mockResolve, + reject: mockReject + ) + + XCTAssertTrue(mockRefreshRateMonitor.isStarted) + + // 10 fps + mockRefreshRateMonitor.executeFrameCallback(frameTime: 0.1) + sharedQueue.sync {} + XCTAssertEqual(rumMonitorMock.lastReceivedPerformanceMetrics[.jsFrameTimeSeconds], 0.1) + + // 30 fps + mockRefreshRateMonitor.executeFrameCallback(frameTime: 0.03) + sharedQueue.sync {} + XCTAssertEqual(rumMonitorMock.lastReceivedPerformanceMetrics[.jsFrameTimeSeconds], 0.03) + + // 45 fps + mockRefreshRateMonitor.executeFrameCallback(frameTime: 0.02) + sharedQueue.sync {} + XCTAssertEqual(rumMonitorMock.lastReceivedPerformanceMetrics[.jsFrameTimeSeconds], 0.02) + + // 60 fps + mockRefreshRateMonitor.executeFrameCallback(frameTime: 0.016) + sharedQueue.sync {} + XCTAssertEqual(rumMonitorMock.lastReceivedPerformanceMetrics[.jsFrameTimeSeconds]!, 0.016, accuracy: 0.001) + + // 90 fps + mockRefreshRateMonitor.executeFrameCallback(frameTime: 0.011) + sharedQueue.sync {} + XCTAssertEqual(rumMonitorMock.lastReceivedPerformanceMetrics[.jsFrameTimeSeconds]!, 0.016, accuracy: 0.001) + + // 120 fps + mockRefreshRateMonitor.executeFrameCallback(frameTime: 0.008) + sharedQueue.sync {} + XCTAssertEqual(rumMonitorMock.lastReceivedPerformanceMetrics[.jsFrameTimeSeconds]!, 0.016, accuracy: 0.001) + } + + func testFrameTimeNormalizationUtilityFunction() { + + // 10 fps, 60fps capable device, 60 fps budget -> Normalized to 10fps + var frameTimeSeconds = DdSdkImplementation.normalizeFrameTimeForDeviceRefreshRate(0.1, fpsBudget: 60.0, deviceDisplayFps: 60.0) + XCTAssertEqual(frameTimeSeconds, 0.1, accuracy: 0.01) + + // 30 fps, 60fps capable device, 60 fps budget -> Normalized to 30fps + frameTimeSeconds = DdSdkImplementation.normalizeFrameTimeForDeviceRefreshRate(0.03, fpsBudget: 60.0, deviceDisplayFps: 60.0) + XCTAssertEqual(frameTimeSeconds, 0.03, accuracy: 0.01) + + // 60 fps, 60fps capable device, 60 fps budget-> Normalized to 60fps + frameTimeSeconds = DdSdkImplementation.normalizeFrameTimeForDeviceRefreshRate(0.016, fpsBudget: 60.0, deviceDisplayFps: 60.0) + XCTAssertEqual(frameTimeSeconds, 0.016, accuracy: 0.01) + + // 60 fps, 120fps capable device, 60 fps budget -> Normalized to 30fps + frameTimeSeconds = DdSdkImplementation.normalizeFrameTimeForDeviceRefreshRate(0.016, fpsBudget: 60.0, deviceDisplayFps: 120.0) + XCTAssertEqual(frameTimeSeconds, 0.03, accuracy: 0.01) + + // 120 fps, 120fps capable device, 60 fps budget -> Normalized to 60fps + frameTimeSeconds = DdSdkImplementation.normalizeFrameTimeForDeviceRefreshRate(0.0083, fpsBudget: 60.0, deviceDisplayFps: 120.0) + XCTAssertEqual(frameTimeSeconds, 0.016, accuracy: 0.001) + + // 90 fps, 120fps capable device, 60 fps budget -> Normalized to 45fps + frameTimeSeconds = DdSdkImplementation.normalizeFrameTimeForDeviceRefreshRate(0.0111, fpsBudget: 60.0, deviceDisplayFps: 120.0) + XCTAssertEqual(frameTimeSeconds, 0.0222, accuracy: 0.001) + + // 100 fps, 120fps capable device, 60 fps budget -> Normalized to 50fps + frameTimeSeconds = DdSdkImplementation.normalizeFrameTimeForDeviceRefreshRate(0.01, fpsBudget: 60.0, deviceDisplayFps: 120.0) + XCTAssertEqual(frameTimeSeconds, 0.02, accuracy: 0.001) + + // 120 fps, 120fps capable device, 120 fps budget -> Normalized to 120fps + frameTimeSeconds = DdSdkImplementation.normalizeFrameTimeForDeviceRefreshRate(0.0083, fpsBudget: 120.0, deviceDisplayFps: 120.0) + XCTAssertEqual(frameTimeSeconds, 0.0083, accuracy: 0.001) + + // 80 fps, 160fps capable device, 60 fps budget -> Normalized to 30fps + frameTimeSeconds = DdSdkImplementation.normalizeFrameTimeForDeviceRefreshRate(0.0125, fpsBudget: 60.0, deviceDisplayFps: 160.0) + XCTAssertEqual(frameTimeSeconds, 0.033, accuracy: 0.001) + + // 160 fps, 160fps capable device, 60 fps budget -> Normalized to 60fps + frameTimeSeconds = DdSdkImplementation.normalizeFrameTimeForDeviceRefreshRate(0.00625, fpsBudget: 60.0, deviceDisplayFps: 160.0) + XCTAssertEqual(frameTimeSeconds, 0.016, accuracy: 0.001) + + // Edge cases + frameTimeSeconds = DdSdkImplementation.normalizeFrameTimeForDeviceRefreshRate(0, fpsBudget: 0, deviceDisplayFps: 0) + XCTAssertEqual(frameTimeSeconds, 0.016, accuracy: 0.001) + + frameTimeSeconds = DdSdkImplementation.normalizeFrameTimeForDeviceRefreshRate(0.016, fpsBudget: 0, deviceDisplayFps: 0) + XCTAssertEqual(frameTimeSeconds, 0.016, accuracy: 0.001) + + frameTimeSeconds = DdSdkImplementation.normalizeFrameTimeForDeviceRefreshRate(0.016, fpsBudget: 60.0, deviceDisplayFps: 0) + XCTAssertEqual(frameTimeSeconds, 0.016, accuracy: 0.001) + + frameTimeSeconds = DdSdkImplementation.normalizeFrameTimeForDeviceRefreshRate(0.016, fpsBudget: 0, deviceDisplayFps: 60.0) + XCTAssertEqual(frameTimeSeconds, 0.016, accuracy: 0.001) + } func testSDKInitializationWithCustomEndpoints() throws { let mockRefreshRateMonitor = MockJSRefreshRateMonitor() From 1f781a5115d18454088dc32f8be9f95475306a17 Mon Sep 17 00:00:00 2001 From: Sergio Barrio Date: Fri, 10 Oct 2025 14:52:21 +0200 Subject: [PATCH 045/410] Expose view Attributes API --- packages/core/__mocks__/react-native.ts | 12 ++ .../reactnative/DdRumImplementation.kt | 44 +++++++ .../reactnative/DdSdkImplementation.kt | 2 - .../kotlin/com/datadog/reactnative/DdRum.kt | 38 ++++++ .../kotlin/com/datadog/reactnative/DdRum.kt | 38 ++++++ .../com/datadog/reactnative/DdRumTest.kt | 58 +++++++++ .../com/datadog/tools/unit/MockRumMonitor.kt | 17 +-- packages/core/ios/Sources/DdRum.mm | 47 ++++++- .../ios/Sources/DdRumImplementation.swift | 28 ++++ .../ios/Sources/DdSdkImplementation.swift | 120 +++++++++++------- packages/core/ios/Tests/DdRumTests.swift | 59 +++++++++ packages/core/ios/Tests/MockRUMMonitor.swift | 38 +++--- packages/core/jest/mock.js | 12 ++ packages/core/src/rum/DdRum.ts | 45 +++++++ packages/core/src/rum/__tests__/DdRum.test.ts | 92 ++++++++++++++ packages/core/src/rum/types.ts | 26 ++++ packages/core/src/specs/NativeDdRum.ts | 25 ++++ 17 files changed, 631 insertions(+), 70 deletions(-) diff --git a/packages/core/__mocks__/react-native.ts b/packages/core/__mocks__/react-native.ts index 24e3f80c7..73308f711 100644 --- a/packages/core/__mocks__/react-native.ts +++ b/packages/core/__mocks__/react-native.ts @@ -132,6 +132,18 @@ actualRN.NativeModules.DdRum = { addTiming: jest.fn().mockImplementation( () => new Promise(resolve => resolve()) ) as jest.MockedFunction, + addViewAttribute: jest.fn().mockImplementation( + () => new Promise(resolve => resolve()) + ) as jest.MockedFunction, + removeViewAttribute: jest.fn().mockImplementation( + () => new Promise(resolve => resolve()) + ) as jest.MockedFunction, + addViewAttributes: jest.fn().mockImplementation( + () => new Promise(resolve => resolve()) + ) as jest.MockedFunction, + removeViewAttributes: jest.fn().mockImplementation( + () => new Promise(resolve => resolve()) + ) as jest.MockedFunction, addViewLoadingTime: jest.fn().mockImplementation( () => new Promise(resolve => resolve()) ) as jest.MockedFunction, diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdRumImplementation.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdRumImplementation.kt index 013872c08..4e3cd416f 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdRumImplementation.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdRumImplementation.kt @@ -13,6 +13,7 @@ import com.datadog.android.rum.RumErrorSource import com.datadog.android.rum.RumResourceKind import com.datadog.android.rum.RumResourceMethod import com.facebook.react.bridge.Promise +import com.facebook.react.bridge.ReadableArray import com.facebook.react.bridge.ReadableMap import java.util.Locale @@ -248,6 +249,49 @@ class DdRumImplementation(private val datadog: DatadogWrapper = DatadogSDKWrappe promise.resolve(null) } + /** + * Adds a custom attribute to the active RUM View. It will be propagated to all future RUM events associated with the active View. + * @param key: key for this view attribute. + * @param value: value for this attribute. + */ + fun addViewAttribute(key: String, value: ReadableMap, promise: Promise) { + val attributeValue = value.toMap()["value"] + val attributes = mutableMapOf() + attributes[key] = attributeValue + datadog.getRumMonitor().addViewAttributes(attributes) + promise.resolve(null) + } + + /** + * Removes an attribute from the active RUM View. + * @param key: key for the attribute to be removed from the view. + */ + fun removeViewAttribute(key: String, promise: Promise) { + val keysToDelete: Collection = listOf(key) + datadog.getRumMonitor().removeViewAttributes(keysToDelete) + promise.resolve(null) + } + + /** + * Adds multiple attributes to the active RUM View. They will be propagated to all future RUM events associated with the active View. + * @param attributes: key/value object containing all attributes to be added to the view. + */ + fun addViewAttributes(attributes: ReadableMap, promise: Promise) { + datadog.getRumMonitor().addViewAttributes(attributes.toMap()) + promise.resolve(null) + } + + /** + * Removes multiple attributes from the active RUM View. + * @param keys: keys for the attributes to be removed from the view. + */ + fun removeViewAttributes(keys: ReadableArray, promise: Promise) { + val keysToDelete = (0 until keys.size()) + .mapNotNull { keys.getString(it) } + datadog.getRumMonitor().removeViewAttributes(keysToDelete) + promise.resolve(null) + } + /** * Adds the loading time of the view to the active view. * It is calculated as the difference between the current time and the start time of the view. diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt index 574394a36..6741e971e 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt @@ -143,8 +143,6 @@ class DdSdkImplementation( if (id != null) { datadog.setUserInfo(id, name, email, extraInfo) - } else { - // TO DO - Log warning? } promise.resolve(null) diff --git a/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdRum.kt b/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdRum.kt index ce8104685..6cb2b385b 100644 --- a/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdRum.kt +++ b/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdRum.kt @@ -9,6 +9,7 @@ package com.datadog.reactnative import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.ReactMethod +import com.facebook.react.bridge.ReadableArray import com.facebook.react.bridge.ReadableMap /** @@ -201,6 +202,43 @@ class DdRum( implementation.addTiming(name, promise) } + /** + * Adds a custom attribute to the active RUM View. It will be propagated to all future RUM events associated with the active View. + * @param key: key for this view attribute. + * @param value: value for this attribute. + */ + @ReactMethod + override fun addViewAttribute(key: String, value: ReadableMap, promise: Promise) { + implementation.addViewAttribute(key, value, promise) + } + + /** + * Removes an attribute from the active RUM View. + * @param key: key for the attribute to be removed from the view. + */ + @ReactMethod + override fun removeViewAttribute(key: String, promise: Promise) { + implementation.removeViewAttribute(key, promise) + } + + /** + * Adds multiple attributes to the active RUM View. They will be propagated to all future RUM events associated with the active View. + * @param attributes: key/value object containing all attributes to be added to the view. + */ + @ReactMethod + override fun addViewAttributes(attributes: ReadableMap, promise: Promise) { + implementation.addViewAttributes(attributes, promise) + } + + /** + * Removes multiple attributes from the active RUM View. + * @param keys: keys for the attributes to be removed from the view. + */ + @ReactMethod + override fun removeViewAttributes(keys: ReadableArray, promise: Promise) { + implementation.removeViewAttributes(keys, promise) + } + /** * Adds the loading time of the view to the active view. * It is calculated as the difference between the current time and the start time of the view. diff --git a/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdRum.kt b/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdRum.kt index 79742e854..a6c4965ea 100644 --- a/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdRum.kt +++ b/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdRum.kt @@ -10,6 +10,7 @@ import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.ReactContextBaseJavaModule import com.facebook.react.bridge.ReactMethod +import com.facebook.react.bridge.ReadableArray import com.facebook.react.bridge.ReadableMap /** @@ -192,6 +193,43 @@ class DdRum( implementation.addTiming(name, promise) } + /** + * Adds a custom attribute to the active RUM View. It will be propagated to all future RUM events associated with the active View. + * @param key: key for this view attribute. + * @param value: value for this attribute. + */ + @ReactMethod + fun addViewAttribute(key: String, value: ReadableMap, promise: Promise) { + implementation.addViewAttribute(key, value, promise) + } + + /** + * Removes an attribute from the active RUM View. + * @param key: key for the attribute to be removed from the view. + */ + @ReactMethod + fun removeViewAttribute(key: String, promise: Promise) { + implementation.removeViewAttribute(key, promise) + } + + /** + * Adds multiple attributes to the active RUM View. They will be propagated to all future RUM events associated with the active View. + * @param attributes: key/value object containing all attributes to be added to the view. + */ + @ReactMethod + fun addViewAttributes(attributes: ReadableMap, promise: Promise) { + implementation.addViewAttributes(attributes, promise) + } + + /** + * Removes multiple attributes from the active RUM View. + * @param keys: keys for the attributes to be removed from the view. + */ + @ReactMethod + fun removeViewAttributes(keys: ReadableArray, promise: Promise) { + implementation.removeViewAttributes(keys, promise) + } + /** * Adds the loading time of the view to the active view. * It is calculated as the difference between the current time and the start time of the view. diff --git a/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdRumTest.kt b/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdRumTest.kt index be1c57b3a..9619794df 100644 --- a/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdRumTest.kt +++ b/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdRumTest.kt @@ -13,13 +13,16 @@ import com.datadog.android.rum.RumMonitor import com.datadog.android.rum.RumResourceKind import com.datadog.android.rum.RumResourceMethod import com.datadog.tools.unit.forge.BaseConfigurator +import com.datadog.tools.unit.toReadableArray import com.datadog.tools.unit.toReadableMap import com.facebook.react.bridge.Promise import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.annotation.AdvancedForgery import fr.xgouchet.elmyr.annotation.BoolForgery import fr.xgouchet.elmyr.annotation.DoubleForgery import fr.xgouchet.elmyr.annotation.Forgery import fr.xgouchet.elmyr.annotation.IntForgery +import fr.xgouchet.elmyr.annotation.MapForgery import fr.xgouchet.elmyr.annotation.StringForgery import fr.xgouchet.elmyr.annotation.StringForgeryType import fr.xgouchet.elmyr.junit5.ForgeConfiguration @@ -456,6 +459,61 @@ internal class DdRumTest { verify(mockRumMonitor).addTiming(timing) } + @Test + fun `M call addViewAttribute W addViewAttribute()`( + @StringForgery key: String, + @StringForgery value: String + ) { + var attributeMap = mutableMapOf() + attributeMap.put("value", value) + + var attributes = mutableMapOf() + attributes.put(key, value) + + // When + testedDdRum.addViewAttribute(key, attributeMap.toReadableMap(), mockPromise) + + // Then + verify(mockRumMonitor).addViewAttributes(attributes) + } + + @Test + fun `M call removeViewAttribute W removeViewAttribute()`(@StringForgery key: String) { + // When + testedDdRum.removeViewAttribute(key, mockPromise) + + // Then + verify(mockRumMonitor).removeViewAttributes(listOf(key)) + } + + @Test + fun `M call addViewAttributes W addViewAttributes()`( + @MapForgery( + key = AdvancedForgery(string = [StringForgery(StringForgeryType.NUMERICAL)]), + value = AdvancedForgery(string = [StringForgery(StringForgeryType.ASCII)]) + ) customAttributes: Map + ) { + // When + testedDdRum.addViewAttributes(customAttributes.toReadableMap(), mockPromise) + + // Then + verify(mockRumMonitor).addViewAttributes(customAttributes) + } + + @Test + fun `𝕄 call removeViewAttributes 𝕎 removeViewAttributes`( + @MapForgery( + key = AdvancedForgery(string = [StringForgery(StringForgeryType.NUMERICAL)]), + value = AdvancedForgery(string = [StringForgery(StringForgeryType.ASCII)]) + ) customAttributes: Map + ) { + // When + testedDdRum.removeViewAttributes(customAttributes.keys.toReadableArray(), mockPromise) + + // Then + verify(mockRumMonitor).removeViewAttributes(customAttributes.keys.toList()) + } + @Test fun `M call addViewLoadingTime w addViewLoadingTime()`(@BoolForgery overwrite: Boolean) { // When diff --git a/packages/core/android/src/test/kotlin/com/datadog/tools/unit/MockRumMonitor.kt b/packages/core/android/src/test/kotlin/com/datadog/tools/unit/MockRumMonitor.kt index 702cc2533..13f73d94a 100644 --- a/packages/core/android/src/test/kotlin/com/datadog/tools/unit/MockRumMonitor.kt +++ b/packages/core/android/src/test/kotlin/com/datadog/tools/unit/MockRumMonitor.kt @@ -30,7 +30,13 @@ class MockRumMonitor : RumMonitor { override fun addAttribute(key: String, value: Any?) {} - override fun addViewAttributes(attributes: Map) {} + override fun removeAttribute(key: String) {} + + override fun clearAttributes() {} + + override fun getAttributes(): Map { + return mapOf() + } override fun addError( message: String, @@ -55,15 +61,10 @@ class MockRumMonitor : RumMonitor { @ExperimentalRumApi override fun addViewLoadingTime(overwrite: Boolean) {} - override fun clearAttributes() {} - - override fun getAttributes(): Map { - return mapOf() - } - override fun getCurrentSessionId(callback: (String?) -> Unit) {} - override fun removeAttribute(key: String) {} + override fun addViewAttributes(attributes: Map) {} + override fun removeViewAttributes(attributes: Collection) {} override fun startAction( diff --git a/packages/core/ios/Sources/DdRum.mm b/packages/core/ios/Sources/DdRum.mm index 5d831942a..f5c324ce8 100644 --- a/packages/core/ios/Sources/DdRum.mm +++ b/packages/core/ios/Sources/DdRum.mm @@ -107,6 +107,35 @@ @implementation DdRum [self addTiming:name resolve:resolve reject:reject]; } +RCT_EXPORT_METHOD(addViewAttribute:(NSString*) key + withValue:(NSDictionary*) value + withResolver:(RCTPromiseResolveBlock)resolve + withRejecter:(RCTPromiseRejectBlock)reject) +{ + [self addViewAttribute:key value:value resolve:resolve reject:reject]; +} + +RCT_EXPORT_METHOD(removeViewAttribute:(NSString*) key + withResolver:(RCTPromiseResolveBlock)resolve + withRejecter:(RCTPromiseRejectBlock)reject) +{ + [self removeViewAttribute:key resolve:resolve reject:reject]; +} + +RCT_EXPORT_METHOD(addViewAttributes:(NSDictionary*) attributes + withResolver:(RCTPromiseResolveBlock)resolve + withRejecter:(RCTPromiseRejectBlock)reject) +{ + [self addViewAttributes:attributes resolve:resolve reject:reject]; +} + +RCT_EXPORT_METHOD(removeViewAttributes:(NSArray *)keys + withResolver:(RCTPromiseResolveBlock)resolve + withRejecter:(RCTPromiseRejectBlock)reject) +{ + [self removeViewAttributes:keys resolve:resolve reject:reject]; +} + RCT_REMAP_METHOD(addViewLoadingTime, withOverwrite:(BOOL)overwrite withResolver:(RCTPromiseResolveBlock)resolve withRejecter:(RCTPromiseRejectBlock)reject) @@ -138,7 +167,7 @@ @implementation DdRum // Thanks to this guard, we won't compile this code when we build for the old architecture. #ifdef RCT_NEW_ARCH_ENABLED - (std::shared_ptr)getTurboModule: - (const facebook::react::ObjCTurboModule::InitParams &)params +(const facebook::react::ObjCTurboModule::InitParams &)params { return std::make_shared(params); } @@ -180,6 +209,22 @@ - (void)addTiming:(NSString *)name resolve:(RCTPromiseResolveBlock)resolve rejec [self.ddRumImplementation addTimingWithName:name resolve:resolve reject:reject]; } +- (void)addViewAttribute:(NSString *)key value:(NSDictionary *)value resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + [self.ddRumImplementation addViewAttributeWithKey:key value:value resolve:resolve reject:reject]; +} + +- (void)removeViewAttribute:(NSString *)key resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + [self.ddRumImplementation removeViewAttributeWithKey:key resolve:resolve reject:reject]; +} + +- (void)addViewAttributes:(NSDictionary *)attributes resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + [self.ddRumImplementation addViewAttributesWithAttributes:attributes resolve:resolve reject:reject]; +} + +- (void)removeViewAttributes:(NSArray *)keys resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + [self.ddRumImplementation removeViewAttributesWithKeys:keys resolve:resolve reject:reject]; +} + - (void)addViewLoadingTime:(BOOL)overwrite resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {\ [self.ddRumImplementation addViewLoadingTimeWithOverwrite:overwrite resolve:resolve reject:reject]; } diff --git a/packages/core/ios/Sources/DdRumImplementation.swift b/packages/core/ios/Sources/DdRumImplementation.swift index 9f8da4c7f..6fac21f82 100644 --- a/packages/core/ios/Sources/DdRumImplementation.swift +++ b/packages/core/ios/Sources/DdRumImplementation.swift @@ -181,6 +181,34 @@ public class DdRumImplementation: NSObject { resolve(nil) } + @objc + public func addViewAttribute(key: AttributeKey, value: NSDictionary, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { + if let attributeValue = value.object(forKey: "value") { + let castedAttribute = castValueToSwift(attributeValue) + nativeRUM.addViewAttribute(forKey: key, value: castedAttribute) + } + resolve(nil) + } + + @objc + public func removeViewAttribute(key: AttributeKey, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { + nativeRUM.removeViewAttribute(forKey: key) + resolve(nil) + } + + @objc + public func addViewAttributes(attributes: NSDictionary, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { + let castedAttributes = castAttributesToSwift(attributes) + nativeRUM.addViewAttributes(castedAttributes) + resolve(nil) + } + + @objc + public func removeViewAttributes(keys: [AttributeKey], resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { + nativeRUM.removeViewAttributes(forKeys: keys) + resolve(nil) + } + @objc public func addViewLoadingTime(overwrite: Bool, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { nativeRUM.addViewLoadingTime(overwrite: overwrite) diff --git a/packages/core/ios/Sources/DdSdkImplementation.swift b/packages/core/ios/Sources/DdSdkImplementation.swift index 6e24af4e3..87fe91729 100644 --- a/packages/core/ios/Sources/DdSdkImplementation.swift +++ b/packages/core/ios/Sources/DdSdkImplementation.swift @@ -4,18 +4,19 @@ * Copyright 2016-Present Datadog, Inc. */ -import Foundation import DatadogCore -import DatadogRUM +import DatadogCrashReporting +import DatadogInternal import DatadogLogs +import DatadogRUM import DatadogTrace -import DatadogCrashReporting import DatadogWebViewTracking -import DatadogInternal +import Foundation import React func getDefaultAppVersion() -> String { - let bundleShortVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String + let bundleShortVersion = + Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String let bundleVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String return bundleShortVersion ?? bundleVersion ?? "0.0.0" } @@ -29,7 +30,7 @@ public class DdSdkImplementation: NSObject { let RUMMonitorInternalProvider: () -> RUMMonitorInternalProtocol? var webviewMessageEmitter: InternalExtension.AbstractMessageEmitter? - private let jsLongTaskThresholdInSeconds: TimeInterval = 0.1; + private let jsLongTaskThresholdInSeconds: TimeInterval = 0.1 @objc public convenience init(bridge: RCTBridge) { @@ -41,7 +42,7 @@ public class DdSdkImplementation: NSObject { RUMMonitorInternalProvider: { RUMMonitor.shared()._internal } ) } - + init( mainDispatchQueue: DispatchQueueType, jsDispatchQueue: DispatchQueueType, @@ -56,10 +57,13 @@ public class DdSdkImplementation: NSObject { self.RUMMonitorInternalProvider = RUMMonitorInternalProvider super.init() } - + // Using @escaping RCTPromiseResolveBlock type will result in an issue when compiling the Swift header file. @objc - public func initialize(configuration: NSDictionary, resolve:@escaping ((Any?) -> Void), reject:RCTPromiseRejectBlock) -> Void { + public func initialize( + configuration: NSDictionary, resolve: @escaping ((Any?) -> Void), + reject: RCTPromiseRejectBlock + ) { let sdkConfiguration = configuration.asDdSdkConfiguration() let nativeInitialization = DdSdkNativeInitialization() @@ -111,7 +115,9 @@ public class DdSdkImplementation: NSObject { } @objc - public func setUserInfo(userInfo: NSDictionary, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { + public func setUserInfo( + userInfo: NSDictionary, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock + ) { let castedUserInfo = castAttributesToSwift(userInfo) let id = castedUserInfo["id"] as? String let name = castedUserInfo["name"] as? String @@ -119,21 +125,22 @@ public class DdSdkImplementation: NSObject { var extraInfo: [AttributeKey: AttributeValue] = [:] if let extraInfoEncodable = castedUserInfo["extraInfo"] as? AnyEncodable, - let extraInfoDict = extraInfoEncodable.value as? [String: Any] { + let extraInfoDict = extraInfoEncodable.value as? [String: Any] + { extraInfo = castAttributesToSwift(extraInfoDict) } if let validId = id { Datadog.setUserInfo(id: validId, name: name, email: email, extraInfo: extraInfo) - } else { - // TO DO - log warning message? } resolve(nil) } - + @objc - public func addUserExtraInfo(extraInfo: NSDictionary, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { + public func addUserExtraInfo( + extraInfo: NSDictionary, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock + ) { let castedExtraInfo = castAttributesToSwift(extraInfo) Datadog.addUserExtraInfo(castedExtraInfo) @@ -141,59 +148,80 @@ public class DdSdkImplementation: NSObject { } @objc - public func clearUserInfo(resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { + public func clearUserInfo(resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) { Datadog.clearUserInfo() resolve(nil) } @objc - public func setTrackingConsent(trackingConsent: NSString, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { + public func setTrackingConsent( + trackingConsent: NSString, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock + ) { Datadog.set(trackingConsent: (trackingConsent as NSString?).asTrackingConsent()) resolve(nil) } - - + @objc - public func sendTelemetryLog(message: NSString, attributes: NSDictionary, config: NSDictionary, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { + public func sendTelemetryLog( + message: NSString, attributes: NSDictionary, config: NSDictionary, + resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock + ) { let castedAttributes = castAttributesToSwift(attributes) let castedConfig = castAttributesToSwift(config) - DdTelemetry.sendTelemetryLog(message: message as String, attributes: castedAttributes, config: castedConfig) + DdTelemetry.sendTelemetryLog( + message: message as String, attributes: castedAttributes, config: castedConfig) resolve(nil) } @objc - public func telemetryDebug(message: NSString, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { - DdTelemetry.telemetryDebug(id: "datadog_react_native:\(message)", message: message as String) + public func telemetryDebug( + message: NSString, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock + ) { + DdTelemetry.telemetryDebug( + id: "datadog_react_native:\(message)", message: message as String) resolve(nil) } - + @objc - public func telemetryError(message: NSString, stack: NSString, kind: NSString, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { - DdTelemetry.telemetryError(id: "datadog_react_native:\(String(describing: kind)):\(message)", message: message as String, kind: kind as String, stack: stack as String) + public func telemetryError( + message: NSString, stack: NSString, kind: NSString, resolve: RCTPromiseResolveBlock, + reject: RCTPromiseRejectBlock + ) { + DdTelemetry.telemetryError( + id: "datadog_react_native:\(String(describing: kind)):\(message)", + message: message as String, kind: kind as String, stack: stack as String) resolve(nil) } @objc - public func consumeWebviewEvent(message: NSString, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { - do{ + public func consumeWebviewEvent( + message: NSString, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock + ) { + do { try DatadogSDKWrapper.shared.sendWebviewMessage(body: message) } catch { - DdTelemetry.telemetryError(id: "datadog_react_native:\(error.localizedDescription)", message: "The message being sent was:\(message)" as String, kind: "WebViewEventBridgeError" as String, stack: String(describing: error) as String) + DdTelemetry.telemetryError( + id: "datadog_react_native:\(error.localizedDescription)", + message: "The message being sent was:\(message)" as String, + kind: "WebViewEventBridgeError" as String, + stack: String(describing: error) as String) } resolve(nil) } - + @objc - public func clearAllData(resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { + public func clearAllData(resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) { Datadog.clearAllData() resolve(nil) } - func overrideReactNativeTelemetry(rnConfiguration: DdSdkConfiguration) -> Void { + func overrideReactNativeTelemetry(rnConfiguration: DdSdkConfiguration) { DdTelemetry.overrideTelemetryConfiguration( - initializationType: rnConfiguration.configurationForTelemetry?.initializationType as? String, - reactNativeVersion: rnConfiguration.configurationForTelemetry?.reactNativeVersion as? String, + initializationType: rnConfiguration.configurationForTelemetry?.initializationType + as? String, + reactNativeVersion: rnConfiguration.configurationForTelemetry?.reactNativeVersion + as? String, reactVersion: rnConfiguration.configurationForTelemetry?.reactVersion as? String, trackCrossPlatformLongTasks: rnConfiguration.longTaskThresholdMs != 0, trackErrors: rnConfiguration.configurationForTelemetry?.trackErrors, @@ -208,24 +236,28 @@ public class DdSdkImplementation: NSObject { func startJSRefreshRateMonitoring(sdkConfiguration: DdSdkConfiguration) { if let frameTimeCallback = buildFrameTimeCallback(sdkConfiguration: sdkConfiguration) { // Falling back to mainDispatchQueue if bridge is nil is only useful for tests - self.jsRefreshRateMonitor.startMonitoring(jsQueue: jsDispatchQueue, frameTimeCallback: frameTimeCallback) + self.jsRefreshRateMonitor.startMonitoring( + jsQueue: jsDispatchQueue, frameTimeCallback: frameTimeCallback) } } - func buildFrameTimeCallback(sdkConfiguration: DdSdkConfiguration)-> ((Double) -> ())? { + func buildFrameTimeCallback(sdkConfiguration: DdSdkConfiguration) -> ((Double) -> Void)? { let jsRefreshRateMonitoringEnabled = sdkConfiguration.vitalsUpdateFrequency != nil let jsLongTaskMonitoringEnabled = sdkConfiguration.longTaskThresholdMs != 0 - - if (!jsRefreshRateMonitoringEnabled && !jsLongTaskMonitoringEnabled) { + + if !jsRefreshRateMonitoringEnabled && !jsLongTaskMonitoringEnabled { return nil } func frameTimeCallback(frameTime: Double) { // These checks happen before dispatching because they are quick and less overhead than the dispatch itself. let shouldRecordFrameTime = jsRefreshRateMonitoringEnabled && frameTime > 0 - let shouldRecordLongTask = jsLongTaskMonitoringEnabled && frameTime > sdkConfiguration.longTaskThresholdMs / 1_000 + let shouldRecordLongTask = + jsLongTaskMonitoringEnabled + && frameTime > sdkConfiguration.longTaskThresholdMs / 1_000 guard shouldRecordFrameTime || shouldRecordLongTask, - let rumMonitorInternal = RUMMonitorInternalProvider() else { return } + let rumMonitorInternal = RUMMonitorInternalProvider() + else { return } // Record current timestamp, it may change slightly before event is created on background thread. let now = Date() @@ -235,12 +267,14 @@ public class DdSdkImplementation: NSObject { let normalizedFrameTimeSeconds = DdSdkImplementation.normalizeFrameTimeForDeviceRefreshRate(frameTime) rumMonitorInternal.updatePerformanceMetric(at: now, metric: .jsFrameTimeSeconds, value: normalizedFrameTimeSeconds, attributes: [:]) } - if (shouldRecordLongTask) { - rumMonitorInternal.addLongTask(at: now, duration: frameTime, attributes: ["long_task.target": "javascript"]) + if shouldRecordLongTask { + rumMonitorInternal.addLongTask( + at: now, duration: frameTime, attributes: ["long_task.target": "javascript"] + ) } } } - + return frameTimeCallback } diff --git a/packages/core/ios/Tests/DdRumTests.swift b/packages/core/ios/Tests/DdRumTests.swift index 1102b7b6b..5f6adc016 100644 --- a/packages/core/ios/Tests/DdRumTests.swift +++ b/packages/core/ios/Tests/DdRumTests.swift @@ -253,6 +253,65 @@ internal class DdRumTests: XCTestCase { XCTAssertEqual(mockNativeRUM.receivedAttributes.count, 0) } + func testAddViewAttribute() throws { + let viewAttributeKey = "attributeKey" + let viewAttributes = NSDictionary( + dictionary: [ + "value": 123, + ] + ) + + rum.addViewAttribute(key: viewAttributeKey, value: viewAttributes, resolve: mockResolve, reject: mockReject) + + XCTAssertEqual(mockNativeRUM.calledMethods.count, 1) + XCTAssertEqual(mockNativeRUM.calledMethods.last, .addViewAttribute(key: viewAttributeKey)) + XCTAssertEqual(mockNativeRUM.receivedAttributes.count, 1) + let lastAttributes = try XCTUnwrap(mockNativeRUM.receivedAttributes.last) + XCTAssertEqual(lastAttributes.count, 1) + XCTAssertEqual(lastAttributes["attributeKey"] as? Int64, 123) + } + + func testRemoveViewAttribute() throws { + let viewAttributeKey = "attributeKey" + + rum.removeViewAttribute(key: viewAttributeKey, resolve: mockResolve, reject: mockReject) + + XCTAssertEqual(mockNativeRUM.calledMethods.count, 1) + XCTAssertEqual(mockNativeRUM.calledMethods.last, .removeViewAttribute(key: viewAttributeKey)) + } + + func testAddViewAttributes() throws { + let viewAttributes = NSDictionary( + dictionary: [ + "attribute-1": 123, + "attribute-2": "abc", + "attribute-3": true, + ] + ) + + rum.addViewAttributes(attributes: viewAttributes, resolve: mockResolve, reject: mockReject) + + + XCTAssertEqual(mockNativeRUM.calledMethods.count, 1) + XCTAssertEqual(mockNativeRUM.calledMethods.last, .addViewAttributes()) + XCTAssertEqual(mockNativeRUM.receivedAttributes.count, 1) + let lastAttributes = try XCTUnwrap(mockNativeRUM.receivedAttributes.last) + XCTAssertEqual(lastAttributes.count, 3) + XCTAssertEqual(lastAttributes["attribute-1"] as? Int64, 123) + XCTAssertEqual(lastAttributes["attribute-2"] as? String, "abc") + XCTAssertEqual(lastAttributes["attribute-3"] as? Bool, true) + } + + + func testRemoveViewAttributes() throws { + let viewAttributeKeys = ["attributeKey1", "attributeKey2", "attributeKey3"] + + rum.removeViewAttributes(keys: viewAttributeKeys, resolve: mockResolve, reject: mockReject) + + XCTAssertEqual(mockNativeRUM.calledMethods.count, 1) + XCTAssertEqual(mockNativeRUM.calledMethods.last, .removeViewAttributes(keys: viewAttributeKeys)) + } + func testAddViewLoadingTime() throws { rum.addViewLoadingTime(overwrite: true, resolve: mockResolve, reject: mockReject) diff --git a/packages/core/ios/Tests/MockRUMMonitor.swift b/packages/core/ios/Tests/MockRUMMonitor.swift index 3a882ed47..4a1d0cd92 100644 --- a/packages/core/ios/Tests/MockRUMMonitor.swift +++ b/packages/core/ios/Tests/MockRUMMonitor.swift @@ -10,22 +10,6 @@ @testable import DatadogSDKReactNative internal class MockRUMMonitor: RUMMonitorProtocol { - func addViewAttribute(forKey key: DatadogInternal.AttributeKey, value: any DatadogInternal.AttributeValue) { - // not implemented - } - - func addViewAttributes(_ attributes: [DatadogInternal.AttributeKey : any DatadogInternal.AttributeValue]) { - // not implemented - } - - func removeViewAttribute(forKey key: DatadogInternal.AttributeKey) { - // not implemented - } - - func removeViewAttributes(forKeys keys: [DatadogInternal.AttributeKey]) { - // not implemented - } - func currentSessionID(completion: @escaping (String?) -> Void) { // not implemented } @@ -71,6 +55,10 @@ internal class MockRUMMonitor: RUMMonitorProtocol { case stopUserAction(type: RUMActionType, name: String?) case addUserAction(type: RUMActionType, name: String) case addTiming(name: String) + case addViewAttribute(key: String) + case removeViewAttribute(key: String) + case addViewAttributes(_: Int? = nil) // We need an attribute for the case to be Equatable + case removeViewAttributes(keys: [String]) case addViewLoadingTime(overwrite: Bool) case stopSession(_: Int? = nil) // We need an attribute for the case to be Equatable case addResourceMetrics(resourceKey: String, @@ -131,6 +119,24 @@ internal class MockRUMMonitor: RUMMonitorProtocol { func addTiming(name: String) { calledMethods.append(.addTiming(name: name)) } + func addViewAttribute(forKey key: DatadogInternal.AttributeKey, value: any DatadogInternal.AttributeValue) { + calledMethods.append(.addViewAttribute(key: key)) + receivedAttributes.append([key :value]) + } + + func removeViewAttribute(forKey key: DatadogInternal.AttributeKey) { + calledMethods.append(.removeViewAttribute(key: key)) + } + + func addViewAttributes(_ attributes: [DatadogInternal.AttributeKey : any DatadogInternal.AttributeValue]) { + calledMethods.append(.addViewAttributes()) + receivedAttributes.append(attributes) + } + + func removeViewAttributes(forKeys keys: [DatadogInternal.AttributeKey]) { + calledMethods.append(.removeViewAttributes(keys: keys)) + } + func addViewLoadingTime(overwrite: Bool) { calledMethods.append(.addViewLoadingTime(overwrite: overwrite)) } diff --git a/packages/core/jest/mock.js b/packages/core/jest/mock.js index c49d13f48..0dc9ec138 100644 --- a/packages/core/jest/mock.js +++ b/packages/core/jest/mock.js @@ -119,6 +119,18 @@ module.exports = { addTiming: jest .fn() .mockImplementation(() => new Promise(resolve => resolve())), + addViewAttribute: jest + .fn() + .mockImplementation(() => new Promise(resolve => resolve())), + removeViewAttribute: jest + .fn() + .mockImplementation(() => new Promise(resolve => resolve())), + addViewAttributes: jest + .fn() + .mockImplementation(() => new Promise(resolve => resolve())), + removeViewAttributes: jest + .fn() + .mockImplementation(() => new Promise(resolve => resolve())), addViewLoadingTime: jest .fn() .mockImplementation(() => new Promise(resolve => resolve())), diff --git a/packages/core/src/rum/DdRum.ts b/packages/core/src/rum/DdRum.ts index 908404fc8..e5deb8146 100644 --- a/packages/core/src/rum/DdRum.ts +++ b/packages/core/src/rum/DdRum.ts @@ -9,6 +9,7 @@ import { DdAttributes } from '../DdAttributes'; import { InternalLog } from '../InternalLog'; import { SdkVerbosity } from '../SdkVerbosity'; import type { DdNativeRumType } from '../nativeModulesTypes'; +import type { Attributes } from '../sdk/AttributesSingleton/types'; import { bufferVoidNativeCall } from '../sdk/DatadogProvider/Buffer/bufferNativeCall'; import { DdSdk } from '../sdk/DdSdk'; import { GlobalState } from '../sdk/GlobalState/GlobalState'; @@ -284,6 +285,50 @@ class DdRumWrapper implements DdRumType { return bufferVoidNativeCall(() => this.nativeRum.addTiming(name)); }; + addViewAttribute = (key: string, value: unknown): Promise => { + InternalLog.log( + `Adding view attribute “${key}" with value “${JSON.stringify( + value + )}” to RUM View`, + SdkVerbosity.DEBUG + ); + return bufferVoidNativeCall(() => + this.nativeRum.addViewAttribute(key, { value }) + ); + }; + + removeViewAttribute = (key: string): Promise => { + InternalLog.log( + `Removing view attribute “${key}" from RUM View`, + SdkVerbosity.DEBUG + ); + return bufferVoidNativeCall(() => + this.nativeRum.removeViewAttribute(key) + ); + }; + + addViewAttributes = (attributes: Attributes): Promise => { + InternalLog.log( + `Adding view attributes "${JSON.stringify( + attributes + )}” to RUM View`, + SdkVerbosity.DEBUG + ); + return bufferVoidNativeCall(() => + this.nativeRum.addViewAttributes(attributes) + ); + }; + + removeViewAttributes = (keys: string[]): Promise => { + InternalLog.log( + `Removing view attributes “${keys}" from RUM View`, + SdkVerbosity.DEBUG + ); + return bufferVoidNativeCall(() => + this.nativeRum.removeViewAttributes(keys) + ); + }; + addViewLoadingTime = (overwrite: boolean): Promise => { InternalLog.log( overwrite diff --git a/packages/core/src/rum/__tests__/DdRum.test.ts b/packages/core/src/rum/__tests__/DdRum.test.ts index 7e5fc24de..41b873fe9 100644 --- a/packages/core/src/rum/__tests__/DdRum.test.ts +++ b/packages/core/src/rum/__tests__/DdRum.test.ts @@ -1091,6 +1091,98 @@ describe('DdRum', () => { }); }); + describe('DdRum.addTiming', () => { + it('calls the native SDK when setting a timing', async () => { + // GIVEN + const timingName = 'testTiming'; + + // WHEN + await DdRum.addTiming(timingName); + + // THEN + expect(NativeModules.DdRum.addTiming).toHaveBeenCalledTimes(1); + expect(NativeModules.DdRum.addTiming).toHaveBeenCalledWith( + timingName + ); + }); + }); + + describe('DdRum.addViewAttribute', () => { + it('calls the native SDK when setting a view attribute', async () => { + // GIVEN + const key = 'testAttribute'; + const value = { test: 'attribute' }; + + // WHEN + + await DdRum.addViewAttribute(key, value); + + // THEN + expect( + NativeModules.DdRum.addViewAttribute + ).toHaveBeenCalledTimes(1); + expect( + NativeModules.DdRum.addViewAttribute + ).toHaveBeenCalledWith(key, { value }); + }); + }); + + describe('DdRum.removViewAttribute', () => { + it('calls the native SDK when removing a view attribute', async () => { + // GIVEN + const key = 'testAttribute'; + + // WHEN + await DdRum.removeViewAttribute(key); + + // THEN + expect( + NativeModules.DdRum.removeViewAttribute + ).toHaveBeenCalledTimes(1); + expect( + NativeModules.DdRum.removeViewAttribute + ).toHaveBeenCalledWith(key); + }); + }); + + describe('DdRum.addViewAttributes', () => { + it('calls the native SDK when setting view attributes', async () => { + // GIVEN + const attributes = { + test: 'attribute' + }; + + // WHEN + await DdRum.addViewAttributes(attributes); + + // THEN + expect( + NativeModules.DdRum.addViewAttributes + ).toHaveBeenCalledTimes(1); + expect( + NativeModules.DdRum.addViewAttributes + ).toHaveBeenCalledWith(attributes); + }); + }); + + describe('DdRum.removViewAttributes', () => { + it('calls the native SDK when removing view attributes', async () => { + // GIVEN + const keysToDelete = ['test1', 'test2']; + + // WHEN + await DdRum.removeViewAttributes(keysToDelete); + + // THEN + expect( + NativeModules.DdRum.removeViewAttributes + ).toHaveBeenCalledTimes(1); + expect( + NativeModules.DdRum.removeViewAttributes + ).toHaveBeenCalledWith(keysToDelete); + }); + }); + describe('DdRum.addAction', () => { test('uses given context when context is valid', async () => { const context = { diff --git a/packages/core/src/rum/types.ts b/packages/core/src/rum/types.ts index fc8d07c02..3def7f0e6 100644 --- a/packages/core/src/rum/types.ts +++ b/packages/core/src/rum/types.ts @@ -4,6 +4,7 @@ * Copyright 2016-Present Datadog, Inc. */ +import type { Attributes } from '../sdk/AttributesSingleton/types'; import type { ErrorSource } from '../types'; import type { DatadogTracingContext } from './instrumentation/resourceTracking/distributedTracing/DatadogTracingContext'; @@ -148,6 +149,31 @@ export type DdRumType = { */ addTiming(name: string): Promise; + /** + * Adds a custom attribute to the active RUM View. It will be propagated to all future RUM events associated with the active View. + * @param key: key for this view attribute. + * @param value: value for this attribute. + */ + addViewAttribute(key: string, value: unknown): Promise; + + /** + * Removes an attribute from the active RUM View. + * @param key: key for the attribute to be removed from the view. + */ + removeViewAttribute(key: string): Promise; + + /** + * Adds multiple attributes to the active RUM View. They will be propagated to all future RUM events associated with the active View. + * @param attributes: key/value object containing all attributes to be added to the view. + */ + addViewAttributes(attributes: Attributes): Promise; + + /** + * Removes multiple attributes from the active RUM View. + * @param keys: keys for the attributes to be removed from the view. + */ + removeViewAttributes(keys: string[]): Promise; + /** * Adds the loading time of the view to the active view. * It is calculated as the difference between the current time and the start time of the view. diff --git a/packages/core/src/specs/NativeDdRum.ts b/packages/core/src/specs/NativeDdRum.ts index f6f7b3daa..e31f5b925 100644 --- a/packages/core/src/specs/NativeDdRum.ts +++ b/packages/core/src/specs/NativeDdRum.ts @@ -136,6 +136,31 @@ export interface Spec extends TurboModule { */ addTiming(name: string): Promise; + /** + * Adds a custom attribute to the active RUM View. It will be propagated to all future RUM events associated with the active View. + * @param key: key for this view attribute. + * @param value: value for this attribute. + */ + addViewAttribute(key: string, value: Object): Promise; + + /** + * Removes an attribute from the active RUM View. + * @param key: key for the attribute to be removed from the view. + */ + removeViewAttribute(key: string): Promise; + + /** + * Adds multiple attributes to the active RUM View. They will be propagated to all future RUM events associated with the active View. + * @param attributes: key/value object containing all attributes to be added to the view. + */ + addViewAttributes(attributes: Object): Promise; + + /** + * Removes multiple attributes from the active RUM View. + * @param keys: keys for the attributes to be removed from the view. + */ + removeViewAttributes(keys: string[]): Promise; + /** * Adds the loading time of the view to the active view. * It is calculated as the difference between the current time and the start time of the view. From 4ae43c2f5959ff95ef6688bccf7e14742983d588 Mon Sep 17 00:00:00 2001 From: Sergio Barrio Date: Fri, 22 Aug 2025 15:43:01 +0200 Subject: [PATCH 046/410] Fix FileBasedConfiguration related issues --- example/datadog-configuration.json | 20 ++ example/src/App.tsx | 20 +- .../codepush/src/__tests__/index.test.tsx | 4 +- .../FileBasedConfiguration.ts | 54 ++--- .../__tests__/FileBasedConfiguration.test.ts | 192 ++++++++++-------- .../__fixtures__/malformed-configuration.json | 1 - 6 files changed, 157 insertions(+), 134 deletions(-) create mode 100644 example/datadog-configuration.json diff --git a/example/datadog-configuration.json b/example/datadog-configuration.json new file mode 100644 index 000000000..684e60304 --- /dev/null +++ b/example/datadog-configuration.json @@ -0,0 +1,20 @@ +{ + "$schema": "./node_modules/@datadog/mobile-react-native/datadog-configuration.schema.json", + "configuration": { + "applicationId": "APP_ID", + "batchSize": "SMALL", + "clientToken": "CLIENT_TOKEN", + "env": "ENVIRONMENT", + "longTaskThresholdMs": 1000, + "nativeCrashReportEnabled": true, + "sessionSamplingRate": 100, + "site": "US1", + "telemetrySampleRate": 20, + "trackBackgroundEvents": false, + "trackErrors": true, + "trackInteractions": true, + "trackResources": true, + "trackingConsent": "GRANTED", + "verbosity": "DEBUG" + } +} diff --git a/example/src/App.tsx b/example/src/App.tsx index cefcafffe..c29982a97 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -7,7 +7,7 @@ import AboutScreen from './screens/AboutScreen'; import style from './screens/styles'; import { navigationRef } from './NavigationRoot'; import { DdRumReactNavigationTracking, ViewNamePredicate } from '@datadog/mobile-react-navigation'; -import {DatadogProvider} from '@datadog/mobile-react-native' +import {DatadogProvider, FileBasedConfiguration} from '@datadog/mobile-react-native' import { Route } from "@react-navigation/native"; import { NestedNavigator } from './screens/NestedNavigator/NestedNavigator'; import { getDatadogConfig, onDatadogInitialization } from './ddUtils'; @@ -19,9 +19,25 @@ const viewPredicate: ViewNamePredicate = function customViewNamePredicate(route: return "Custom RN " + trackedName; } +// === Datadog Provider Configuration schemes === + +// 1.- Direct configuration +const configuration = getDatadogConfig(TrackingConsent.GRANTED) + +// 2.- File based configuration from .json +// const configuration = new FileBasedConfiguration(require("../datadog-configuration.json")); + +// 3.- File based configuration from .json and custom mapper setup +// const configuration = new FileBasedConfiguration( { +// configuration: require("../datadog-configuration.json").configuration, +// errorEventMapper: (event) => event, +// resourceEventMapper: (event) => event, +// actionEventMapper: (event) => event}); + + export default function App() { return ( - + { DdRumReactNavigationTracking.startTrackingViews(navigationRef.current, viewPredicate) }}> diff --git a/packages/codepush/src/__tests__/index.test.tsx b/packages/codepush/src/__tests__/index.test.tsx index 5c94c16ef..63e1af7d6 100644 --- a/packages/codepush/src/__tests__/index.test.tsx +++ b/packages/codepush/src/__tests__/index.test.tsx @@ -279,7 +279,7 @@ describe('AppCenter Codepush integration', () => { }; const configuration = new FileBasedConfiguration({ - configuration: { configuration: autoInstrumentationConfig } + configuration: autoInstrumentationConfig }); render(); @@ -346,7 +346,7 @@ describe('AppCenter Codepush integration', () => { }; const configuration = new FileBasedConfiguration({ - configuration: { configuration: autoInstrumentationConfig } + configuration: autoInstrumentationConfig }); render(); diff --git a/packages/core/src/sdk/FileBasedConfiguration/FileBasedConfiguration.ts b/packages/core/src/sdk/FileBasedConfiguration/FileBasedConfiguration.ts index ddd1943aa..3fc69f1f3 100644 --- a/packages/core/src/sdk/FileBasedConfiguration/FileBasedConfiguration.ts +++ b/packages/core/src/sdk/FileBasedConfiguration/FileBasedConfiguration.ts @@ -56,44 +56,7 @@ export class FileBasedConfiguration extends DatadogProviderConfiguration { const resolveJSONConfiguration = ( userSpecifiedConfiguration: unknown ): Record => { - if ( - userSpecifiedConfiguration === undefined || - userSpecifiedConfiguration === null - ) { - try { - // This corresponds to a file located at the root of a RN project. - // /!\ We have to write the require this way as dynamic requires are not supported by Hermes. - // eslint-disable-next-line global-require, @typescript-eslint/no-var-requires - const jsonContent = require('../../../../../../datadog-configuration.json'); - - if ( - typeof jsonContent !== 'object' || - !jsonContent['configuration'] - ) { - console.error(`Failed to parse the Datadog configuration file located at the root of the project. -Your configuration must validate the node_modules/@datadog/mobile-react-native/datadog-configuration.schema.json JSON schema. -You can use VSCode to check your configuration by adding the following line to your JSON file: -{ - "$schema": "./node_modules/@datadog/mobile-react-native/datadog-configuration.schema.json", -}`); - - return {}; - } - - return jsonContent.configuration as Record; - } catch (error) { - console.error(`Failed to read Datadog configuration file at the root of the project. -If you don't have a datadog-configuration.json file at the same level as your node_modules directory,\ -please use the following syntax:\n -new FileBasedConfiguration({configuration: require('./file/to/configuration-file.json')}) -`); - return {}; - } - } - if ( - typeof userSpecifiedConfiguration !== 'object' || - !(userSpecifiedConfiguration as any)['configuration'] - ) { + if (typeof userSpecifiedConfiguration !== 'object') { console.error(`Failed to parse the Datadog configuration file you provided. Your configuration must validate the node_modules/@datadog/mobile-react-native/datadog-configuration.schema.json JSON schema. You can use VSCode to check your configuration by adding the following line to your JSON file: @@ -104,10 +67,7 @@ You can use VSCode to check your configuration by adding the following line to y return {}; } - return (userSpecifiedConfiguration as any)['configuration'] as Record< - string, - any - >; + return (userSpecifiedConfiguration as any) as Record; }; export const getJSONConfiguration = ( @@ -130,6 +90,16 @@ export const getJSONConfiguration = ( } => { const configuration = resolveJSONConfiguration(userSpecifiedConfiguration); + if ( + configuration.clientToken === undefined || + configuration.env === undefined || + configuration.applicationId === undefined + ) { + console.warn( + 'DATADOG: Warning: Malformed json configuration file - clientToken, applicationId and env are mandatory properties.' + ); + } + return { clientToken: configuration.clientToken, env: configuration.env, diff --git a/packages/core/src/sdk/FileBasedConfiguration/__tests__/FileBasedConfiguration.test.ts b/packages/core/src/sdk/FileBasedConfiguration/__tests__/FileBasedConfiguration.test.ts index 716243e86..6d3ee2e44 100644 --- a/packages/core/src/sdk/FileBasedConfiguration/__tests__/FileBasedConfiguration.test.ts +++ b/packages/core/src/sdk/FileBasedConfiguration/__tests__/FileBasedConfiguration.test.ts @@ -16,35 +16,99 @@ import malformedConfiguration from './__fixtures__/malformed-configuration.json' describe('FileBasedConfiguration', () => { describe('with user-specified configuration', () => { + it('resolves configuration fields', () => { + const configuration = new FileBasedConfiguration( + configurationAllFields + ); + + expect(configuration).toMatchInlineSnapshot(` + FileBasedConfiguration { + "actionEventMapper": null, + "actionNameAttribute": "action-name-attr", + "additionalConfiguration": {}, + "applicationId": "fake-app-id", + "batchProcessingLevel": "MEDIUM", + "batchSize": "MEDIUM", + "bundleLogsWithRum": true, + "bundleLogsWithTraces": true, + "clientToken": "fake-client-token", + "customEndpoints": {}, + "env": "fake-env", + "errorEventMapper": null, + "firstPartyHosts": [ + { + "match": "example.com", + "propagatorTypes": [ + "b3multi", + "tracecontext", + ], + }, + ], + "initializationMode": "SYNC", + "logEventMapper": null, + "longTaskThresholdMs": 44, + "nativeCrashReportEnabled": false, + "nativeInteractionTracking": false, + "nativeLongTaskThresholdMs": 200, + "nativeViewTracking": false, + "proxyConfig": undefined, + "resourceEventMapper": null, + "resourceTracingSamplingRate": 33, + "serviceName": undefined, + "sessionSamplingRate": 100, + "site": "US5", + "telemetrySampleRate": 20, + "trackBackgroundEvents": false, + "trackErrors": true, + "trackFrustrations": true, + "trackInteractions": true, + "trackResources": true, + "trackWatchdogTerminations": false, + "trackingConsent": "not_granted", + "uploadFrequency": "AVERAGE", + "useAccessibilityLabel": false, + "verbosity": "warn", + "vitalsUpdateFrequency": "AVERAGE", + } + `); + }); + + it('prints a warning message when the configuration file cannot be parsed correctly', () => { + const warnSpy = jest.spyOn(console, 'warn'); + getJSONConfiguration(malformedConfiguration); + + expect(warnSpy).toHaveBeenCalledWith( + 'DATADOG: Warning: Malformed json configuration file - clientToken, applicationId and env are mandatory properties.' + ); + }); + it('resolves all properties from a given file path', () => { const config = new FileBasedConfiguration({ configuration: { - configuration: { - applicationId: 'fake-app-id', - env: 'fake-env', - clientToken: 'fake-client-token', - trackInteractions: true, - trackResources: true, - trackErrors: true, - trackingConsent: 'NOT_GRANTED', - longTaskThresholdMs: 44, - site: 'US5', - verbosity: 'WARN', - actionNameAttribute: 'action-name-attr', - useAccessibilityLabel: false, - resourceTracingSamplingRate: 33, - firstPartyHosts: [ - { - match: 'example.com', - propagatorTypes: [ - 'B3MULTI', - 'TRACECONTEXT', - 'B3', - 'DATADOG' - ] - } - ] - } + applicationId: 'fake-app-id', + env: 'fake-env', + clientToken: 'fake-client-token', + trackInteractions: true, + trackResources: true, + trackErrors: true, + trackingConsent: 'NOT_GRANTED', + longTaskThresholdMs: 44, + site: 'US5', + verbosity: 'WARN', + actionNameAttribute: 'action-name-attr', + useAccessibilityLabel: false, + resourceTracingSamplingRate: 33, + firstPartyHosts: [ + { + match: 'example.com', + propagatorTypes: [ + 'B3MULTI', + 'TRACECONTEXT', + 'B3', + 'DATADOG' + ] + } + ] } }); expect(config).toMatchInlineSnapshot(` @@ -103,11 +167,9 @@ describe('FileBasedConfiguration', () => { it('applies default values to configuration from a given file path', () => { const config = new FileBasedConfiguration({ configuration: { - configuration: { - applicationId: 'fake-app-id', - env: 'fake-env', - clientToken: 'fake-client-token' - } + applicationId: 'fake-app-id', + env: 'fake-env', + clientToken: 'fake-client-token' } }); expect(config).toMatchInlineSnapshot(` @@ -159,11 +221,9 @@ describe('FileBasedConfiguration', () => { const resourceEventMapper = () => null; const config = new FileBasedConfiguration({ configuration: { - configuration: { - applicationId: 'fake-app-id', - env: 'fake-env', - clientToken: 'fake-client-token' - } + applicationId: 'fake-app-id', + env: 'fake-env', + clientToken: 'fake-client-token' }, actionEventMapper, errorEventMapper, @@ -188,62 +248,20 @@ describe('FileBasedConfiguration', () => { it('prints a warning message when the first party hosts contain unknown propagator types', () => { const config = new FileBasedConfiguration({ configuration: { - configuration: { - applicationId: 'fake-app-id', - env: 'fake-env', - clientToken: 'fake-client-token', - firstPartyHosts: [ - { - match: 'example.com', - propagatorTypes: ['UNKNOWN'] - } - ] - } + applicationId: 'fake-app-id', + env: 'fake-env', + clientToken: 'fake-client-token', + firstPartyHosts: [ + { + match: 'example.com', + propagatorTypes: ['UNKNOWN'] + } + ] } }); expect(config.firstPartyHosts).toHaveLength(0); }); }); - describe('with resolved file configuration', () => { - it('resolves configuration fields', () => { - const configuration = getJSONConfiguration(configurationAllFields); - - expect(configuration).toMatchInlineSnapshot(` - { - "actionNameAttribute": "action-name-attr", - "applicationId": "fake-app-id", - "clientToken": "fake-client-token", - "env": "fake-env", - "firstPartyHosts": [ - { - "match": "example.com", - "propagatorTypes": [ - "b3multi", - "tracecontext", - ], - }, - ], - "longTaskThresholdMs": 44, - "resourceTracingSamplingRate": 33, - "site": "US5", - "trackErrors": true, - "trackInteractions": true, - "trackResources": true, - "trackingConsent": "not_granted", - "useAccessibilityLabel": false, - "verbosity": "warn", - } - `); - }); - it('prints a warning message when the configuration file is not found', () => { - expect(() => getJSONConfiguration(undefined)).not.toThrow(); - }); - it('prints a warning message when the configuration file cannot be parsed correctly', () => { - expect(() => - getJSONConfiguration(malformedConfiguration) - ).not.toThrow(); - }); - }); describe('formatPropagatorType', () => { it('formats all propagatorTypes correctly', () => { diff --git a/packages/core/src/sdk/FileBasedConfiguration/__tests__/__fixtures__/malformed-configuration.json b/packages/core/src/sdk/FileBasedConfiguration/__tests__/__fixtures__/malformed-configuration.json index 28423084d..0e1b26639 100644 --- a/packages/core/src/sdk/FileBasedConfiguration/__tests__/__fixtures__/malformed-configuration.json +++ b/packages/core/src/sdk/FileBasedConfiguration/__tests__/__fixtures__/malformed-configuration.json @@ -1,5 +1,4 @@ { "clientToken": "clientToken", - "env": "env", "applicationId": "applicationId" } From c20186b97fa8198a15da7652cb7265600392a297 Mon Sep 17 00:00:00 2001 From: Sergio Barrio Date: Mon, 27 Oct 2025 14:45:54 +0100 Subject: [PATCH 047/410] Bump Android Native SDK to 3.2.0 and regenerate app podfiles --- benchmarks/android/app/build.gradle | 2 +- benchmarks/ios/Podfile.lock | 10 +++++----- example-new-architecture/ios/Podfile.lock | 6 +++--- example/ios/Podfile.lock | 14 +++++++------- packages/core/android/build.gradle | 10 +++++----- .../android/build.gradle | 4 ++-- packages/react-native-webview/android/build.gradle | 2 +- 7 files changed, 24 insertions(+), 24 deletions(-) diff --git a/benchmarks/android/app/build.gradle b/benchmarks/android/app/build.gradle index 04c240bd0..6d9f28764 100644 --- a/benchmarks/android/app/build.gradle +++ b/benchmarks/android/app/build.gradle @@ -129,5 +129,5 @@ dependencies { // Benchmark tools from dd-sdk-android are used for vitals recording // Remember to bump thid alongside the main dd-sdk-android dependencies - implementation("com.datadoghq:dd-sdk-android-benchmark-internal:3.1.0") + implementation("com.datadoghq:dd-sdk-android-benchmark-internal:3.2.0") } diff --git a/benchmarks/ios/Podfile.lock b/benchmarks/ios/Podfile.lock index 45c15e56f..abcaf1925 100644 --- a/benchmarks/ios/Podfile.lock +++ b/benchmarks/ios/Podfile.lock @@ -10,7 +10,7 @@ PODS: - DatadogInternal (= 3.1.0) - DatadogRUM (3.1.0): - DatadogInternal (= 3.1.0) - - DatadogSDKReactNative (2.12.1): + - DatadogSDKReactNative (2.13.0): - DatadogCore (= 3.1.0) - DatadogCrashReporting (= 3.1.0) - DatadogLogs (= 3.1.0) @@ -60,7 +60,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - DatadogSDKReactNativeWebView (2.12.1): + - DatadogSDKReactNativeWebView (2.13.0): - DatadogInternal (= 3.1.0) - DatadogSDKReactNative - DatadogWebViewTracking (= 3.1.0) @@ -2075,9 +2075,9 @@ SPEC CHECKSUMS: DatadogInternal: 7837b2ce3d525d429682532eeda697b181299fdc DatadogLogs: 250894b5a99da5b924a019049c0d0326823cdbd6 DatadogRUM: 0d2a60e1abb8aacfb8827ef84f6d5deb4d5026c8 - DatadogSDKReactNative: 8e0f39de38621d4d7ed961a74d8a216fd3a38321 - DatadogSDKReactNativeSessionReplay: f9288c8e981dcc65d1f727b01421ee9a7601e75f - DatadogSDKReactNativeWebView: 993527f6c5d38e0fcc4804a6a60c334dd199dc5b + DatadogSDKReactNative: 620018df2896abcfad6b338c633cc8eccd5de406 + DatadogSDKReactNativeSessionReplay: b2ef22431dd0816adea8d65df13180cf40533f9d + DatadogSDKReactNativeWebView: 299629cf348a5e8f1dabb8289920a00eee625d6a DatadogSessionReplay: 6bc71888e2b41dd0de3325f06f0c0b3cee0e6df4 DatadogTrace: f59e933074cd285ad7e9f5af991f8fe04b095991 DatadogWebViewTracking: 9bc92b4147aeed47eb1911451f651094aa6dd6c1 diff --git a/example-new-architecture/ios/Podfile.lock b/example-new-architecture/ios/Podfile.lock index c0cd90bf6..4655380ca 100644 --- a/example-new-architecture/ios/Podfile.lock +++ b/example-new-architecture/ios/Podfile.lock @@ -10,7 +10,7 @@ PODS: - DatadogInternal (= 3.1.0) - DatadogRUM (3.1.0): - DatadogInternal (= 3.1.0) - - DatadogSDKReactNative (2.12.1): + - DatadogSDKReactNative (2.13.0): - DatadogCore (= 3.1.0) - DatadogCrashReporting (= 3.1.0) - DatadogLogs (= 3.1.0) @@ -37,7 +37,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - DatadogSDKReactNative/Tests (2.12.1): + - DatadogSDKReactNative/Tests (2.13.0): - DatadogCore (= 3.1.0) - DatadogCrashReporting (= 3.1.0) - DatadogLogs (= 3.1.0) @@ -1855,7 +1855,7 @@ SPEC CHECKSUMS: DatadogInternal: 7837b2ce3d525d429682532eeda697b181299fdc DatadogLogs: 250894b5a99da5b924a019049c0d0326823cdbd6 DatadogRUM: 0d2a60e1abb8aacfb8827ef84f6d5deb4d5026c8 - DatadogSDKReactNative: 069ea9876220b2d09b0f4b180ce571b1b6ecbb35 + DatadogSDKReactNative: 2f11191b56e18680f633bfb125ab1832b327d9b4 DatadogTrace: f59e933074cd285ad7e9f5af991f8fe04b095991 DatadogWebViewTracking: 9bc92b4147aeed47eb1911451f651094aa6dd6c1 DoubleConversion: f16ae600a246532c4020132d54af21d0ddb2a385 diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index feb34e2a3..ca237868f 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -10,7 +10,7 @@ PODS: - DatadogInternal (= 3.1.0) - DatadogRUM (3.1.0): - DatadogInternal (= 3.1.0) - - DatadogSDKReactNative (2.12.1): + - DatadogSDKReactNative (2.13.0): - DatadogCore (= 3.1.0) - DatadogCrashReporting (= 3.1.0) - DatadogLogs (= 3.1.0) @@ -18,7 +18,7 @@ PODS: - DatadogTrace (= 3.1.0) - DatadogWebViewTracking (= 3.1.0) - React-Core - - DatadogSDKReactNative/Tests (2.12.1): + - DatadogSDKReactNative/Tests (2.13.0): - DatadogCore (= 3.1.0) - DatadogCrashReporting (= 3.1.0) - DatadogLogs (= 3.1.0) @@ -73,12 +73,12 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - DatadogSDKReactNativeWebView (2.12.1): + - DatadogSDKReactNativeWebView (2.13.0): - DatadogInternal (= 3.1.0) - DatadogSDKReactNative - DatadogWebViewTracking (= 3.1.0) - React-Core - - DatadogSDKReactNativeWebView/Tests (2.12.1): + - DatadogSDKReactNativeWebView/Tests (2.13.0): - DatadogInternal (= 3.1.0) - DatadogSDKReactNative - DatadogWebViewTracking (= 3.1.0) @@ -1993,9 +1993,9 @@ SPEC CHECKSUMS: DatadogInternal: 7837b2ce3d525d429682532eeda697b181299fdc DatadogLogs: 250894b5a99da5b924a019049c0d0326823cdbd6 DatadogRUM: 0d2a60e1abb8aacfb8827ef84f6d5deb4d5026c8 - DatadogSDKReactNative: af351a4e1ce08124c290c52de94b0062a166cc67 - DatadogSDKReactNativeSessionReplay: dcbd55d9d0f2b86026996a8b7ec9654922d5dfe1 - DatadogSDKReactNativeWebView: 096ac87eb753b6a217b93441983264b9837c3b7e + DatadogSDKReactNative: 822ff8092666172584d4d5e56f79c3799887d408 + DatadogSDKReactNativeSessionReplay: afc4e2b1db34ba8af3a442b0691359faaf5e586e + DatadogSDKReactNativeWebView: 00affefdaca0cf2375e669fa03925d8fa75263d0 DatadogSessionReplay: 6bc71888e2b41dd0de3325f06f0c0b3cee0e6df4 DatadogTrace: f59e933074cd285ad7e9f5af991f8fe04b095991 DatadogWebViewTracking: 9bc92b4147aeed47eb1911451f651094aa6dd6c1 diff --git a/packages/core/android/build.gradle b/packages/core/android/build.gradle index 59bb75cc3..080185d41 100644 --- a/packages/core/android/build.gradle +++ b/packages/core/android/build.gradle @@ -201,16 +201,16 @@ dependencies { // This breaks builds if the React Native target is below 0.76.0. as it relies on Gradle 8.5.0. // To avoid this, we enforce 1.0.0-beta01 on RN < 0.76.0 if (reactNativeMinorVersion < 76) { - implementation("com.datadoghq:dd-sdk-android-rum:3.1.0") { + implementation("com.datadoghq:dd-sdk-android-rum:3.2.0") { exclude group: "androidx.metrics", module: "metrics-performance" } implementation "androidx.metrics:metrics-performance:1.0.0-beta01" } else { - implementation "com.datadoghq:dd-sdk-android-rum:3.1.0" + implementation "com.datadoghq:dd-sdk-android-rum:3.2.0" } - implementation "com.datadoghq:dd-sdk-android-logs:3.1.0" - implementation "com.datadoghq:dd-sdk-android-trace:3.1.0" - implementation "com.datadoghq:dd-sdk-android-webview:3.1.0" + implementation "com.datadoghq:dd-sdk-android-logs:3.2.0" + implementation "com.datadoghq:dd-sdk-android-trace:3.2.0" + implementation "com.datadoghq:dd-sdk-android-webview:3.2.0" implementation "com.google.code.gson:gson:2.10.0" testImplementation "org.junit.platform:junit-platform-launcher:1.6.2" testImplementation "org.junit.jupiter:junit-jupiter-api:5.6.2" diff --git a/packages/react-native-session-replay/android/build.gradle b/packages/react-native-session-replay/android/build.gradle index f21f71cbb..f6a313a00 100644 --- a/packages/react-native-session-replay/android/build.gradle +++ b/packages/react-native-session-replay/android/build.gradle @@ -214,8 +214,8 @@ dependencies { api "com.facebook.react:react-android:$reactNativeVersion" } implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" - implementation "com.datadoghq:dd-sdk-android-session-replay:3.1.0" - implementation "com.datadoghq:dd-sdk-android-internal:3.1.0" + implementation "com.datadoghq:dd-sdk-android-session-replay:3.2.0" + implementation "com.datadoghq:dd-sdk-android-internal:3.2.0" implementation project(path: ':datadog_mobile-react-native') testImplementation "org.junit.platform:junit-platform-launcher:1.6.2" diff --git a/packages/react-native-webview/android/build.gradle b/packages/react-native-webview/android/build.gradle index 87ca1b7e7..dbb8d0593 100644 --- a/packages/react-native-webview/android/build.gradle +++ b/packages/react-native-webview/android/build.gradle @@ -190,7 +190,7 @@ dependencies { implementation "com.facebook.react:react-android:$reactNativeVersion" } - implementation "com.datadoghq:dd-sdk-android-webview:3.1.0" + implementation "com.datadoghq:dd-sdk-android-webview:3.2.0" implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation project(path: ':datadog_mobile-react-native') From 781b137da9ec4276380c4fded2a0147fa75b0d16 Mon Sep 17 00:00:00 2001 From: Sergio Barrio Date: Mon, 27 Oct 2025 16:28:37 +0100 Subject: [PATCH 048/410] Handle optional String on removeAttributes --- .../main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt index b18040ad7..6688f8061 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt @@ -115,8 +115,7 @@ class DdSdkImplementation( fun removeAttributes(keys: ReadableArray, promise: Promise) { val keysArray = mutableListOf() for (i in 0 until keys.size()) { - val key: String = keys.getString(i) - keysArray.add(key) + keys.getString(i)?.let { if (it.isNotBlank()) keysArray.add(it) } } val keysStringArray = keysArray.toTypedArray() From ba1f828904b4597fd7d27e7eb1a62a27748edd18 Mon Sep 17 00:00:00 2001 From: Sergio Barrio Date: Mon, 27 Oct 2025 11:37:44 +0100 Subject: [PATCH 049/410] Bump minSdkVersion to 23 --- packages/core/android/build.gradle | 14 +------------- packages/core/android/gradle.properties | 2 +- .../android/gradle.properties | 2 +- .../android/gradle.properties | 2 +- .../react-native-webview/android/gradle.properties | 2 +- 5 files changed, 5 insertions(+), 17 deletions(-) diff --git a/packages/core/android/build.gradle b/packages/core/android/build.gradle index 080185d41..1344b2531 100644 --- a/packages/core/android/build.gradle +++ b/packages/core/android/build.gradle @@ -195,19 +195,7 @@ dependencies { } implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" compileOnly "com.squareup.okhttp3:okhttp:3.12.13" - - // dd-sdk-android-rum requires androidx.metrics:metrics-performance. - // From 2.21.0, it uses 1.0.0-beta02, which requires Gradle 8.6.0. - // This breaks builds if the React Native target is below 0.76.0. as it relies on Gradle 8.5.0. - // To avoid this, we enforce 1.0.0-beta01 on RN < 0.76.0 - if (reactNativeMinorVersion < 76) { - implementation("com.datadoghq:dd-sdk-android-rum:3.2.0") { - exclude group: "androidx.metrics", module: "metrics-performance" - } - implementation "androidx.metrics:metrics-performance:1.0.0-beta01" - } else { - implementation "com.datadoghq:dd-sdk-android-rum:3.2.0" - } + implementation "com.datadoghq:dd-sdk-android-rum:3.2.0" implementation "com.datadoghq:dd-sdk-android-logs:3.2.0" implementation "com.datadoghq:dd-sdk-android-trace:3.2.0" implementation "com.datadoghq:dd-sdk-android-webview:3.2.0" diff --git a/packages/core/android/gradle.properties b/packages/core/android/gradle.properties index c9f7a205e..65f975c3e 100644 --- a/packages/core/android/gradle.properties +++ b/packages/core/android/gradle.properties @@ -1,5 +1,5 @@ DdSdkReactNative_kotlinVersion=1.8.21 -DdSdkReactNative_minSdkVersion=21 +DdSdkReactNative_minSdkVersion=23 DdSdkReactNative_compileSdkVersion=33 DdSdkReactNative_buildToolsVersion=33.0.0 DdSdkReactNative_targetSdkVersion=33 diff --git a/packages/internal-testing-tools/android/gradle.properties b/packages/internal-testing-tools/android/gradle.properties index 2a3186ab3..7a783c97d 100644 --- a/packages/internal-testing-tools/android/gradle.properties +++ b/packages/internal-testing-tools/android/gradle.properties @@ -1,5 +1,5 @@ DatadogInternalTesting_kotlinVersion=1.8.21 -DatadogInternalTesting_minSdkVersion=21 +DatadogInternalTesting_minSdkVersion=23 DatadogInternalTesting_compileSdkVersion=33 DatadogInternalTesting_buildToolsVersion=33.0.0 DatadogInternalTesting_targetSdkVersion=33 diff --git a/packages/react-native-session-replay/android/gradle.properties b/packages/react-native-session-replay/android/gradle.properties index 9f1573be5..d072f557e 100644 --- a/packages/react-native-session-replay/android/gradle.properties +++ b/packages/react-native-session-replay/android/gradle.properties @@ -1,6 +1,6 @@ DatadogSDKReactNativeSessionReplay_kotlinVersion=1.8.21 DatadogSDKReactNativeSessionReplay_compileSdkVersion=33 -DatadogSDKReactNativeSessionReplay_minSdkVersion=21 +DatadogSDKReactNativeSessionReplay_minSdkVersion=23 DatadogSDKReactNativeSessionReplay_buildToolsVersion=33.0.0 DatadogSDKReactNativeSessionReplay_targetSdkVersion=33 android.useAndroidX=true diff --git a/packages/react-native-webview/android/gradle.properties b/packages/react-native-webview/android/gradle.properties index 622c7b6b9..25112d024 100644 --- a/packages/react-native-webview/android/gradle.properties +++ b/packages/react-native-webview/android/gradle.properties @@ -1,5 +1,5 @@ DatadogSDKReactNativeWebView_kotlinVersion=1.7.21 -DatadogSDKReactNativeWebView_minSdkVersion=21 +DatadogSDKReactNativeWebView_minSdkVersion=23 DatadogSDKReactNativeWebView_compileSdkVersion=33 DatadogSDKReactNativeWebView_buildToolsVersion=33.0.0 DatadogSDKReactNativeWebView_targetSdkVersion=33 From 86cf5ab141edae8664d3e31657d196ad1e976cc4 Mon Sep 17 00:00:00 2001 From: Carlos Nogueira Date: Wed, 5 Nov 2025 15:58:05 +0000 Subject: [PATCH 050/410] Expose sdk iOS config option `trackMemoryWarnings` --- packages/core/ios/Sources/DdSdkConfiguration.swift | 5 ++++- .../ios/Sources/DdSdkNativeInitialization.swift | 1 + .../core/ios/Sources/RNDdSdkConfiguration.swift | 9 +++++++-- packages/core/src/DdSdkReactNative.tsx | 3 ++- .../core/src/DdSdkReactNativeConfiguration.tsx | 14 +++++++++++++- .../DdSdkReactNativeConfiguration.test.ts | 3 +++ .../__tests__/initialization.test.tsx | 1 + .../__tests__/FileBasedConfiguration.test.ts | 3 +++ packages/core/src/types.tsx | 3 ++- 9 files changed, 36 insertions(+), 6 deletions(-) diff --git a/packages/core/ios/Sources/DdSdkConfiguration.swift b/packages/core/ios/Sources/DdSdkConfiguration.swift index 7a76cf39d..288e2a539 100644 --- a/packages/core/ios/Sources/DdSdkConfiguration.swift +++ b/packages/core/ios/Sources/DdSdkConfiguration.swift @@ -76,6 +76,7 @@ public class DdSdkConfiguration: NSObject { public var trackWatchdogTerminations: Bool public var batchProcessingLevel: Datadog.Configuration.BatchProcessingLevel public var initialResourceThreshold: Double? = nil + public var trackMemoryWarnings: Bool public init( clientToken: String, @@ -108,7 +109,8 @@ public class DdSdkConfiguration: NSObject { appHangThreshold: Double?, trackWatchdogTerminations: Bool, batchProcessingLevel: Datadog.Configuration.BatchProcessingLevel, - initialResourceThreshold: Double? + initialResourceThreshold: Double?, + trackMemoryWarnings: Bool = true ) { self.clientToken = clientToken self.env = env @@ -141,6 +143,7 @@ public class DdSdkConfiguration: NSObject { self.trackWatchdogTerminations = trackWatchdogTerminations self.batchProcessingLevel = batchProcessingLevel self.initialResourceThreshold = initialResourceThreshold + self.trackMemoryWarnings = trackMemoryWarnings } } diff --git a/packages/core/ios/Sources/DdSdkNativeInitialization.swift b/packages/core/ios/Sources/DdSdkNativeInitialization.swift index 7b0f1be19..bf39f5e97 100644 --- a/packages/core/ios/Sources/DdSdkNativeInitialization.swift +++ b/packages/core/ios/Sources/DdSdkNativeInitialization.swift @@ -188,6 +188,7 @@ public class DdSdkNativeInitialization: NSObject { }, onSessionStart: DdSdkSessionStartedListener.instance.rumSessionListener, customEndpoint: customRUMEndpointURL, + trackMemoryWarnings: configuration.trackMemoryWarnings, telemetrySampleRate: (configuration.telemetrySampleRate as? NSNumber)?.floatValue ?? Float(DefaultConfiguration.telemetrySampleRate) ) } diff --git a/packages/core/ios/Sources/RNDdSdkConfiguration.swift b/packages/core/ios/Sources/RNDdSdkConfiguration.swift index 66437c481..6869d1711 100644 --- a/packages/core/ios/Sources/RNDdSdkConfiguration.swift +++ b/packages/core/ios/Sources/RNDdSdkConfiguration.swift @@ -43,6 +43,7 @@ extension NSDictionary { let trackWatchdogTerminations = object(forKey: "trackWatchdogTerminations") as? Bool let batchProcessingLevel = object(forKey: "batchProcessingLevel") as? NSString let initialResourceThreshold = object(forKey: "initialResourceThreshold") as? Double + let trackMemoryWarnings = object(forKey: "trackMemoryWarnings") as? Bool return DdSdkConfiguration( clientToken: (clientToken != nil) ? clientToken! : String(), @@ -75,7 +76,8 @@ extension NSDictionary { appHangThreshold: appHangThreshold, trackWatchdogTerminations: trackWatchdogTerminations ?? DefaultConfiguration.trackWatchdogTerminations, batchProcessingLevel: batchProcessingLevel.asBatchProcessingLevel(), - initialResourceThreshold: initialResourceThreshold + initialResourceThreshold: initialResourceThreshold, + trackMemoryWarnings: trackMemoryWarnings ?? DefaultConfiguration.trackMemoryWarnings ) } @@ -206,6 +208,7 @@ internal struct DefaultConfiguration { static let bundleLogsWithRum = true static let bundleLogsWithTraces = true static let trackWatchdogTerminations = false + static let trackMemoryWarnings = true } extension Dictionary where Key == String, Value == AnyObject { @@ -244,6 +247,7 @@ extension Dictionary where Key == String, Value == AnyObject { let trackWatchdogTerminations = configuration["trackWatchdogTerminations"] as? Bool let batchProcessingLevel = configuration["batchProcessingLevel"] as? NSString let initialResourceThreshold = configuration["initialResourceThreshold"] as? Double + let trackMemoryWarnings = configuration["trackMemoryWarnings"] as? Bool return DdSdkConfiguration( clientToken: clientToken ?? String(), @@ -279,7 +283,8 @@ extension Dictionary where Key == String, Value == AnyObject { appHangThreshold: appHangThreshold, trackWatchdogTerminations: trackWatchdogTerminations ?? DefaultConfiguration.trackWatchdogTerminations, batchProcessingLevel: batchProcessingLevel.asBatchProcessingLevel(), - initialResourceThreshold: initialResourceThreshold + initialResourceThreshold: initialResourceThreshold, + trackMemoryWarnings: trackMemoryWarnings ?? DefaultConfiguration.trackMemoryWarnings ) } } diff --git a/packages/core/src/DdSdkReactNative.tsx b/packages/core/src/DdSdkReactNative.tsx index 8360a695b..b1ea4f4c1 100644 --- a/packages/core/src/DdSdkReactNative.tsx +++ b/packages/core/src/DdSdkReactNative.tsx @@ -397,7 +397,8 @@ export class DdSdkReactNative { configuration.resourceTracingSamplingRate, configuration.trackWatchdogTerminations, configuration.batchProcessingLevel, - configuration.initialResourceThreshold + configuration.initialResourceThreshold, + configuration.trackMemoryWarnings ); }; diff --git a/packages/core/src/DdSdkReactNativeConfiguration.tsx b/packages/core/src/DdSdkReactNativeConfiguration.tsx index 44debb2d2..4ec1ef883 100644 --- a/packages/core/src/DdSdkReactNativeConfiguration.tsx +++ b/packages/core/src/DdSdkReactNativeConfiguration.tsx @@ -133,7 +133,8 @@ export const DEFAULTS = { bundleLogsWithTraces: true, useAccessibilityLabel: true, trackWatchdogTerminations: false, - batchProcessingLevel: BatchProcessingLevel.MEDIUM + batchProcessingLevel: BatchProcessingLevel.MEDIUM, + trackMemoryWarnings: true }; /** @@ -328,6 +329,16 @@ export class DdSdkReactNativeConfiguration { public trackWatchdogTerminations: boolean = DEFAULTS.trackWatchdogTerminations; + /** + * Enables tracking of memory warnings as RUM events. + * + * When enabled, the SDK will automatically record a RUM event each time the app + * receives a memory warning from the operating system. + * + * **Note:** This setting is only supported on **iOS**. It has no effect on other platforms. + */ + public trackMemoryWarnings: boolean = DEFAULTS.trackMemoryWarnings; + /** * Specifies a custom prop to name RUM actions on elements having an `onPress` prop. * @@ -469,6 +480,7 @@ export type PartialInitializationConfiguration = { readonly bundleLogsWithTraces?: boolean; readonly batchProcessingLevel?: BatchProcessingLevel; readonly initialResourceThreshold?: number; + readonly trackMemoryWarnings?: boolean; }; const setConfigurationAttribute = < diff --git a/packages/core/src/__tests__/DdSdkReactNativeConfiguration.test.ts b/packages/core/src/__tests__/DdSdkReactNativeConfiguration.test.ts index b1522ef7f..60a9d13ff 100644 --- a/packages/core/src/__tests__/DdSdkReactNativeConfiguration.test.ts +++ b/packages/core/src/__tests__/DdSdkReactNativeConfiguration.test.ts @@ -60,6 +60,7 @@ describe('DdSdkReactNativeConfiguration', () => { "trackErrors": false, "trackFrustrations": true, "trackInteractions": false, + "trackMemoryWarnings": true, "trackResources": false, "trackWatchdogTerminations": false, "trackingConsent": "granted", @@ -170,6 +171,7 @@ describe('DdSdkReactNativeConfiguration', () => { "trackErrors": true, "trackFrustrations": true, "trackInteractions": true, + "trackMemoryWarnings": true, "trackResources": true, "trackWatchdogTerminations": false, "trackingConsent": "pending", @@ -246,6 +248,7 @@ describe('DdSdkReactNativeConfiguration', () => { "trackErrors": false, "trackFrustrations": false, "trackInteractions": false, + "trackMemoryWarnings": true, "trackResources": false, "trackWatchdogTerminations": false, "trackingConsent": "granted", diff --git a/packages/core/src/sdk/DatadogProvider/__tests__/initialization.test.tsx b/packages/core/src/sdk/DatadogProvider/__tests__/initialization.test.tsx index c654cd24b..8ace4b731 100644 --- a/packages/core/src/sdk/DatadogProvider/__tests__/initialization.test.tsx +++ b/packages/core/src/sdk/DatadogProvider/__tests__/initialization.test.tsx @@ -100,6 +100,7 @@ describe('DatadogProvider', () => { "telemetrySampleRate": 20, "trackBackgroundEvents": false, "trackFrustrations": true, + "trackMemoryWarnings": true, "trackNonFatalAnrs": undefined, "trackWatchdogTerminations": false, "trackingConsent": "granted", diff --git a/packages/core/src/sdk/FileBasedConfiguration/__tests__/FileBasedConfiguration.test.ts b/packages/core/src/sdk/FileBasedConfiguration/__tests__/FileBasedConfiguration.test.ts index 6d3ee2e44..1670e5fe2 100644 --- a/packages/core/src/sdk/FileBasedConfiguration/__tests__/FileBasedConfiguration.test.ts +++ b/packages/core/src/sdk/FileBasedConfiguration/__tests__/FileBasedConfiguration.test.ts @@ -62,6 +62,7 @@ describe('FileBasedConfiguration', () => { "trackErrors": true, "trackFrustrations": true, "trackInteractions": true, + "trackMemoryWarnings": true, "trackResources": true, "trackWatchdogTerminations": false, "trackingConsent": "not_granted", @@ -154,6 +155,7 @@ describe('FileBasedConfiguration', () => { "trackErrors": true, "trackFrustrations": true, "trackInteractions": true, + "trackMemoryWarnings": true, "trackResources": true, "trackWatchdogTerminations": false, "trackingConsent": "not_granted", @@ -205,6 +207,7 @@ describe('FileBasedConfiguration', () => { "trackErrors": false, "trackFrustrations": true, "trackInteractions": false, + "trackMemoryWarnings": true, "trackResources": false, "trackWatchdogTerminations": false, "trackingConsent": "granted", diff --git a/packages/core/src/types.tsx b/packages/core/src/types.tsx index c8d9821cc..5c7d64cec 100644 --- a/packages/core/src/types.tsx +++ b/packages/core/src/types.tsx @@ -69,7 +69,8 @@ export class DdSdkConfiguration { readonly resourceTracingSamplingRate: number, readonly trackWatchdogTerminations: boolean | undefined, readonly batchProcessingLevel: BatchProcessingLevel, // eslint-disable-next-line no-empty-function - readonly initialResourceThreshold: number | undefined + readonly initialResourceThreshold: number | undefined, + readonly trackMemoryWarnings: boolean ) {} } From f351c697d6d8c61d44c08d4ec9b18f89242b92c0 Mon Sep 17 00:00:00 2001 From: Sergio Barrio Date: Tue, 18 Nov 2025 09:57:58 +0100 Subject: [PATCH 051/410] Remove defaultPrivacyLevel from Session Replay --- ...lemetryConfigurationEventForgeryFactory.kt | 1 - .../src/SessionReplay.ts | 41 +------------------ .../src/__tests__/SessionReplay.test.ts | 38 +---------------- 3 files changed, 2 insertions(+), 78 deletions(-) diff --git a/packages/core/android/src/test/kotlin/com/datadog/tools/unit/forge/TelemetryConfigurationEventForgeryFactory.kt b/packages/core/android/src/test/kotlin/com/datadog/tools/unit/forge/TelemetryConfigurationEventForgeryFactory.kt index 10e894e6e..684d92c21 100644 --- a/packages/core/android/src/test/kotlin/com/datadog/tools/unit/forge/TelemetryConfigurationEventForgeryFactory.kt +++ b/packages/core/android/src/test/kotlin/com/datadog/tools/unit/forge/TelemetryConfigurationEventForgeryFactory.kt @@ -86,7 +86,6 @@ internal class TelemetryConfigurationEventForgeryFactory : ) } }, - defaultPrivacyLevel = forge.aNullable { aString() }, enablePrivacyForActionName = forge.aNullable { aBool() }, useExcludedActivityUrls = forge.aNullable { aBool() }, useWorkerUrl = forge.aNullable { aBool() }, diff --git a/packages/react-native-session-replay/src/SessionReplay.ts b/packages/react-native-session-replay/src/SessionReplay.ts index 60d4e8ff8..516b8ffb4 100644 --- a/packages/react-native-session-replay/src/SessionReplay.ts +++ b/packages/react-native-session-replay/src/SessionReplay.ts @@ -95,15 +95,6 @@ export interface SessionReplayConfiguration { * Default: `true`. */ startRecordingImmediately?: boolean; - - /** - * Defines the way sensitive content (e.g. text) should be masked. - * - * Default `SessionReplayPrivacy.MASK`. - * @deprecated Use {@link imagePrivacyLevel}, {@link touchPrivacyLevel} and {@link textAndInputPrivacyLevel} instead. - * Note: setting this property (`defaultPrivacyLevel`) will override the individual privacy levels. - */ - defaultPrivacyLevel?: SessionReplayPrivacy; } type InternalBaseSessionReplayConfiguration = { @@ -121,11 +112,8 @@ type InternalPrivacySessionReplayConfiguration = { type InternalSessionReplayConfiguration = InternalBaseSessionReplayConfiguration & InternalPrivacySessionReplayConfiguration; -const DEFAULTS: InternalSessionReplayConfiguration & { - defaultPrivacyLevel: SessionReplayPrivacy; -} = { +const DEFAULTS: InternalSessionReplayConfiguration = { replaySampleRate: 100, - defaultPrivacyLevel: SessionReplayPrivacy.MASK, customEndpoint: '', imagePrivacyLevel: ImagePrivacyLevel.MASK_ALL, touchPrivacyLevel: TouchPrivacyLevel.HIDE, @@ -175,33 +163,6 @@ export class SessionReplayWrapper { DEFAULTS.textAndInputPrivacyLevel }; - // Legacy Default Privacy Level property handling - if (configuration.defaultPrivacyLevel) { - switch (configuration.defaultPrivacyLevel) { - case SessionReplayPrivacy.MASK: - privacyConfig.imagePrivacyLevel = - ImagePrivacyLevel.MASK_ALL; - privacyConfig.touchPrivacyLevel = TouchPrivacyLevel.HIDE; - privacyConfig.textAndInputPrivacyLevel = - TextAndInputPrivacyLevel.MASK_ALL; - break; - case SessionReplayPrivacy.MASK_USER_INPUT: - privacyConfig.imagePrivacyLevel = - ImagePrivacyLevel.MASK_NONE; - privacyConfig.touchPrivacyLevel = TouchPrivacyLevel.HIDE; - privacyConfig.textAndInputPrivacyLevel = - TextAndInputPrivacyLevel.MASK_ALL_INPUTS; - break; - case SessionReplayPrivacy.ALLOW: - privacyConfig.imagePrivacyLevel = - ImagePrivacyLevel.MASK_NONE; - privacyConfig.touchPrivacyLevel = TouchPrivacyLevel.SHOW; - privacyConfig.textAndInputPrivacyLevel = - TextAndInputPrivacyLevel.MASK_SENSITIVE_INPUTS; - break; - } - } - return { ...baseConfig, ...privacyConfig }; }; diff --git a/packages/react-native-session-replay/src/__tests__/SessionReplay.test.ts b/packages/react-native-session-replay/src/__tests__/SessionReplay.test.ts index c755fc6e2..43f449cf7 100644 --- a/packages/react-native-session-replay/src/__tests__/SessionReplay.test.ts +++ b/packages/react-native-session-replay/src/__tests__/SessionReplay.test.ts @@ -9,7 +9,6 @@ import { NativeModules } from 'react-native'; import { ImagePrivacyLevel, SessionReplay, - SessionReplayPrivacy, TextAndInputPrivacyLevel, TouchPrivacyLevel } from '../SessionReplay'; @@ -41,27 +40,9 @@ describe('SessionReplay', () => { ); }); - it('calls native session replay with provided configuration { w defaultPrivacyLevel = ALLOW }', () => { + it('calls native session replay with provided configuration { w custom endpoint }', () => { SessionReplay.enable({ replaySampleRate: 100, - defaultPrivacyLevel: SessionReplayPrivacy.ALLOW, - customEndpoint: 'https://session-replay.example.com' - }); - - expect(NativeModules.DdSessionReplay.enable).toHaveBeenCalledWith( - 100, - 'https://session-replay.example.com', - 'MASK_NONE', - 'SHOW', - 'MASK_SENSITIVE_INPUTS', - true - ); - }); - - it('calls native session replay with provided configuration { w defaultPrivacyLevel = MASK }', () => { - SessionReplay.enable({ - replaySampleRate: 100, - defaultPrivacyLevel: SessionReplayPrivacy.MASK, customEndpoint: 'https://session-replay.example.com' }); @@ -75,23 +56,6 @@ describe('SessionReplay', () => { ); }); - it('calls native session replay with provided configuration { w defaultPrivacyLevel = MASK_USER_INPUT }', () => { - SessionReplay.enable({ - replaySampleRate: 100, - defaultPrivacyLevel: SessionReplayPrivacy.MASK_USER_INPUT, - customEndpoint: 'https://session-replay.example.com' - }); - - expect(NativeModules.DdSessionReplay.enable).toHaveBeenCalledWith( - 100, - 'https://session-replay.example.com', - 'MASK_NONE', - 'HIDE', - 'MASK_ALL_INPUTS', - true - ); - }); - it('calls native session replay with provided configuration { w random privacy levels }', () => { const TIMES = 20; From 2bedb0d2a87b714f7d27228f706d3e6360c2f811 Mon Sep 17 00:00:00 2001 From: Carlos Nogueira Date: Mon, 3 Nov 2025 18:05:04 +0000 Subject: [PATCH 052/410] Expose `setAccountInfo` API to JS layer add account info to `applyEventMapper` --- .../datadog/reactnative/DatadogSDKWrapper.kt | 20 ++++- .../com/datadog/reactnative/DatadogWrapper.kt | 27 ++++++ .../reactnative/DdSdkImplementation.kt | 41 +++++++++ .../kotlin/com/datadog/reactnative/DdSdk.kt | 26 ++++++ .../kotlin/com/datadog/reactnative/DdSdk.kt | 26 ++++++ packages/core/ios/Sources/DdSdk.mm | 32 +++++++ .../ios/Sources/DdSdkImplementation.swift | 38 ++++++++ packages/core/jest/mock.js | 9 ++ packages/core/src/DdSdkReactNative.tsx | 66 ++++++++++++++ .../AccountInfoSingleton.ts | 46 ++++++++++ .../__tests__/AccountInfoSingleton.test.ts | 90 +++++++++++++++++++ .../src/sdk/AccountInfoSingleton/types.ts | 11 +++ .../core/src/sdk/EventMappers/EventMapper.ts | 5 ++ packages/core/src/specs/NativeDdSdk.ts | 17 ++++ packages/core/src/types.tsx | 27 ++++++ 15 files changed, 480 insertions(+), 1 deletion(-) create mode 100644 packages/core/src/sdk/AccountInfoSingleton/AccountInfoSingleton.ts create mode 100644 packages/core/src/sdk/AccountInfoSingleton/__tests__/AccountInfoSingleton.test.ts create mode 100644 packages/core/src/sdk/AccountInfoSingleton/types.ts diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogSDKWrapper.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogSDKWrapper.kt index 06151d834..56a15373c 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogSDKWrapper.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogSDKWrapper.kt @@ -89,7 +89,25 @@ internal class DatadogSDKWrapper : DatadogWrapper { override fun clearUserInfo() { Datadog.clearUserInfo() } - + + override fun setAccountInfo( + id: String, + name: String?, + extraInfo: Map + ) { + Datadog.setAccountInfo(id, name, extraInfo) + } + + override fun addAccountExtraInfo( + extraInfo: Map + ) { + Datadog.addAccountExtraInfo(extraInfo) + } + + override fun clearAccountInfo() { + Datadog.clearAccountInfo() + } + override fun addRumGlobalAttribute(key: String, value: Any?) { this.getRumMonitor().addAttribute(key, value) } diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogWrapper.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogWrapper.kt index d6395b18b..c72f2faef 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogWrapper.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogWrapper.kt @@ -91,6 +91,33 @@ interface DatadogWrapper { */ fun clearUserInfo() + /** + * Sets the account information. + * + * @param id a unique account identifier (relevant to your business domain) + * @param name (nullable) the account name + * @param extraInfo additional information. An extra information can be + * nested up to 8 levels deep. Keys using more than 8 levels will be sanitized by SDK. + */ + fun setAccountInfo( + id: String, + name: String?, + extraInfo: Map + ) + + /** + * Sets the account information. + * @param extraInfo: The additional information. (To set the id or name please use setAccountInfo). + */ + fun addAccountExtraInfo( + extraInfo: Map + ) + + /** + * Clears the account information. + */ + fun clearAccountInfo() + /** Adds a global attribute. * diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt index 6688f8061..9264fafa0 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt @@ -168,6 +168,47 @@ class DdSdkImplementation( promise.resolve(null) } + /** + * Set the account information. + * @param accountInfo The account object (use builtin attributes: 'id', 'name', and any custom + * attribute inside 'extraInfo'). + */ + fun setAccountInfo(accountInfo: ReadableMap, promise: Promise) { + val accountInfoMap = accountInfo.toHashMap().toMutableMap() + val id = accountInfoMap["id"] as? String + val name = accountInfoMap["name"] as? String + val extraInfo = (accountInfoMap["extraInfo"] as? Map<*, *>)?.filterKeys { it is String } + ?.mapKeys { it.key as String } + ?.mapValues { it.value } ?: emptyMap() + + if (id != null) { + datadog.setAccountInfo(id, name, extraInfo) + } + + promise.resolve(null) + } + + /** + * Sets the account extra information. + * @param accountExtraInfo: The additional information. (To set the id or name please use setAccountInfo). + */ + fun addAccountExtraInfo( + accountExtraInfo: ReadableMap, promise: Promise + ) { + val extraInfoMap = accountExtraInfo.toHashMap().toMutableMap() + + datadog.addAccountExtraInfo(extraInfoMap) + promise.resolve(null) + } + + /** + * Clears the account information. + */ + fun clearAccountInfo(promise: Promise) { + datadog.clearAccountInfo() + promise.resolve(null) + } + /** * Set the tracking consent regarding the data collection. * @param trackingConsent Consent, which can take one of the following values: 'pending', diff --git a/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdSdk.kt b/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdSdk.kt index a9d430081..421812545 100644 --- a/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdSdk.kt +++ b/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdSdk.kt @@ -106,6 +106,32 @@ class DdSdk( implementation.clearUserInfo(promise) } + /** + * Set the account information. + * @param account The account object (use builtin attributes: 'id', 'name', and any custom * attribute inside 'extraInfo'). + */ + @ReactMethod + override fun setAccountInfo(account: ReadableMap, promise: Promise) { + implementation.setAccountInfo(account, promise) + } + + /** + * Sets the account information. + * @param extraAccountInfo: The additional information. (To set the id or name please use setAccountInfo). + */ + @ReactMethod + override fun addAccountExtraInfo(extraInfo: ReadableMap, promise: Promise) { + implementation.addAccountExtraInfo(extraInfo, promise) + } + + /** + * Clears the account information. + */ + @ReactMethod + override fun clearAccountInfo(promise: Promise) { + implementation.clearAccountInfo(promise) + } + /** * Set the tracking consent regarding the data collection. * @param trackingConsent Consent, which can take one of the following values: 'pending', diff --git a/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt b/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt index 958ba521b..ef91ca549 100644 --- a/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt +++ b/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt @@ -132,6 +132,32 @@ class DdSdk( implementation.clearUserInfo(promise) } + /** + * Set the account information. + * @param account The account object (use builtin attributes: 'id', 'name', and any custom * attribute inside 'extraInfo'). + */ + @ReactMethod + fun setAccountInfo(account: ReadableMap, promise: Promise) { + implementation.setAccountInfo(account, promise) + } + + /** + * Sets the account information. + * @param extraAccountInfo: The additional information. (To set the id or name please use setAccountInfo). + */ + @ReactMethod + fun addAccountExtraInfo(extraInfo: ReadableMap, promise: Promise) { + implementation.addAccountExtraInfo(extraInfo, promise) + } + + /** + * Clears the account information. + */ + @ReactMethod + fun clearAccountInfo(promise: Promise) { + implementation.clearAccountInfo(promise) + } + /** * Set the tracking consent regarding the data collection. * @param trackingConsent Consent, which can take one of the following values: 'pending', diff --git a/packages/core/ios/Sources/DdSdk.mm b/packages/core/ios/Sources/DdSdk.mm index 7129d7af1..489210503 100644 --- a/packages/core/ios/Sources/DdSdk.mm +++ b/packages/core/ios/Sources/DdSdk.mm @@ -79,6 +79,26 @@ + (void)initFromNative { [self clearUserInfo:resolve reject:reject]; } +RCT_REMAP_METHOD(setAccountInfo, withAccountInfo:(NSDictionary*)accountInfo + withResolver:(RCTPromiseResolveBlock)resolve + withRejecter:(RCTPromiseRejectBlock)reject) +{ + [self setAccountInfo:accountInfo resolve:resolve reject:reject]; +} + +RCT_REMAP_METHOD(addAccountExtraInfo, withAccountExtraInfo:(NSDictionary*)extraInfo + withResolver:(RCTPromiseResolveBlock)resolve + withRejecter:(RCTPromiseRejectBlock)reject) +{ + [self addAccountExtraInfo:extraInfo resolve:resolve reject:reject]; +} + +RCT_EXPORT_METHOD(clearAccountInfo:(RCTPromiseResolveBlock)resolve + withRejecter:(RCTPromiseRejectBlock)reject) +{ + [self clearAccountInfo:resolve reject:reject]; +} + RCT_REMAP_METHOD(setTrackingConsent, withTrackingConsent:(NSString*)trackingConsent withResolver:(RCTPromiseResolveBlock)resolve withRejecter:(RCTPromiseRejectBlock)reject) @@ -189,6 +209,18 @@ -(void)addUserExtraInfo:(NSDictionary *)extraInfo resolve:(RCTPromiseResolveBloc [self.ddSdkImplementation addUserExtraInfoWithExtraInfo:extraInfo resolve:resolve reject:reject]; } +- (void)setAccountInfo:(NSDictionary *)accountInfo resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + [self.ddSdkImplementation setAccountInfoWithAccountInfo:accountInfo resolve:resolve reject:reject]; +} + +- (void)clearAccountInfo:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + [self.ddSdkImplementation clearAccountInfoWithResolve:resolve reject:reject]; +} + +-(void)addAccountExtraInfo:(NSDictionary *)extraInfo resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + [self.ddSdkImplementation addAccountExtraInfoWithExtraInfo:extraInfo resolve:resolve reject:reject]; +} + - (void)sendTelemetryLog:(NSString *)message attributes:(NSDictionary *)attributes config:(NSDictionary *)config resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { [self.ddSdkImplementation sendTelemetryLogWithMessage:message attributes:attributes config:config resolve:resolve reject:reject]; } diff --git a/packages/core/ios/Sources/DdSdkImplementation.swift b/packages/core/ios/Sources/DdSdkImplementation.swift index 87fe91729..9ea820858 100644 --- a/packages/core/ios/Sources/DdSdkImplementation.swift +++ b/packages/core/ios/Sources/DdSdkImplementation.swift @@ -153,6 +153,44 @@ public class DdSdkImplementation: NSObject { resolve(nil) } + @objc + public func setAccountInfo( + accountInfo: NSDictionary, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock + ) { + let castedAccountInfo = castAttributesToSwift(accountInfo) + let id = castedAccountInfo["id"] as? String + let name = castedAccountInfo["name"] as? String + var extraInfo: [AttributeKey: AttributeValue] = [:] + + if let extraInfoEncodable = castedAccountInfo["extraInfo"] as? AnyEncodable, + let extraInfoDict = extraInfoEncodable.value as? [String: Any] + { + extraInfo = castAttributesToSwift(extraInfoDict) + } + + if let validId = id { + Datadog.setAccountInfo(id: validId, name: name, extraInfo: extraInfo) + } + + resolve(nil) + } + + @objc + public func addAccountExtraInfo( + extraInfo: NSDictionary, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock + ) { + let castedExtraInfo = castAttributesToSwift(extraInfo) + + Datadog.addAccountExtraInfo(castedExtraInfo) + resolve(nil) + } + + @objc + public func clearAccountInfo(resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) { + Datadog.clearAccountInfo() + resolve(nil) + } + @objc public func setTrackingConsent( trackingConsent: NSString, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock diff --git a/packages/core/jest/mock.js b/packages/core/jest/mock.js index 0dc9ec138..9f5ee2c41 100644 --- a/packages/core/jest/mock.js +++ b/packages/core/jest/mock.js @@ -36,6 +36,15 @@ module.exports = { clearUserInfo: jest .fn() .mockImplementation(() => new Promise(resolve => resolve())), + setAccountInfo: jest + .fn() + .mockImplementation(() => new Promise(resolve => resolve())), + addAccountExtraInfo: jest + .fn() + .mockImplementation(() => new Promise(resolve => resolve())), + clearAccountInfo: jest + .fn() + .mockImplementation(() => new Promise(resolve => resolve())), addAttribute: jest .fn() .mockImplementation(() => new Promise(resolve => resolve())), diff --git a/packages/core/src/DdSdkReactNative.tsx b/packages/core/src/DdSdkReactNative.tsx index b1ea4f4c1..e07ba4e34 100644 --- a/packages/core/src/DdSdkReactNative.tsx +++ b/packages/core/src/DdSdkReactNative.tsx @@ -30,6 +30,7 @@ import { DdRumErrorTracking } from './rum/instrumentation/DdRumErrorTracking'; import { DdBabelInteractionTracking } from './rum/instrumentation/interactionTracking/DdBabelInteractionTracking'; import { DdRumUserInteractionTracking } from './rum/instrumentation/interactionTracking/DdRumUserInteractionTracking'; import { DdRumResourceTracking } from './rum/instrumentation/resourceTracking/DdRumResourceTracking'; +import { AccountInfoSingleton } from './sdk/AccountInfoSingleton/AccountInfoSingleton'; import { AttributesSingleton } from './sdk/AttributesSingleton/AttributesSingleton'; import type { Attributes } from './sdk/AttributesSingleton/types'; import { registerNativeBridge } from './sdk/DatadogInternalBridge/DdSdkInternalNativeBridge'; @@ -299,6 +300,71 @@ export class DdSdkReactNative { UserInfoSingleton.getInstance().setUserInfo(updatedUserInfo); }; + /** + * Sets the account information. + * @param id: A mandatory unique account identifier (relevant to your business domain). + * @param name: The account name. + * @param extraInfo: Additional information. + * @returns a Promise. + */ + static setAccountInfo = async (accountInfo: { + id: string; + name?: string; + extraInfo?: Record; + }): Promise => { + InternalLog.log( + `Setting account ${JSON.stringify(accountInfo)}`, + SdkVerbosity.DEBUG + ); + + await DdSdk.setAccountInfo(accountInfo); + AccountInfoSingleton.getInstance().setAccountInfo(accountInfo); + }; + + /** + * Clears the account information. + * @returns a Promise. + */ + static clearAccountInfo = async (): Promise => { + InternalLog.log('Clearing account info', SdkVerbosity.DEBUG); + await DdSdk.clearAccountInfo(); + AccountInfoSingleton.getInstance().clearAccountInfo(); + }; + + /** + * Set the account information. + * @param extraAccountInfo: The additional information. (To set the id or name please use setAccountInfo). + * @returns a Promise. + */ + static addAccountExtraInfo = async ( + extraAccountInfo: Record + ): Promise => { + InternalLog.log( + `Adding extra account info ${JSON.stringify(extraAccountInfo)}`, + SdkVerbosity.DEBUG + ); + + const accountInfo = AccountInfoSingleton.getInstance().getAccountInfo(); + if (!accountInfo) { + InternalLog.log( + 'Skipped adding Account Extra Info: Account Info is currently undefined. An account ID must be set before adding extra info. Please call setAccountInfo() first.', + SdkVerbosity.WARN + ); + + return; + } + + const extraInfo = { + ...accountInfo.extraInfo, + ...extraAccountInfo + }; + + await DdSdk.addAccountExtraInfo(extraInfo); + AccountInfoSingleton.getInstance().addAccountExtraInfo( + extraAccountInfo + ); + }; + /** * Set the tracking consent regarding the data collection. * @param trackingConsent: One of TrackingConsent values. diff --git a/packages/core/src/sdk/AccountInfoSingleton/AccountInfoSingleton.ts b/packages/core/src/sdk/AccountInfoSingleton/AccountInfoSingleton.ts new file mode 100644 index 000000000..5f2be8dea --- /dev/null +++ b/packages/core/src/sdk/AccountInfoSingleton/AccountInfoSingleton.ts @@ -0,0 +1,46 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +import type { AccountInfo } from './types'; + +class AccountInfoProvider { + private accountInfo: AccountInfo | undefined = undefined; + + setAccountInfo = (accountInfo: AccountInfo) => { + this.accountInfo = accountInfo; + }; + + addAccountExtraInfo = (extraInfo: AccountInfo['extraInfo']) => { + if (!this.accountInfo) { + return; + } + + this.accountInfo.extraInfo = { + ...this.accountInfo.extraInfo, + ...extraInfo + }; + }; + + getAccountInfo = (): AccountInfo | undefined => { + return this.accountInfo; + }; + + clearAccountInfo = () => { + this.accountInfo = undefined; + }; +} + +export class AccountInfoSingleton { + private static accountInfoProvider = new AccountInfoProvider(); + + static getInstance = (): AccountInfoProvider => { + return AccountInfoSingleton.accountInfoProvider; + }; + + static reset = () => { + AccountInfoSingleton.accountInfoProvider = new AccountInfoProvider(); + }; +} diff --git a/packages/core/src/sdk/AccountInfoSingleton/__tests__/AccountInfoSingleton.test.ts b/packages/core/src/sdk/AccountInfoSingleton/__tests__/AccountInfoSingleton.test.ts new file mode 100644 index 000000000..9af387619 --- /dev/null +++ b/packages/core/src/sdk/AccountInfoSingleton/__tests__/AccountInfoSingleton.test.ts @@ -0,0 +1,90 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +import { AccountInfoSingleton } from '../AccountInfoSingleton'; + +describe('AccountInfoSingleton', () => { + beforeEach(() => { + AccountInfoSingleton.reset(); + }); + + it('returns undefined by default', () => { + expect( + AccountInfoSingleton.getInstance().getAccountInfo() + ).toBeUndefined(); + }); + + it('stores and returns account info after `setAccountInfo`', () => { + const info = { + id: 'test', + name: 'test user', + extraInfo: { premium: true } + }; + + AccountInfoSingleton.getInstance().setAccountInfo(info); + + expect(AccountInfoSingleton.getInstance().getAccountInfo()).toEqual( + info + ); + }); + + it('adds extra account info with `addAccountExtraInfo`', () => { + const info = { + id: 'test', + name: 'test user', + extraInfo: { premium: true } + }; + + AccountInfoSingleton.getInstance().setAccountInfo(info); + AccountInfoSingleton.getInstance().addAccountExtraInfo({ + testGroup: 'A' + }); + + expect(AccountInfoSingleton.getInstance().getAccountInfo()).toEqual({ + ...info, + extraInfo: { ...info.extraInfo, testGroup: 'A' } + }); + }); + + it('clears account info with `clearAccountInfo`', () => { + AccountInfoSingleton.getInstance().setAccountInfo({ + id: 'test', + name: 'test user', + extraInfo: { premium: true } + }); + + AccountInfoSingleton.getInstance().clearAccountInfo(); + + expect( + AccountInfoSingleton.getInstance().getAccountInfo() + ).toBeUndefined(); + }); + + it('`reset()` replaces the provider and clears stored account info', () => { + const instanceBefore = AccountInfoSingleton.getInstance(); + + AccountInfoSingleton.getInstance().setAccountInfo({ + id: 'test', + name: 'test user', + extraInfo: { premium: true } + }); + + AccountInfoSingleton.reset(); + + const instanceAfter = AccountInfoSingleton.getInstance(); + + expect(instanceAfter).not.toBe(instanceBefore); + + expect(instanceAfter.getAccountInfo()).toBeUndefined(); + }); + + it('getInstance returns the same provider between calls (singleton behavior)', () => { + const a = AccountInfoSingleton.getInstance(); + const b = AccountInfoSingleton.getInstance(); + + expect(a).toBe(b); + }); +}); diff --git a/packages/core/src/sdk/AccountInfoSingleton/types.ts b/packages/core/src/sdk/AccountInfoSingleton/types.ts new file mode 100644 index 000000000..1dceb0958 --- /dev/null +++ b/packages/core/src/sdk/AccountInfoSingleton/types.ts @@ -0,0 +1,11 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +export type AccountInfo = { + readonly id: string; + readonly name?: string; + extraInfo?: Record; +}; diff --git a/packages/core/src/sdk/EventMappers/EventMapper.ts b/packages/core/src/sdk/EventMappers/EventMapper.ts index 9ca252d72..e1cbaae19 100644 --- a/packages/core/src/sdk/EventMappers/EventMapper.ts +++ b/packages/core/src/sdk/EventMappers/EventMapper.ts @@ -7,6 +7,8 @@ import { InternalLog } from '../../InternalLog'; import { SdkVerbosity } from '../../SdkVerbosity'; import { DdSdk } from '../../sdk/DdSdk'; +import { AccountInfoSingleton } from '../AccountInfoSingleton/AccountInfoSingleton'; +import type { AccountInfo } from '../AccountInfoSingleton/types'; import { AttributesSingleton } from '../AttributesSingleton/AttributesSingleton'; import type { Attributes } from '../AttributesSingleton/types'; import { UserInfoSingleton } from '../UserInfoSingleton/UserInfoSingleton'; @@ -16,6 +18,7 @@ import { deepClone } from './utils/deepClone'; export type AdditionalEventDataForMapper = { userInfo?: UserInfo; + accountInfo?: AccountInfo; attributes: Attributes; }; @@ -66,9 +69,11 @@ export class EventMapper { // formatting const userInfo = UserInfoSingleton.getInstance().getUserInfo(); + const accountInfo = AccountInfoSingleton.getInstance().getAccountInfo(); const attributes = AttributesSingleton.getInstance().getAttributes(); const initialEvent = this.formatRawEventForMapper(rawEvent, { userInfo, + accountInfo, attributes }); diff --git a/packages/core/src/specs/NativeDdSdk.ts b/packages/core/src/specs/NativeDdSdk.ts index 70401fe3c..68c9c4711 100644 --- a/packages/core/src/specs/NativeDdSdk.ts +++ b/packages/core/src/specs/NativeDdSdk.ts @@ -67,6 +67,23 @@ export interface Spec extends TurboModule { */ addUserExtraInfo(extraInfo: Object): Promise; + /** + * Set the account information. + * @param account: The account object (use builtin attributes: 'id', 'name', and any custom attribute under extraInfo). + */ + setAccountInfo(account: Object): Promise; + + /** + * Clears the account information. + */ + clearAccountInfo(): Promise; + + /** + * Add custom attributes to the current account information + * @param extraInfo: The extraInfo object containing additional custom attributes + */ + addAccountExtraInfo(extraInfo: Object): Promise; + /** * Set the tracking consent regarding the data collection. * @param trackingConsent: Consent, which can take one of the following values: 'pending', 'granted', 'not_granted'. diff --git a/packages/core/src/types.tsx b/packages/core/src/types.tsx index 5c7d64cec..7f97779ee 100644 --- a/packages/core/src/types.tsx +++ b/packages/core/src/types.tsx @@ -129,6 +129,27 @@ export type DdSdkType = { */ addUserExtraInfo(extraUserInfo: Record): Promise; + /** + * Sets the account information. + * @param id: A unique account identifier (relevant to your business domain) + * @param name: The account name. + * @param extraInfo: Additional information. + */ + setAccountInfo(accountInfo: AccountInfo): Promise; + + /** + * Clears the account information. + */ + clearAccountInfo(): Promise; + + /** + * Add additional account information. + * @param extraAccountInfo: The additional information. (To set the id or name please use setAccountInfo). + */ + addAccountExtraInfo( + extraAccountInfo: Record + ): Promise; + /** * Set the tracking consent regarding the data collection. * @param trackingConsent: Consent, which can take one of the following values: 'pending', 'granted', 'not_granted'. @@ -176,6 +197,12 @@ export type UserInfo = { extraInfo?: object; }; +export type AccountInfo = { + id: string; + name?: string; + extraInfo?: object; +}; + // DdLogs export type LogStatus = 'debug' | 'info' | 'warn' | 'error'; From ae0c7ceb4ca7f89d2e892457bec548a81a9c36ec Mon Sep 17 00:00:00 2001 From: Carlos Nogueira Date: Fri, 7 Nov 2025 14:49:40 +0000 Subject: [PATCH 053/410] Add `userId` and `accountId`to baggage headers Preserve user baggage header when setting session ID Enforced W3 specification for 'baggage' header Only inject Session ID header if propagator=Datadog|W3C Additional test for baggage header and minor warn message improvement --- packages/core/src/rum/DdRum.ts | 32 ++-- packages/core/src/rum/__tests__/DdRum.test.ts | 2 +- packages/core/src/rum/helper.ts | 36 ++++ .../__tests__/headers.test.ts | 14 ++ .../distributedTracing/distributedTracing.tsx | 21 ++- .../distributedTracingHeaders.ts | 44 ++++- .../graphql/__tests__/graphqlHeaders.test.ts | 10 +- .../graphql/graphqlHeaders.ts | 9 +- .../resourceTracking/headers.ts | 12 ++ .../requestProxy/XHRProxy/XHRProxy.ts | 79 +++++--- .../XHRProxy/__tests__/XHRProxy.test.ts | 89 ++++++++- .../__tests__/baggageHeaderUtils.test.ts | 117 ++++++++++++ .../XHRProxy/baggageHeaderUtils.ts | 172 ++++++++++++++++++ .../DdSdkInternalNativeBridge.tsx | 2 +- 14 files changed, 577 insertions(+), 62 deletions(-) create mode 100644 packages/core/src/rum/helper.ts create mode 100644 packages/core/src/rum/instrumentation/resourceTracking/__tests__/headers.test.ts create mode 100644 packages/core/src/rum/instrumentation/resourceTracking/headers.ts create mode 100644 packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/__tests__/baggageHeaderUtils.test.ts create mode 100644 packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/baggageHeaderUtils.ts diff --git a/packages/core/src/rum/DdRum.ts b/packages/core/src/rum/DdRum.ts index e5deb8146..1f3703e63 100644 --- a/packages/core/src/rum/DdRum.ts +++ b/packages/core/src/rum/DdRum.ts @@ -20,12 +20,19 @@ import { getGlobalInstance } from '../utils/singletonUtils'; import { DefaultTimeProvider } from '../utils/time-provider/DefaultTimeProvider'; import type { TimeProvider } from '../utils/time-provider/TimeProvider'; -import { generateActionEventMapper } from './eventMappers/actionEventMapper'; import type { ActionEventMapper } from './eventMappers/actionEventMapper'; -import { generateErrorEventMapper } from './eventMappers/errorEventMapper'; +import { generateActionEventMapper } from './eventMappers/actionEventMapper'; import type { ErrorEventMapper } from './eventMappers/errorEventMapper'; -import { generateResourceEventMapper } from './eventMappers/resourceEventMapper'; +import { generateErrorEventMapper } from './eventMappers/errorEventMapper'; import type { ResourceEventMapper } from './eventMappers/resourceEventMapper'; +import { generateResourceEventMapper } from './eventMappers/resourceEventMapper'; +import { + clearCachedSessionId, + getCachedAccountId, + getCachedSessionId, + getCachedUserId, + setCachedSessionId +} from './helper'; import type { DatadogTracingContext } from './instrumentation/resourceTracking/distributedTracing/DatadogTracingContext'; import { DatadogTracingIdentifier } from './instrumentation/resourceTracking/distributedTracing/DatadogTracingIdentifier'; import { TracingIdentifier } from './instrumentation/resourceTracking/distributedTracing/TracingIdentifier'; @@ -33,16 +40,12 @@ import { getTracingContext, getTracingContextForPropagators } from './instrumentation/resourceTracking/distributedTracing/distributedTracingHeaders'; -import { - getCachedSessionId, - setCachedSessionId -} from './sessionId/sessionIdHelper'; import type { DdRumType, - RumActionType, - ResourceKind, FirstPartyHost, - PropagatorType + PropagatorType, + ResourceKind, + RumActionType } from './types'; const RUM_MODULE = 'com.datadog.reactnative.rum'; @@ -343,6 +346,7 @@ class DdRumWrapper implements DdRumType { stopSession = (): Promise => { InternalLog.log('Stopping RUM Session', SdkVerbosity.DEBUG); + clearCachedSessionId(); return bufferVoidNativeCall(() => this.nativeRum.stopSession()); }; @@ -381,7 +385,9 @@ class DdRumWrapper implements DdRumType { url, tracingSamplingRate, firstPartyHosts, - getCachedSessionId() + getCachedSessionId(), + getCachedUserId(), + getCachedAccountId() ); }; @@ -392,7 +398,9 @@ class DdRumWrapper implements DdRumType { return getTracingContextForPropagators( propagators, tracingSamplingRate, - getCachedSessionId() + getCachedSessionId(), + getCachedUserId(), + getCachedAccountId() ); }; diff --git a/packages/core/src/rum/__tests__/DdRum.test.ts b/packages/core/src/rum/__tests__/DdRum.test.ts index 41b873fe9..e4318322b 100644 --- a/packages/core/src/rum/__tests__/DdRum.test.ts +++ b/packages/core/src/rum/__tests__/DdRum.test.ts @@ -17,11 +17,11 @@ import { DdRum } from '../DdRum'; import type { ActionEventMapper } from '../eventMappers/actionEventMapper'; import type { ErrorEventMapper } from '../eventMappers/errorEventMapper'; import type { ResourceEventMapper } from '../eventMappers/resourceEventMapper'; +import { setCachedSessionId } from '../helper'; import { DatadogTracingContext } from '../instrumentation/resourceTracking/distributedTracing/DatadogTracingContext'; import { DatadogTracingIdentifier } from '../instrumentation/resourceTracking/distributedTracing/DatadogTracingIdentifier'; import { TracingIdFormat } from '../instrumentation/resourceTracking/distributedTracing/TracingIdentifier'; import { TracingIdentifierUtils } from '../instrumentation/resourceTracking/distributedTracing/__tests__/__utils__/TracingIdentifierUtils'; -import { setCachedSessionId } from '../sessionId/sessionIdHelper'; import type { FirstPartyHost } from '../types'; import { PropagatorType, RumActionType } from '../types'; diff --git a/packages/core/src/rum/helper.ts b/packages/core/src/rum/helper.ts new file mode 100644 index 000000000..a153b79b5 --- /dev/null +++ b/packages/core/src/rum/helper.ts @@ -0,0 +1,36 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ +let _cachedSessionId: string | undefined; +let _cachedUserId: string | undefined; +let _cachedAccountId: string | undefined; + +export const getCachedSessionId = () => { + return _cachedSessionId; +}; + +export const setCachedSessionId = (sessionId: string) => { + _cachedSessionId = sessionId; +}; + +export const clearCachedSessionId = () => { + _cachedSessionId = undefined; +}; + +export const getCachedUserId = () => { + return _cachedUserId; +}; + +export const setCachedUserId = (userId: string) => { + _cachedUserId = userId; +}; + +export const getCachedAccountId = () => { + return _cachedAccountId; +}; + +export const setCachedAccountId = (accountId: string) => { + _cachedAccountId = accountId; +}; diff --git a/packages/core/src/rum/instrumentation/resourceTracking/__tests__/headers.test.ts b/packages/core/src/rum/instrumentation/resourceTracking/__tests__/headers.test.ts new file mode 100644 index 000000000..dbb18b89b --- /dev/null +++ b/packages/core/src/rum/instrumentation/resourceTracking/__tests__/headers.test.ts @@ -0,0 +1,14 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ +import { isDatadogCustomHeader } from '../headers'; + +describe('headers', () => { + describe('isDatadogCustomHeader', () => { + it('returns false for non-custom headers', () => { + expect(isDatadogCustomHeader('non-custom-header')).toBeFalsy(); + }); + }); +}); diff --git a/packages/core/src/rum/instrumentation/resourceTracking/distributedTracing/distributedTracing.tsx b/packages/core/src/rum/instrumentation/resourceTracking/distributedTracing/distributedTracing.tsx index 9c4fcbff7..2ebb98233 100644 --- a/packages/core/src/rum/instrumentation/resourceTracking/distributedTracing/distributedTracing.tsx +++ b/packages/core/src/rum/instrumentation/resourceTracking/distributedTracing/distributedTracing.tsx @@ -26,6 +26,9 @@ export type DdRumResourceTracingAttributes = rulePsr: number; propagatorTypes: PropagatorType[]; rumSessionId?: string; + userId?: string; + accountId?: string; + baggageHeaders?: Set; } | { tracingStrategy: 'DISCARD'; @@ -43,12 +46,16 @@ export const getTracingAttributes = ({ hostname, firstPartyHostsRegexMap, tracingSamplingRate, - rumSessionId + rumSessionId, + userId, + accountId }: { hostname: Hostname | null; firstPartyHostsRegexMap: RegexMap; tracingSamplingRate: number; rumSessionId?: string; + userId?: string; + accountId?: string; }): DdRumResourceTracingAttributes => { if (hostname === null) { return DISCARDED_TRACE_ATTRIBUTES; @@ -61,7 +68,9 @@ export const getTracingAttributes = ({ return generateTracingAttributesWithSampling( tracingSamplingRate, propagatorsForHost, - rumSessionId + rumSessionId, + userId, + accountId ); } return DISCARDED_TRACE_ATTRIBUTES; @@ -70,7 +79,9 @@ export const getTracingAttributes = ({ export const generateTracingAttributesWithSampling = ( tracingSamplingRate: number, propagatorTypes: PropagatorType[], - rumSessionId?: string + rumSessionId?: string, + userId?: string, + accountId?: string ): DdRumResourceTracingAttributes => { if (!propagatorTypes || propagatorTypes.length === 0) { return DISCARDED_TRACE_ATTRIBUTES; @@ -93,7 +104,9 @@ export const generateTracingAttributesWithSampling = ( tracingStrategy: 'KEEP', rulePsr: tracingSamplingRate / 100, propagatorTypes, - rumSessionId + rumSessionId, + userId, + accountId }; return tracingAttributes; diff --git a/packages/core/src/rum/instrumentation/resourceTracking/distributedTracing/distributedTracingHeaders.ts b/packages/core/src/rum/instrumentation/resourceTracking/distributedTracing/distributedTracingHeaders.ts index f4decf2ac..033775a17 100644 --- a/packages/core/src/rum/instrumentation/resourceTracking/distributedTracing/distributedTracingHeaders.ts +++ b/packages/core/src/rum/instrumentation/resourceTracking/distributedTracing/distributedTracingHeaders.ts @@ -1,6 +1,5 @@ /* - * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. - * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. This product includes software developed at Datadog (https://www.datadoghq.com/). * Copyright 2016-Present Datadog, Inc. */ @@ -29,6 +28,8 @@ export const PARENT_ID_HEADER_KEY = 'x-datadog-parent-id'; export const TAGS_HEADER_KEY = 'x-datadog-tags'; export const DD_TRACE_ID_TAG = '_dd.p.tid'; export const DD_RUM_SESSION_ID_TAG = 'session.id'; +export const DD_RUM_USER_ID_TAG = 'user.id'; +export const DD_RUM_ACCOUNT_ID_TAG = 'account.id'; /** * OTel headers @@ -48,9 +49,12 @@ export const getTracingHeadersFromAttributes = ( if (tracingAttributes.tracingStrategy === 'DISCARD') { return headers; } + + let hasDatadogOrW3CPropagator = false; tracingAttributes.propagatorTypes.forEach(propagator => { switch (propagator) { case PropagatorType.DATADOG: { + hasDatadogOrW3CPropagator = true; headers.push( { header: ORIGIN_HEADER_KEY, @@ -82,6 +86,7 @@ export const getTracingHeadersFromAttributes = ( break; } case PropagatorType.TRACECONTEXT: { + hasDatadogOrW3CPropagator = true; const isSampled = tracingAttributes.samplingPriorityHeader === '1'; headers.push( @@ -137,13 +142,30 @@ export const getTracingHeadersFromAttributes = ( ); } } + }); + + if (hasDatadogOrW3CPropagator) { if (tracingAttributes.rumSessionId) { headers.push({ header: BAGGAGE_HEADER_KEY, value: `${DD_RUM_SESSION_ID_TAG}=${tracingAttributes.rumSessionId}` }); } - }); + + if (tracingAttributes.userId) { + headers.push({ + header: BAGGAGE_HEADER_KEY, + value: `${DD_RUM_USER_ID_TAG}=${tracingAttributes.userId}` + }); + } + + if (tracingAttributes.accountId) { + headers.push({ + header: BAGGAGE_HEADER_KEY, + value: `${DD_RUM_ACCOUNT_ID_TAG}=${tracingAttributes.accountId}` + }); + } + } return headers; }; @@ -152,7 +174,9 @@ export const getTracingContext = ( url: string, tracingSamplingRate: number, firstPartyHosts: FirstPartyHost[], - rumSessionId?: string + rumSessionId?: string, + userId?: string, + accountId?: string ): DatadogTracingContext => { const hostname = URLHostParser(url); const firstPartyHostsRegexMap = firstPartyHostsRegexMapBuilder( @@ -162,7 +186,9 @@ export const getTracingContext = ( hostname, firstPartyHostsRegexMap, tracingSamplingRate, - rumSessionId + rumSessionId, + userId, + accountId }); return getTracingContextForAttributes( @@ -174,13 +200,17 @@ export const getTracingContext = ( export const getTracingContextForPropagators = ( propagators: PropagatorType[], tracingSamplingRate: number, - rumSessionId?: string + rumSessionId?: string, + userId?: string, + accountId?: string ): DatadogTracingContext => { return getTracingContextForAttributes( generateTracingAttributesWithSampling( tracingSamplingRate, propagators, - rumSessionId + rumSessionId, + userId, + accountId ), tracingSamplingRate ); diff --git a/packages/core/src/rum/instrumentation/resourceTracking/graphql/__tests__/graphqlHeaders.test.ts b/packages/core/src/rum/instrumentation/resourceTracking/graphql/__tests__/graphqlHeaders.test.ts index b7d7dfa10..78c0f3b88 100644 --- a/packages/core/src/rum/instrumentation/resourceTracking/graphql/__tests__/graphqlHeaders.test.ts +++ b/packages/core/src/rum/instrumentation/resourceTracking/graphql/__tests__/graphqlHeaders.test.ts @@ -4,11 +4,11 @@ * Copyright 2016-Present Datadog, Inc. */ +import { isDatadogCustomHeader } from '../../headers'; import { DATADOG_GRAPH_QL_OPERATION_NAME_HEADER, DATADOG_GRAPH_QL_OPERATION_TYPE_HEADER, - DATADOG_GRAPH_QL_VARIABLES_HEADER, - isDatadogCustomHeader + DATADOG_GRAPH_QL_VARIABLES_HEADER } from '../graphqlHeaders'; describe('GraphQL custom headers', () => { @@ -19,10 +19,4 @@ describe('GraphQL custom headers', () => { ])('%s matches the custom header pattern', header => { expect(isDatadogCustomHeader(header)).toBeTruthy(); }); - - describe('isDatadogCustomHeader', () => { - it('returns false for non-custom headers', () => { - expect(isDatadogCustomHeader('non-custom-header')).toBeFalsy(); - }); - }); }); diff --git a/packages/core/src/rum/instrumentation/resourceTracking/graphql/graphqlHeaders.ts b/packages/core/src/rum/instrumentation/resourceTracking/graphql/graphqlHeaders.ts index 87c79e65e..730b1c468 100644 --- a/packages/core/src/rum/instrumentation/resourceTracking/graphql/graphqlHeaders.ts +++ b/packages/core/src/rum/instrumentation/resourceTracking/graphql/graphqlHeaders.ts @@ -1,15 +1,10 @@ +import { DATADOG_CUSTOM_HEADER_PREFIX } from '../headers'; + /* * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. * This product includes software developed at Datadog (https://www.datadoghq.com/). * Copyright 2016-Present Datadog, Inc. */ - -const DATADOG_CUSTOM_HEADER_PREFIX = '_dd-custom-header'; - export const DATADOG_GRAPH_QL_OPERATION_NAME_HEADER = `${DATADOG_CUSTOM_HEADER_PREFIX}-graph-ql-operation-name`; export const DATADOG_GRAPH_QL_VARIABLES_HEADER = `${DATADOG_CUSTOM_HEADER_PREFIX}-graph-ql-variables`; export const DATADOG_GRAPH_QL_OPERATION_TYPE_HEADER = `${DATADOG_CUSTOM_HEADER_PREFIX}-graph-ql-operation-type`; - -export const isDatadogCustomHeader = (header: string) => { - return header.match(new RegExp(`^${DATADOG_CUSTOM_HEADER_PREFIX}`)); -}; diff --git a/packages/core/src/rum/instrumentation/resourceTracking/headers.ts b/packages/core/src/rum/instrumentation/resourceTracking/headers.ts new file mode 100644 index 000000000..6ecdd37ae --- /dev/null +++ b/packages/core/src/rum/instrumentation/resourceTracking/headers.ts @@ -0,0 +1,12 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +export const DATADOG_CUSTOM_HEADER_PREFIX = '_dd-custom-header'; +export const DATADOG_BAGGAGE_HEADER = `${DATADOG_CUSTOM_HEADER_PREFIX}-baggage`; + +export const isDatadogCustomHeader = (header: string) => { + return header.match(new RegExp(`^${DATADOG_CUSTOM_HEADER_PREFIX}`)); +}; diff --git a/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/XHRProxy.ts b/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/XHRProxy.ts index e81c8014f..723ace5ed 100644 --- a/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/XHRProxy.ts +++ b/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/XHRProxy.ts @@ -5,21 +5,29 @@ */ import { Timer } from '../../../../../utils/Timer'; -import { getCachedSessionId } from '../../../../sessionId/sessionIdHelper'; -import { getTracingHeadersFromAttributes } from '../../distributedTracing/distributedTracingHeaders'; +import { + getCachedAccountId, + getCachedSessionId, + getCachedUserId +} from '../../../../helper'; +import { + BAGGAGE_HEADER_KEY, + getTracingHeadersFromAttributes +} from '../../distributedTracing/distributedTracingHeaders'; import type { DdRumResourceTracingAttributes } from '../../distributedTracing/distributedTracing'; import { getTracingAttributes } from '../../distributedTracing/distributedTracing'; import { DATADOG_GRAPH_QL_OPERATION_NAME_HEADER, DATADOG_GRAPH_QL_OPERATION_TYPE_HEADER, - DATADOG_GRAPH_QL_VARIABLES_HEADER, - isDatadogCustomHeader + DATADOG_GRAPH_QL_VARIABLES_HEADER } from '../../graphql/graphqlHeaders'; +import { DATADOG_BAGGAGE_HEADER, isDatadogCustomHeader } from '../../headers'; import type { RequestProxyOptions } from '../interfaces/RequestProxy'; import { RequestProxy } from '../interfaces/RequestProxy'; import type { ResourceReporter } from './DatadogRumResource/ResourceReporter'; import { URLHostParser } from './URLHostParser'; +import { formatBaggageHeader } from './baggageHeaderUtils'; import { calculateResponseSize } from './responseSize'; const RESPONSE_START_LABEL = 'response_start'; @@ -39,6 +47,7 @@ interface DdRumXhrContext { reported: boolean; timer: Timer; tracingAttributes: DdRumResourceTracingAttributes; + baggageHeaderEntries: Set; } interface XHRProxyProviders { @@ -110,8 +119,11 @@ const proxyOpen = ( hostname, firstPartyHostsRegexMap, tracingSamplingRate, - rumSessionId: getCachedSessionId() - }) + rumSessionId: getCachedSessionId(), + userId: getCachedUserId(), + accountId: getCachedAccountId() + }), + baggageHeaderEntries: new Set() }; // eslint-disable-next-line prefer-rest-params return originalXhrOpen.apply(this, arguments as any); @@ -127,12 +139,22 @@ const proxySend = (providers: XHRProxyProviders): void => { // keep track of start time this._datadog_xhr.timer.start(); + // Tracing Headers const tracingHeaders = getTracingHeadersFromAttributes( this._datadog_xhr.tracingAttributes ); + tracingHeaders.forEach(({ header, value }) => { this.setRequestHeader(header, value); }); + + // Join all baggage header entries + const baggageHeader = formatBaggageHeader( + this._datadog_xhr.baggageHeaderEntries + ); + if (baggageHeader) { + this.setRequestHeader(DATADOG_BAGGAGE_HEADER, baggageHeader); + } } proxyOnReadyStateChange(this, providers); @@ -211,22 +233,37 @@ const proxySetRequestHeader = (providers: XHRProxyProviders): void => { header: string, value: string ) { - if (isDatadogCustomHeader(header)) { - if (header === DATADOG_GRAPH_QL_OPERATION_NAME_HEADER) { - this._datadog_xhr.graphql.operationName = value; - return; - } - if (header === DATADOG_GRAPH_QL_OPERATION_TYPE_HEADER) { - this._datadog_xhr.graphql.operationType = value; - return; - } - if (header === DATADOG_GRAPH_QL_VARIABLES_HEADER) { - this._datadog_xhr.graphql.variables = value; - return; + const key = header.toLowerCase(); + if (isDatadogCustomHeader(key)) { + switch (key) { + case DATADOG_GRAPH_QL_OPERATION_NAME_HEADER: + this._datadog_xhr.graphql.operationName = value; + break; + case DATADOG_GRAPH_QL_OPERATION_TYPE_HEADER: + this._datadog_xhr.graphql.operationType = value; + break; + case DATADOG_GRAPH_QL_VARIABLES_HEADER: + this._datadog_xhr.graphql.variables = value; + break; + case DATADOG_BAGGAGE_HEADER: + // Apply Baggage Header only if pre-processed by Datadog + return originalXhrSetRequestHeader.apply(this, [ + BAGGAGE_HEADER_KEY, + value + ]); + default: + return originalXhrSetRequestHeader.apply( + this, + // eslint-disable-next-line prefer-rest-params + arguments as any + ); } + } else if (key === BAGGAGE_HEADER_KEY) { + // Intercept User Baggage Header entries to apply them later + this._datadog_xhr.baggageHeaderEntries?.add(value); + } else { + // eslint-disable-next-line prefer-rest-params + return originalXhrSetRequestHeader.apply(this, arguments as any); } - - // eslint-disable-next-line prefer-rest-params - return originalXhrSetRequestHeader.apply(this, arguments as any); }; }; diff --git a/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/__tests__/XHRProxy.test.ts b/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/__tests__/XHRProxy.test.ts index 907bfe57a..048dbadec 100644 --- a/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/__tests__/XHRProxy.test.ts +++ b/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/__tests__/XHRProxy.test.ts @@ -11,7 +11,7 @@ import { InternalLog } from '../../../../../../InternalLog'; import { SdkVerbosity } from '../../../../../../SdkVerbosity'; import { BufferSingleton } from '../../../../../../sdk/DatadogProvider/Buffer/BufferSingleton'; import { DdRum } from '../../../../../DdRum'; -import { setCachedSessionId } from '../../../../../sessionId/sessionIdHelper'; +import { setCachedSessionId } from '../../../../../helper'; import { PropagatorType } from '../../../../../types'; import { XMLHttpRequestMock } from '../../../__tests__/__utils__/XMLHttpRequestMock'; import { TracingIdentifierUtils } from '../../../distributedTracing/__tests__/__utils__/TracingIdentifierUtils'; @@ -839,6 +839,93 @@ describe('XHRProxy', () => { // THEN expect(xhr.requestHeaders[BAGGAGE_HEADER_KEY]).toBeUndefined(); }); + + it('does not add rum session id to baggage headers when propagator type is not datadog or w3c', async () => { + // GIVEN + const method = 'GET'; + const url = 'https://example.com'; + xhrProxy.onTrackingStart({ + tracingSamplingRate: 100, + firstPartyHostsRegexMap: firstPartyHostsRegexMapBuilder([ + { + match: 'api.example.com', + propagatorTypes: [ + PropagatorType.DATADOG, + PropagatorType.TRACECONTEXT + ] + }, + { + match: 'example.com', // <-- no datadog or tracecontext here + propagatorTypes: [ + PropagatorType.B3, + PropagatorType.B3MULTI + ] + } + ]) + }); + + setCachedSessionId('TEST-SESSION-ID'); + + // WHEN + const xhr = new XMLHttpRequestMock(); + xhr.open(method, url); + xhr.send(); + xhr.notifyResponseArrived(); + xhr.complete(200, 'ok'); + await flushPromises(); + + // THEN + expect(xhr.requestHeaders[BAGGAGE_HEADER_KEY]).toBeUndefined(); + }); + + it('rum session id does not overwrite existing baggage headers', async () => { + // GIVEN + const method = 'GET'; + const url = 'https://api.example.com:443/v2/user'; + xhrProxy.onTrackingStart({ + tracingSamplingRate: 100, + firstPartyHostsRegexMap: firstPartyHostsRegexMapBuilder([ + { + match: 'api.example.com', + propagatorTypes: [ + PropagatorType.DATADOG, + PropagatorType.TRACECONTEXT + ] + }, + { + match: 'example.com', + propagatorTypes: [ + PropagatorType.B3, + PropagatorType.B3MULTI + ] + } + ]) + }); + + setCachedSessionId('TEST-SESSION-ID'); + + // WHEN + const xhr = new XMLHttpRequestMock(); + xhr.open(method, url); + xhr.setRequestHeader('baggage', 'existing.key=existing-value'); + xhr.send(); + xhr.notifyResponseArrived(); + xhr.complete(200, 'ok'); + await flushPromises(); + + // THEN + expect(xhr.requestHeaders[BAGGAGE_HEADER_KEY]).not.toBeUndefined(); + expect(xhr.requestHeaders[BAGGAGE_HEADER_KEY]).toContain( + 'existing.key=existing-value' + ); + + const values = xhr.requestHeaders[BAGGAGE_HEADER_KEY].split( + ',' + ).sort(); + + expect(values[0]).toBe('existing.key=existing-value'); + expect(values[1]).toBe('session.id=TEST-SESSION-ID'); + }); }); describe('DdRum.startResource calls', () => { diff --git a/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/__tests__/baggageHeaderUtils.test.ts b/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/__tests__/baggageHeaderUtils.test.ts new file mode 100644 index 000000000..eee59838f --- /dev/null +++ b/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/__tests__/baggageHeaderUtils.test.ts @@ -0,0 +1,117 @@ +import { InternalLog } from '../../../../../../InternalLog'; +import { SdkVerbosity } from '../../../../../../SdkVerbosity'; +import { formatBaggageHeader } from '../baggageHeaderUtils'; + +describe('formatBaggageHeader', () => { + let logSpy: jest.SpyInstance; + + beforeEach(() => { + logSpy = jest.spyOn(InternalLog, 'log').mockImplementation(() => {}); + }); + + afterEach(() => { + logSpy.mockRestore(); + }); + + it('should format simple key=value entries correctly', () => { + const entries = new Set(['userId=alice', 'isProduction=false']); + const result = formatBaggageHeader(entries); + expect(result).toBe('userId=alice,isProduction=false'); + expect(logSpy).not.toHaveBeenCalled(); + }); + + it('should percent-encode spaces and non-ASCII characters in values', () => { + const entries = new Set(['user=Amélie', 'region=us east']); + const result = formatBaggageHeader(entries); + expect(result).toBe('user=Am%C3%A9lie,region=us%20east'); + }); + + it('should support properties with and without values', () => { + const entries = new Set(['traceId=abc123;sampled=true;debug']); + const result = formatBaggageHeader(entries); + expect(result).toBe('traceId=abc123;sampled=true;debug'); + }); + + it('should trim whitespace around keys, values, and properties', () => { + const entries = new Set([' foo = bar ; p1 = one ; p2 ']); + const result = formatBaggageHeader(entries); + expect(result).toBe('foo=bar;p1=one;p2'); + }); + + it('should skip invalid entries without crashing', () => { + const entries = new Set(['valid=ok', 'invalidEntry']); + const result = formatBaggageHeader(entries); + expect(result).toBe('valid=ok'); + expect(logSpy).toHaveBeenCalledWith( + expect.stringContaining('Dropped invalid baggage header entry'), + SdkVerbosity.WARN + ); + }); + + it('should skip entries with invalid key (non-token)', () => { + const entries = new Set(['in valid=value', 'user=ok']); + const result = formatBaggageHeader(entries); + expect(result).toBe('user=ok'); + expect(logSpy).toHaveBeenCalledWith( + expect.stringContaining('key not compliant'), + SdkVerbosity.WARN + ); + }); + + it('should skip invalid properties (bad property key)', () => { + const entries = new Set(['user=ok;invalid key=value;good=yes']); + const result = formatBaggageHeader(entries); + expect(result).toBe('user=ok;good=yes'); + expect(logSpy).toHaveBeenCalledWith( + expect.stringContaining('property key not compliant'), + SdkVerbosity.WARN + ); + }); + + it('should log warning when too many members (>64)', () => { + const entries = new Set(); + for (let i = 0; i < 70; i++) { + entries.add(`k${i}=v${i}`); + } + const result = formatBaggageHeader(entries); + expect(result?.startsWith('k0=v0')).toBe(true); + expect(logSpy).toHaveBeenCalledWith( + expect.stringContaining('Too many baggage members'), + SdkVerbosity.WARN + ); + }); + + it('should log warning when header exceeds byte limit', () => { + const bigValue = 'x'.repeat(9000); + const entries = new Set([`large=${bigValue}`]); + const result = formatBaggageHeader(entries); + expect(result).toContain('large='); + expect(logSpy).toHaveBeenCalledWith( + expect.stringContaining('Baggage header too large'), + SdkVerbosity.WARN + ); + }); + + it('should return null if all entries are invalid', () => { + const entries = new Set(['badEntry', 'stillBad']); + const result = formatBaggageHeader(entries); + expect(result).toBeNull(); + }); + + it('should preserve insertion order', () => { + const entries = new Set(['first=1', 'second=2', 'third=3']); + const result = formatBaggageHeader(entries); + expect(result).toBe('first=1,second=2,third=3'); + }); + + it('should trim keys and values', () => { + const entries = new Set([ + 'traceId=abc123;sampled=true;debug', + 'test1 = this is a test' + ]); + const result = formatBaggageHeader(entries); + expect(result).toBe( + 'traceId=abc123;sampled=true;debug,test1=this%20is%20a%20test' + ); + }); +}); diff --git a/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/baggageHeaderUtils.ts b/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/baggageHeaderUtils.ts new file mode 100644 index 000000000..7094ec4e8 --- /dev/null +++ b/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/baggageHeaderUtils.ts @@ -0,0 +1,172 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +import { InternalLog } from '../../../../../InternalLog'; +import { SdkVerbosity } from '../../../../../SdkVerbosity'; + +// The resulting baggage-string should contain 64 list-members or less (https://www.w3.org/TR/baggage/#limits) +const MAX_MEMBERS = 64; + +// The resulting baggage-string should be of size 8192 bytes or less (https://www.w3.org/TR/baggage/#limits) +const MAX_BYTES = 8192; + +// The keys must follow RFC 7230 token grammar (https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.6) +const TOKEN_REGEX = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/; + +/** + * Lazy property for {@link getBaggageHeaderSafeChars}. + */ +let baggageHeaderSafeChars: Set | undefined; + +/** + * Transform a Set of baggage entries (strings like "key=value;prop1=foo;prop2") + * into a compliant baggage header value per W3C Baggage spec. + */ +export function formatBaggageHeader(entries: Set): string | null { + const formattedParts: string[] = []; + + for (const rawEntry of entries) { + if (!rawEntry.includes('=')) { + InternalLog.log( + 'XHRProxy: Dropped invalid baggage header entry - expected format "key=value".', + SdkVerbosity.WARN + ); + continue; + } + + // Split first key=value from properties (properties are after first ';') + const [mainPart, ...rawProperties] = rawEntry.split(';'); + const idx = mainPart.indexOf('='); + if (idx <= 0) { + InternalLog.log( + "XHRProxy: Dropped invalid baggage header entry - no '=' or empty key", + SdkVerbosity.WARN + ); + continue; + } + + const rawKey = mainPart.slice(0, idx).trim(); + const rawValue = mainPart.slice(idx + 1).trim(); + + if (!TOKEN_REGEX.test(rawKey)) { + InternalLog.log( + 'XHRProxy: Dropped invalid baggage header entry - key not compliant to RFC 7230 token grammar', + SdkVerbosity.WARN + ); + continue; + } + + const encodedValue = encodeValue(rawValue); + + // Handle properties + const properties: string[] = []; + for (const rawProperty of rawProperties) { + const trimmed = rawProperty.trim(); + if (!trimmed) { + continue; + } + + const eqIdx = trimmed.indexOf('='); + if (eqIdx === -1) { + // Property with no value (key1=value1;prop1; ... ) + const propKey = trimmed.trim(); + if (!TOKEN_REGEX.test(propKey)) { + InternalLog.log( + 'XHRProxy: Dropped invalid baggage header entry - property key not compliant to RFC 7230 token grammar', + SdkVerbosity.WARN + ); + continue; + } + properties.push(propKey); + } else { + // Property in key-value format (key1=value1;prop1=propValue1; ... ) + const propKey = trimmed.slice(0, eqIdx).trim(); + const propVal = trimmed.slice(eqIdx + 1).trim(); + if (!TOKEN_REGEX.test(propKey)) { + InternalLog.log( + 'XHRProxy: Dropped invalid baggage header entry - key-value property key not compliant to RFC 7230 token grammar', + SdkVerbosity.WARN + ); + continue; + } + properties.push(`${propKey}=${encodeValue(propVal)}`); + } + } + + const joinedProps = properties.length ? `;${properties.join(';')}` : ''; + formattedParts.push(`${rawKey}=${encodedValue}${joinedProps}`); + } + + if (formattedParts.length > MAX_MEMBERS) { + InternalLog.log( + `XHRProxy: Too many baggage members: ${formattedParts.length} > ${MAX_MEMBERS} - entries may be dropped (https://www.w3.org/TR/baggage/#limits)`, + SdkVerbosity.WARN + ); + } else if (formattedParts.length === 0) { + return null; + } + + const headerValue = formattedParts.join(','); + const byteLength = Buffer.byteLength(headerValue, 'utf8'); + + if (byteLength > MAX_BYTES) { + InternalLog.log( + `Baggage header too large: ${byteLength} bytes > ${MAX_BYTES} - entries may be dropped (https://www.w3.org/TR/baggage/#limits)`, + SdkVerbosity.WARN + ); + } + + return headerValue; +} + +/** + * Returns a set of valid baggage header characters. + */ +function getBaggageHeaderSafeChars(): Set { + if (baggageHeaderSafeChars) { + return baggageHeaderSafeChars; + } + + const safeChars = new Set(); + for (let c = 0x21; c <= 0x7e; c++) { + if ( + c === 0x22 || + c === 0x2c || + c === 0x3b || + c === 0x5c || + c === 0x20 + ) { + continue; + } + safeChars.add(String.fromCharCode(c)); + } + + baggageHeaderSafeChars = safeChars; + + return safeChars; +} + +/* + * Percent-encode all characters outside baggage-octet range. + */ +function encodeValue(raw: string): string { + const safeChars = getBaggageHeaderSafeChars(); + let result = ''; + for (const ch of Array.from(raw)) { + if (safeChars.has(ch)) { + result += ch; + } else { + const utf8Bytes = Buffer.from(ch, 'utf8'); + for (const value of utf8Bytes) { + result += `%${value + .toString(16) + .toUpperCase() + .padStart(2, '0')}`; + } + } + } + return result; +} diff --git a/packages/core/src/sdk/DatadogInternalBridge/DdSdkInternalNativeBridge.tsx b/packages/core/src/sdk/DatadogInternalBridge/DdSdkInternalNativeBridge.tsx index bbae2cd12..34a1e623a 100644 --- a/packages/core/src/sdk/DatadogInternalBridge/DdSdkInternalNativeBridge.tsx +++ b/packages/core/src/sdk/DatadogInternalBridge/DdSdkInternalNativeBridge.tsx @@ -5,7 +5,7 @@ */ import { InternalLog } from '../../InternalLog'; import { SdkVerbosity } from '../../SdkVerbosity'; -import { setCachedSessionId } from '../../rum/sessionId/sessionIdHelper'; +import { setCachedSessionId } from '../../rum/helper'; import { DatadogDefaultEventEmitter } from '../DatadogEventEmitter/DatadogDefaultEventEmitter'; import type { DatadogEventEmitter } from '../DatadogEventEmitter/DatadogEventEmitter'; From df2b8e68b6e3ef829b0b64144f050a33d4226c48 Mon Sep 17 00:00:00 2001 From: Carlos Nogueira Date: Thu, 13 Nov 2025 17:24:34 +0000 Subject: [PATCH 054/410] Cache `userId` and `accountId` when first set --- .../requestProxy/XHRProxy/__tests__/XHRProxy.test.ts | 10 +++++++++- .../sdk/AccountInfoSingleton/AccountInfoSingleton.ts | 3 +++ .../src/sdk/UserInfoSingleton/UserInfoSingleton.ts | 3 +++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/__tests__/XHRProxy.test.ts b/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/__tests__/XHRProxy.test.ts index 048dbadec..a18825771 100644 --- a/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/__tests__/XHRProxy.test.ts +++ b/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/__tests__/XHRProxy.test.ts @@ -11,7 +11,11 @@ import { InternalLog } from '../../../../../../InternalLog'; import { SdkVerbosity } from '../../../../../../SdkVerbosity'; import { BufferSingleton } from '../../../../../../sdk/DatadogProvider/Buffer/BufferSingleton'; import { DdRum } from '../../../../../DdRum'; -import { setCachedSessionId } from '../../../../../helper'; +import { + setCachedSessionId, + setCachedUserId, + setCachedAccountId +} from '../../../../../helper'; import { PropagatorType } from '../../../../../types'; import { XMLHttpRequestMock } from '../../../__tests__/__utils__/XMLHttpRequestMock'; import { TracingIdentifierUtils } from '../../../distributedTracing/__tests__/__utils__/TracingIdentifierUtils'; @@ -90,6 +94,10 @@ afterEach(() => { (Date.now as jest.MockedFunction).mockClear(); jest.spyOn(global.Math, 'random').mockRestore(); DdRum.unregisterResourceEventMapper(); + + setCachedSessionId(undefined as any); + setCachedUserId(undefined as any); + setCachedAccountId(undefined as any); }); describe('XHRProxy', () => { diff --git a/packages/core/src/sdk/AccountInfoSingleton/AccountInfoSingleton.ts b/packages/core/src/sdk/AccountInfoSingleton/AccountInfoSingleton.ts index 5f2be8dea..439c1493a 100644 --- a/packages/core/src/sdk/AccountInfoSingleton/AccountInfoSingleton.ts +++ b/packages/core/src/sdk/AccountInfoSingleton/AccountInfoSingleton.ts @@ -4,6 +4,8 @@ * Copyright 2016-Present Datadog, Inc. */ +import { setCachedAccountId } from '../../rum/helper'; + import type { AccountInfo } from './types'; class AccountInfoProvider { @@ -11,6 +13,7 @@ class AccountInfoProvider { setAccountInfo = (accountInfo: AccountInfo) => { this.accountInfo = accountInfo; + setCachedAccountId(this.accountInfo.id); }; addAccountExtraInfo = (extraInfo: AccountInfo['extraInfo']) => { diff --git a/packages/core/src/sdk/UserInfoSingleton/UserInfoSingleton.ts b/packages/core/src/sdk/UserInfoSingleton/UserInfoSingleton.ts index 3ce23614b..2408fdbf2 100644 --- a/packages/core/src/sdk/UserInfoSingleton/UserInfoSingleton.ts +++ b/packages/core/src/sdk/UserInfoSingleton/UserInfoSingleton.ts @@ -4,6 +4,8 @@ * Copyright 2016-Present Datadog, Inc. */ +import { setCachedUserId } from '../../rum/helper'; + import type { UserInfo } from './types'; class UserInfoProvider { @@ -11,6 +13,7 @@ class UserInfoProvider { setUserInfo = (userInfo: UserInfo) => { this.userInfo = userInfo; + setCachedUserId(this.userInfo.id); }; getUserInfo = (): UserInfo | undefined => { From 54b243aec31c07f4a3976e028c3a21a413ca04c1 Mon Sep 17 00:00:00 2001 From: Carlos Nogueira Date: Fri, 21 Nov 2025 11:20:51 +0000 Subject: [PATCH 055/410] Integrate Feature Operations into `core` SDK --- packages/codepush/__mocks__/react-native.ts | 11 +++- packages/core/__mocks__/react-native.ts | 11 +++- .../reactnative/DdRumImplementation.kt | 54 ++++++++++++++++ .../kotlin/com/datadog/reactnative/DdRum.kt | 63 ++++++++++++++++++- .../kotlin/com/datadog/reactnative/DdRum.kt | 56 +++++++++++++++++ packages/core/ios/Sources/DdRum.mm | 43 +++++++++++++ .../ios/Sources/DdRumImplementation.swift | 51 +++++++++++++++ packages/core/jest/mock.js | 9 +++ packages/core/src/index.tsx | 3 +- packages/core/src/rum/DdRum.ts | 54 +++++++++++++++- packages/core/src/rum/types.ts | 42 ++++++++++++- packages/core/src/specs/NativeDdRum.ts | 40 ++++++++++++ packages/core/src/types.tsx | 6 ++ .../__mocks__/react-native.ts | 11 +++- 14 files changed, 447 insertions(+), 7 deletions(-) diff --git a/packages/codepush/__mocks__/react-native.ts b/packages/codepush/__mocks__/react-native.ts index 0c8189840..a87659170 100644 --- a/packages/codepush/__mocks__/react-native.ts +++ b/packages/codepush/__mocks__/react-native.ts @@ -119,7 +119,16 @@ actualRN.NativeModules.DdRum = { new Promise(resolve => resolve('test-session-id') ) - ) as jest.MockedFunction + ) as jest.MockedFunction, + startFeatureOperation: jest.fn().mockImplementation( + () => new Promise(resolve => resolve()) + ) as jest.MockedFunction, + succeedFeatureOperation: jest.fn().mockImplementation( + () => new Promise(resolve => resolve()) + ) as jest.MockedFunction, + failFeatureOperation: jest.fn().mockImplementation( + () => new Promise(resolve => resolve()) + ) as jest.MockedFunction }; module.exports = actualRN; diff --git a/packages/core/__mocks__/react-native.ts b/packages/core/__mocks__/react-native.ts index 73308f711..a4622b6b9 100644 --- a/packages/core/__mocks__/react-native.ts +++ b/packages/core/__mocks__/react-native.ts @@ -155,7 +155,16 @@ actualRN.NativeModules.DdRum = { new Promise(resolve => resolve('test-session-id') ) - ) as jest.MockedFunction + ) as jest.MockedFunction, + startFeatureOperation: jest.fn().mockImplementation( + () => new Promise(resolve => resolve()) + ) as jest.MockedFunction, + succeedFeatureOperation: jest.fn().mockImplementation( + () => new Promise(resolve => resolve()) + ) as jest.MockedFunction, + failFeatureOperation: jest.fn().mockImplementation( + () => new Promise(resolve => resolve()) + ) as jest.MockedFunction }; module.exports = actualRN; diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdRumImplementation.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdRumImplementation.kt index 4e3cd416f..67a299f36 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdRumImplementation.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdRumImplementation.kt @@ -12,6 +12,7 @@ import com.datadog.android.rum.RumAttributes import com.datadog.android.rum.RumErrorSource import com.datadog.android.rum.RumResourceKind import com.datadog.android.rum.RumResourceMethod +import com.datadog.android.rum.featureoperations.FailureReason import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReadableArray import com.facebook.react.bridge.ReadableMap @@ -333,6 +334,59 @@ class DdRumImplementation(private val datadog: DatadogWrapper = DatadogSDKWrappe } } + /** + * Starts a Feature Operation. + * + * @param name Human-readable operation name (e.g., "login_flow"). + * @param operationKey Optional key that uniquely identifies this operation instance. + * @param attributes Additional attributes to attach to the operation. + * @param promise Resolved with `null` when the call completes. + */ + fun startFeatureOperation(name: String, operationKey: String? = null, attributes: ReadableMap, promise: Promise) { + val attributesMap = attributes.toHashMap().toMutableMap() + datadog.getRumMonitor().startFeatureOperation(name, operationKey, attributesMap); + promise.resolve(null) + } + + /** + * Marks a Feature Operation as successfully completed. + * + * @param name The name of the feature operation (for example, `"login_flow"`). + * @param operationKey The key of the operation instance to complete, if one was provided when starting it. + * @param attributes A map of custom attributes to attach to this completion event. + */ + fun succeedFeatureOperation(name: String, operationKey: String? = null, attributes: ReadableMap, promise: Promise) { + val attributesMap = attributes.toHashMap().toMutableMap() + datadog.getRumMonitor().succeedFeatureOperation(name, operationKey, attributesMap) + promise.resolve(null) + } + + + /** + * Marks a Feature Operation as failed. + * + * @param name The name of the feature operation (for example, `"login_flow"`). + * @param operationKey The key of the operation instance to fail, if one was provided when starting it. + * @param failureReason The reason for the failure. Possible values are defined in [FailureReason] + * (e.g., `FailureReason.ERROR`, `FailureReason.ABANDONED`, `FailureReason.OTHER`). + * @param attributes A map of custom attributes to attach to this failure event. + */ + fun failFeatureOperation( + name: String, + operationKey: String? = null, + failureReason: String, + attributes: ReadableMap, + promise: Promise + ) { + val attributesMap = attributes.toHashMap().toMutableMap() + val reason = runCatching { + enumValueOf(failureReason.uppercase()) + }.getOrDefault(FailureReason.OTHER) + + datadog.getRumMonitor().failFeatureOperation(name, operationKey, reason, attributesMap) + promise.resolve(null) + } + // region Internal private fun String.asRumActionType(): RumActionType { diff --git a/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdRum.kt b/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdRum.kt index 6cb2b385b..30788acff 100644 --- a/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdRum.kt +++ b/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdRum.kt @@ -6,6 +6,7 @@ package com.datadog.reactnative +import com.datadog.android.rum.featureoperations.FailureReason import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.ReactMethod @@ -52,7 +53,12 @@ class DdRum( * If not provided, current timestamp will be used. */ @ReactMethod - override fun stopView(key: String, context: ReadableMap, timestampMs: Double, promise: Promise) { + override fun stopView( + key: String, + context: ReadableMap, + timestampMs: Double, + promise: Promise + ) { implementation.stopView(key, context, timestampMs, promise) } @@ -276,4 +282,59 @@ class DdRum( override fun getCurrentSessionId(promise: Promise) { implementation.getCurrentSessionId(promise) } + + /** + * Starts a RUM Feature Operation. + * + * @param name Human-readable operation name (e.g., "login_flow"). + * @param operationKey Optional key that uniquely identifies this operation instance. + * @param attributes Additional attributes to attach to the operation. + * @param promise Resolved with `null` when the call completes. + */ + @ReactMethod + override fun startFeatureOperation( + name: String, + operationKey: String?, + attributes: ReadableMap, + promise: Promise + ) { + implementation.startFeatureOperation(name, operationKey, attributes, promise) + } + + /** + * Marks a Feature Operation as successfully completed. + * + * @param name The name of the feature operation (for example, `"login_flow"`). + * @param operationKey The key of the operation instance to complete, if one was provided when starting it. + * @param attributes A map of custom attributes to attach to this completion event. + */ + @ReactMethod + override fun succeedFeatureOperation( + name: String, + operationKey: String?, + attributes: ReadableMap, + promise: Promise + ) { + implementation.succeedFeatureOperation(name, operationKey, attributes, promise) + } + + /** + * Marks a Feature Operation as failed. + * + * @param name The name of the feature operation (for example, `"login_flow"`). + * @param operationKey The key of the operation instance to fail, if one was provided when starting it. + * @param failureReason The reason for the failure. Possible values are defined in [FailureReason] + * (e.g., `FailureReason.ERROR`, `FailureReason.ABANDONED`, `FailureReason.OTHER`). + * @param attributes A map of custom attributes to attach to this failure event. + */ + @ReactMethod + override fun failFeatureOperation( + name: String, + operationKey: String?, + failureReason: String, + attributes: ReadableMap, + promise: Promise + ) { + implementation.failFeatureOperation(name, operationKey, failureReason, attributes, promise) + } } diff --git a/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdRum.kt b/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdRum.kt index a6c4965ea..4ef8409b2 100644 --- a/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdRum.kt +++ b/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdRum.kt @@ -6,6 +6,7 @@ package com.datadog.reactnative +import com.datadog.android.rum.featureoperations.FailureReason import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.ReactContextBaseJavaModule @@ -266,4 +267,59 @@ class DdRum( fun getCurrentSessionId(promise: Promise) { implementation.getCurrentSessionId(promise) } + + /** + * Starts a RUM Feature Operation. + * + * @param name Human-readable operation name (e.g., "login_flow"). + * @param operationKey Optional key that uniquely identifies this operation instance. + * @param attributes Additional attributes to attach to the operation. + * @param promise Resolved with `null` when the call completes. + */ + @ReactMethod + fun startFeatureOperation( + name: String, + operationKey: String? = null, + attributes: ReadableMap, + promise: Promise + ) { + implementation.startFeatureOperation(name, operationKey, attributes, promise) + } + + /** + * Marks a Feature Operation as successfully completed. + * + * @param name The name of the feature operation (for example, "login_flow"). + * @param operationKey The key of the operation instance to complete, if one was provided. + * @param attributes A map of custom attributes to attach to this completion event. + */ + @ReactMethod + fun succeedFeatureOperation( + name: String, + operationKey: String? = null, + attributes: ReadableMap, + promise: Promise + ) { + implementation.succeedFeatureOperation(name, operationKey, attributes, promise) + } + + /** + * Marks a Feature Operation as failed. + * + * @param name The name of the feature operation (for example, "login_flow"). + * @param operationKey The key of the operation instance to fail, if one was provided. + * @param failureReason The reason for the failure. Values are defined in [FailureReason] + * (e.g., `FailureReason.ERROR`, `FailureReason.ABANDONED`, `FailureReason.OTHER`). + * @param attributes A map of custom attributes to attach to this failure event. + */ + @ReactMethod + fun failFeatureOperation( + name: String, + operationKey: String? = null, + failureReason: String, + attributes: ReadableMap, + promise: Promise + ) { + implementation.failFeatureOperation(name, operationKey, failureReason, attributes, promise) + } } diff --git a/packages/core/ios/Sources/DdRum.mm b/packages/core/ios/Sources/DdRum.mm index f5c324ce8..c891537f3 100644 --- a/packages/core/ios/Sources/DdRum.mm +++ b/packages/core/ios/Sources/DdRum.mm @@ -164,6 +164,37 @@ @implementation DdRum [self getCurrentSessionId:resolve reject:reject]; } +RCT_REMAP_METHOD(startFeatureOperation, + startWithName:(NSString*)name + withOperationKey:(NSString*)operationKey + withAttributes:(NSDictionary*)attributes + withResolver:(RCTPromiseResolveBlock)resolve + withRejecter:(RCTPromiseRejectBlock)reject) +{ + [self startFeatureOperation:name operationKey:operationKey attributes:attributes resolve:resolve reject:reject]; +} + +RCT_REMAP_METHOD(succeedFeatureOperation, + succeedWithName:(NSString*)name + withOperationKey:(NSString*)operationKey + withAttributes:(NSDictionary*)attributes + withResolver:(RCTPromiseResolveBlock)resolve + withRejecter:(RCTPromiseRejectBlock)reject) +{ + [self succeedFeatureOperation:name operationKey:operationKey attributes:attributes resolve:resolve reject:reject]; +} + +RCT_REMAP_METHOD(failFeatureOperation, + failWithName:(NSString*)name + withOperationKey:(NSString*)operationKey + withReason:(NSString*)reason + withAttributes:(NSDictionary*)attributes + withResolver:(RCTPromiseResolveBlock)resolve + withRejecter:(RCTPromiseRejectBlock)reject) +{ + [self failFeatureOperation:name operationKey:operationKey reason:reason attributes:attributes resolve:resolve reject:reject]; +} + // Thanks to this guard, we won't compile this code when we build for the old architecture. #ifdef RCT_NEW_ARCH_ENABLED - (std::shared_ptr)getTurboModule: @@ -257,4 +288,16 @@ - (void)stopView:(NSString *)key context:(NSDictionary *)context timestampMs:(do [self.ddRumImplementation stopViewWithKey:key context:context timestampMs:timestampMs resolve:resolve reject:reject]; } +- (void) startFeatureOperation:(NSString *)name operationKey:(NSString *)operationKey attributes:(NSDictionary *)attributes resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + [self.ddRumImplementation startFeatureOperationWithName:name operationKey:operationKey attributes:attributes resolve:resolve reject:reject]; +} + +- (void) succeedFeatureOperation:(NSString *)name operationKey:(NSString *)operationKey attributes:(NSDictionary *)attributes resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + [self.ddRumImplementation succeedFeatureOperationWithName:name operationKey:operationKey attributes:attributes resolve:resolve reject:reject]; +} + +- (void) failFeatureOperation:(NSString *)name operationKey:(NSString *)operationKey reason:(NSString *)reason attributes:(NSDictionary *)attributes resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + [self.ddRumImplementation failFeatureOperationWithName:name operationKey:operationKey reason:reason attributes:attributes resolve:resolve reject:reject]; +} + @end diff --git a/packages/core/ios/Sources/DdRumImplementation.swift b/packages/core/ios/Sources/DdRumImplementation.swift index 6fac21f82..0ac8a19bf 100644 --- a/packages/core/ios/Sources/DdRumImplementation.swift +++ b/packages/core/ios/Sources/DdRumImplementation.swift @@ -63,6 +63,16 @@ private extension RUMMethod { } } +internal extension RUMFeatureOperationFailureReason { + init(from string: String) { + switch string.lowercased() { + case "error": self = .error + case "abandoned": self = .abandoned + default: self = .other + } + } +} + @objc public class DdRumImplementation: NSObject { internal static let timestampKey = "_dd.timestamp" @@ -236,6 +246,47 @@ public class DdRumImplementation: NSObject { resolve(sessionId) } } + + @objc + public func startFeatureOperation( + name: String, + operationKey: String?, + attributes: NSDictionary, + resolve: @escaping (Any?) -> Void, + reject: RCTPromiseRejectBlock + ){ + let castedAttributes = castAttributesToSwift(attributes) + nativeRUM.startFeatureOperation(name: name, operationKey: operationKey, attributes: castedAttributes) + resolve(nil) + } + + @objc + public func succeedFeatureOperation( + name: String, + operationKey: String?, + attributes: NSDictionary, + resolve: @escaping (Any?) -> Void, + reject: RCTPromiseRejectBlock + ){ + let castedAttributes = castAttributesToSwift(attributes) + nativeRUM.succeedFeatureOperation(name: name, operationKey: operationKey, attributes: castedAttributes) + resolve(nil) + } + + @objc + public func failFeatureOperation( + name: String, + operationKey: String?, + reason: String, + attributes: NSDictionary, + resolve: @escaping (Any?) -> Void, + reject: RCTPromiseRejectBlock + ){ + let castedAttributes = castAttributesToSwift(attributes) + nativeRUM.failFeatureOperation(name: name, operationKey: operationKey, + reason: RUMFeatureOperationFailureReason(from: reason), attributes: castedAttributes) + resolve(nil) + } // MARK: - Private methods diff --git a/packages/core/jest/mock.js b/packages/core/jest/mock.js index 9f5ee2c41..aadc79d27 100644 --- a/packages/core/jest/mock.js +++ b/packages/core/jest/mock.js @@ -154,6 +154,15 @@ module.exports = { .mockImplementation( () => new Promise(resolve => resolve('test-session-id')) ), + startFeatureOperation: jest + .fn() + .mockImplementation(() => new Promise(resolve => resolve())), + succeedFeatureOperation: jest + .fn() + .mockImplementation(() => new Promise(resolve => resolve())), + failFeatureOperation: jest + .fn() + .mockImplementation(() => new Promise() < (resolve => resolve())), setTimeProvider: jest.fn().mockImplementation(() => {}), timeProvider: jest.fn().mockReturnValue(undefined), getTracingContext: jest.fn().mockReturnValue(undefined), diff --git a/packages/core/src/index.tsx b/packages/core/src/index.tsx index 062fecc90..ce39c9dfa 100644 --- a/packages/core/src/index.tsx +++ b/packages/core/src/index.tsx @@ -42,7 +42,7 @@ import { DatadogProvider } from './sdk/DatadogProvider/DatadogProvider'; import { DdSdk } from './sdk/DdSdk'; import { FileBasedConfiguration } from './sdk/FileBasedConfiguration/FileBasedConfiguration'; import { DdTrace } from './trace/DdTrace'; -import { ErrorSource } from './types'; +import { ErrorSource, FeatureOperationFailure } from './types'; import { DefaultTimeProvider } from './utils/time-provider/DefaultTimeProvider'; import type { Timestamp } from './utils/time-provider/TimeProvider'; import { TimeProvider } from './utils/time-provider/TimeProvider'; @@ -57,6 +57,7 @@ export { DdRum, RumActionType, ErrorSource, + FeatureOperationFailure, DdSdkReactNativeConfiguration, DdSdkReactNative, DdSdk, diff --git a/packages/core/src/rum/DdRum.ts b/packages/core/src/rum/DdRum.ts index 1f3703e63..3ae7c10ce 100644 --- a/packages/core/src/rum/DdRum.ts +++ b/packages/core/src/rum/DdRum.ts @@ -13,7 +13,7 @@ import type { Attributes } from '../sdk/AttributesSingleton/types'; import { bufferVoidNativeCall } from '../sdk/DatadogProvider/Buffer/bufferNativeCall'; import { DdSdk } from '../sdk/DdSdk'; import { GlobalState } from '../sdk/GlobalState/GlobalState'; -import type { ErrorSource } from '../types'; +import type { ErrorSource, FeatureOperationFailure } from '../types'; import { validateContext } from '../utils/argsUtils'; import { getErrorContext } from '../utils/errorUtils'; import { getGlobalInstance } from '../utils/singletonUtils'; @@ -133,6 +133,58 @@ class DdRumWrapper implements DdRumType { return this.callNativeStopAction(...nativeCallArgs); }; + startFeatureOperation( + name: string, + operationKey: string | null, + attributes: object + ): Promise { + InternalLog.log( + `Starting feature operation “${name}” (${operationKey})`, + SdkVerbosity.DEBUG + ); + return bufferVoidNativeCall(() => + this.nativeRum.startFeatureOperation(name, operationKey, attributes) + ); + } + + succeedFeatureOperation( + name: string, + operationKey: string | null, + attributes: object + ): Promise { + InternalLog.log( + `Succeding feature operation “${name}” (${operationKey})`, + SdkVerbosity.DEBUG + ); + return bufferVoidNativeCall(() => + this.nativeRum.succeedFeatureOperation( + name, + operationKey, + attributes + ) + ); + } + + failFeatureOperation( + name: string, + operationKey: string | null, + reason: FeatureOperationFailure, + attributes: object + ): Promise { + InternalLog.log( + `Failing feature operation “${name}” (${operationKey})`, + SdkVerbosity.DEBUG + ); + return bufferVoidNativeCall(() => + this.nativeRum.failFeatureOperation( + name, + operationKey, + reason, + attributes + ) + ); + } + setTimeProvider = (timeProvider: TimeProvider): void => { this.timeProvider = timeProvider; }; diff --git a/packages/core/src/rum/types.ts b/packages/core/src/rum/types.ts index 3def7f0e6..b879010f7 100644 --- a/packages/core/src/rum/types.ts +++ b/packages/core/src/rum/types.ts @@ -5,7 +5,7 @@ */ import type { Attributes } from '../sdk/AttributesSingleton/types'; -import type { ErrorSource } from '../types'; +import type { ErrorSource, FeatureOperationFailure } from '../types'; import type { DatadogTracingContext } from './instrumentation/resourceTracking/distributedTracing/DatadogTracingContext'; import type { DatadogTracingIdentifier } from './instrumentation/resourceTracking/distributedTracing/DatadogTracingIdentifier'; @@ -230,6 +230,46 @@ export type DdRumType = { * Generates a unique 128bit Span ID. */ generateSpanId(): DatadogTracingIdentifier; + + /** + * Starts a Feature Operation, representing a high-level logical flow within your application (e.g., `login_flow`). + * @param name - The name of the feature operation (for example, `"login_flow"`). + * @param operationKey - An optional key to uniquely identify a specific instance of this operation when multiple are running concurrently. + * @param attributes - Custom attributes to attach to this operation. + */ + startFeatureOperation( + name: string, + operationKey: string | null, + attributes: object + ): Promise; + + /** + * Marks a Feature Operation as successfully completed. + * Should be called when a previously started operation (via `startFeatureOperation`) finishes without error. + * @param name - The name of the feature operation (for example, `"login_flow"`). + * @param operationKey - The key for the operation instance to complete, if it was specified when starting it. + * @param attributes - Custom attributes to attach to this operation’s completion event. + */ + succeedFeatureOperation( + name: string, + operationKey: string | null, + attributes: object + ): Promise; + + /** + * Marks a Feature Operation as failed. + * Should be called when a previously started operation (via `startFeatureOperation`) ends with an error. + * @param name - The name of the feature operation (for example, `"login_flow"`). + * @param operationKey - The key for the operation instance to fail, if it was specified when starting it. + * @param reason - The reason for the failure. + * @param attributes - Custom attributes to attach to this operation’s failure event. + */ + failFeatureOperation( + name: string, + operationKey: string | null, + reason: FeatureOperationFailure, + attributes: object + ): Promise; }; /** diff --git a/packages/core/src/specs/NativeDdRum.ts b/packages/core/src/specs/NativeDdRum.ts index e31f5b925..9c0c459b3 100644 --- a/packages/core/src/specs/NativeDdRum.ts +++ b/packages/core/src/specs/NativeDdRum.ts @@ -185,6 +185,46 @@ export interface Spec extends TurboModule { * Get current Session ID, or `undefined` if not available. */ getCurrentSessionId(): Promise; + + /** + * Starts a Feature Operation, representing a high-level logical flow within your application (e.g., `login_flow`). + * @param name - The name of the feature operation (for example, `"login_flow"`). + * @param operationKey - An optional key to uniquely identify a specific instance of this operation when multiple are running concurrently. + * @param attributes - Custom attributes to attach to this operation. + */ + startFeatureOperation( + name: string, + operationKey: string | null, + attributes: Object + ): Promise; + + /** + * Marks a Feature Operation as successfully completed. + * Should be called when a previously started operation (via `startFeatureOperation`) finishes without error. + * @param name - The name of the feature operation (for example, `"login_flow"`). + * @param operationKey - The key for the operation instance to complete, if it was specified when starting it. + * @param attributes - Custom attributes to attach to this operation’s completion event. + */ + succeedFeatureOperation( + name: string, + operationKey: string | null, + attributes: Object + ): Promise; + + /** + * Marks a Feature Operation as failed. + * Should be called when a previously started operation (via `startFeatureOperation`) ends with an error. + * @param name - The name of the feature operation (for example, `"login_flow"`). + * @param operationKey - The key for the operation instance to fail, if it was specified when starting it. + * @param reason - The reason for the failure. + * @param attributes - Custom attributes to attach to this operation’s failure event. + */ + failFeatureOperation( + name: string, + operationKey: string | null, + reason: string, + attributes: Object + ): Promise; } // eslint-disable-next-line import/no-default-export diff --git a/packages/core/src/types.tsx b/packages/core/src/types.tsx index 7f97779ee..cd697c04b 100644 --- a/packages/core/src/types.tsx +++ b/packages/core/src/types.tsx @@ -232,3 +232,9 @@ export enum ErrorSource { WEBVIEW = 'WEBVIEW', CUSTOM = 'CUSTOM' } + +export enum FeatureOperationFailure { + ERROR = 'ERROR', + ABANDONED = 'ABANDONED', + OTHER = 'OTHER' +} diff --git a/packages/react-native-apollo-client/__mocks__/react-native.ts b/packages/react-native-apollo-client/__mocks__/react-native.ts index bbac607d3..ded32499f 100644 --- a/packages/react-native-apollo-client/__mocks__/react-native.ts +++ b/packages/react-native-apollo-client/__mocks__/react-native.ts @@ -110,7 +110,16 @@ actualRN.NativeModules.DdRum = { new Promise(resolve => resolve('test-session-id') ) - ) as jest.MockedFunction + ) as jest.MockedFunction, + startFeatureOperation: jest.fn().mockImplementation( + () => new Promise(resolve => resolve()) + ) as jest.MockedFunction, + succeedFeatureOperation: jest.fn().mockImplementation( + () => new Promise(resolve => resolve()) + ) as jest.MockedFunction, + failFeatureOperation: jest.fn().mockImplementation( + () => new Promise(resolve => resolve()) + ) as jest.MockedFunction }; module.exports = actualRN; From 0af85d385ae1213cd0d26e3dc77df2e81e488a87 Mon Sep 17 00:00:00 2001 From: "Xavier F. Gouchet" Date: Mon, 7 Apr 2025 14:55:53 +0200 Subject: [PATCH 056/410] RUM-9023 use session id to sample network traces --- .../distributedTracing/distributedTracing.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/core/src/rum/instrumentation/resourceTracking/distributedTracing/distributedTracing.tsx b/packages/core/src/rum/instrumentation/resourceTracking/distributedTracing/distributedTracing.tsx index e529f3cd1..9c4fcbff7 100644 --- a/packages/core/src/rum/instrumentation/resourceTracking/distributedTracing/distributedTracing.tsx +++ b/packages/core/src/rum/instrumentation/resourceTracking/distributedTracing/distributedTracing.tsx @@ -77,7 +77,12 @@ export const generateTracingAttributesWithSampling = ( } const traceId = TracingIdentifier.createTraceId(); - const hash = Number(traceId.id.multiply(knuthFactor).remainder(twoPow64)); + // for a UUID with value aaaaaaaa-bbbb-Mccc-Nddd-1234567890ab + // we use as the input id the last part : 0x1234567890ab + const baseId = rumSessionId + ? BigInt(rumSessionId.split('-')[4], 16) + : traceId.id; + const hash = Number(baseId.multiply(knuthFactor).remainder(twoPow64)); const threshold = (tracingSamplingRate / 100) * Number(twoPow64); const isSampled = hash <= threshold; From 45cc7892dbdd421aaccf4f626cfb15115c9c0cc7 Mon Sep 17 00:00:00 2001 From: "Xavier F. Gouchet" Date: Mon, 28 Apr 2025 13:55:23 +0200 Subject: [PATCH 057/410] RUM-7747 update default tracing sampling rate --- packages/core/ios/Sources/RNDdSdkConfiguration.swift | 2 +- packages/core/src/DdSdkReactNativeConfiguration.tsx | 2 +- .../src/__tests__/DdSdkReactNativeConfiguration.test.ts | 6 +++--- .../sdk/DatadogProvider/__tests__/initialization.test.tsx | 2 +- .../__tests__/FileBasedConfiguration.test.ts | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/core/ios/Sources/RNDdSdkConfiguration.swift b/packages/core/ios/Sources/RNDdSdkConfiguration.swift index a765e9759..66437c481 100644 --- a/packages/core/ios/Sources/RNDdSdkConfiguration.swift +++ b/packages/core/ios/Sources/RNDdSdkConfiguration.swift @@ -194,7 +194,7 @@ extension NSArray { internal struct DefaultConfiguration { static let nativeCrashReportEnabled = false static let sessionSamplingRate = 100.0 - static let resourceTracingSamplingRate = 20.0 + static let resourceTracingSamplingRate = 100.0 static let longTaskThresholdMs = 0.0 static let nativeLongTaskThresholdMs = 200.0 static let nativeViewTracking = false diff --git a/packages/core/src/DdSdkReactNativeConfiguration.tsx b/packages/core/src/DdSdkReactNativeConfiguration.tsx index 158d258eb..9be08fa28 100644 --- a/packages/core/src/DdSdkReactNativeConfiguration.tsx +++ b/packages/core/src/DdSdkReactNativeConfiguration.tsx @@ -109,7 +109,7 @@ export const formatFirstPartyHosts = ( export const DEFAULTS = { nativeCrashReportEnabled: false, sessionSamplingRate: 100.0, - resourceTracingSamplingRate: 20.0, + resourceTracingSamplingRate: 100.0, site: 'US1', longTaskThresholdMs: 0, nativeLongTaskThresholdMs: 200, diff --git a/packages/core/src/__tests__/DdSdkReactNativeConfiguration.test.ts b/packages/core/src/__tests__/DdSdkReactNativeConfiguration.test.ts index 6d9d081af..b1522ef7f 100644 --- a/packages/core/src/__tests__/DdSdkReactNativeConfiguration.test.ts +++ b/packages/core/src/__tests__/DdSdkReactNativeConfiguration.test.ts @@ -51,7 +51,7 @@ describe('DdSdkReactNativeConfiguration', () => { "nativeViewTracking": false, "proxyConfig": undefined, "resourceEventMapper": null, - "resourceTracingSamplingRate": 20, + "resourceTracingSamplingRate": 100, "serviceName": undefined, "sessionSamplingRate": 100, "site": "US1", @@ -79,7 +79,7 @@ describe('DdSdkReactNativeConfiguration', () => { trackInteractions: true, trackResources: true, firstPartyHosts: ['api.com'], - resourceTracingSamplingRate: 100, + resourceTracingSamplingRate: 80, logEventMapper: event => event, errorEventMapper: event => event, resourceEventMapper: event => event, @@ -161,7 +161,7 @@ describe('DdSdkReactNativeConfiguration', () => { "type": "https", }, "resourceEventMapper": [Function], - "resourceTracingSamplingRate": 100, + "resourceTracingSamplingRate": 80, "serviceName": "com.test.app", "sessionSamplingRate": 80, "site": "EU", diff --git a/packages/core/src/sdk/DatadogProvider/__tests__/initialization.test.tsx b/packages/core/src/sdk/DatadogProvider/__tests__/initialization.test.tsx index 590c34b98..c654cd24b 100644 --- a/packages/core/src/sdk/DatadogProvider/__tests__/initialization.test.tsx +++ b/packages/core/src/sdk/DatadogProvider/__tests__/initialization.test.tsx @@ -93,7 +93,7 @@ describe('DatadogProvider', () => { "nativeLongTaskThresholdMs": 200, "nativeViewTracking": false, "proxyConfig": undefined, - "resourceTracingSamplingRate": 20, + "resourceTracingSamplingRate": 100, "sampleRate": 100, "serviceName": undefined, "site": "US1", diff --git a/packages/core/src/sdk/FileBasedConfiguration/__tests__/FileBasedConfiguration.test.ts b/packages/core/src/sdk/FileBasedConfiguration/__tests__/FileBasedConfiguration.test.ts index 523cf67cc..716243e86 100644 --- a/packages/core/src/sdk/FileBasedConfiguration/__tests__/FileBasedConfiguration.test.ts +++ b/packages/core/src/sdk/FileBasedConfiguration/__tests__/FileBasedConfiguration.test.ts @@ -134,7 +134,7 @@ describe('FileBasedConfiguration', () => { "nativeViewTracking": false, "proxyConfig": undefined, "resourceEventMapper": null, - "resourceTracingSamplingRate": 20, + "resourceTracingSamplingRate": 100, "serviceName": undefined, "sessionSamplingRate": 100, "site": "US1", From 8683f6c297a14ba82f314d9f63cdeb03bf043d50 Mon Sep 17 00:00:00 2001 From: Sergio Barrio Date: Tue, 2 Sep 2025 10:49:34 +0200 Subject: [PATCH 058/410] Remove fatal errors from logs --- .../DdRumErrorTracking.test.tsx | 202 +----------------- .../core/src/logs/__tests__/DdLogs.test.ts | 4 +- .../instrumentation/DdRumErrorTracking.tsx | 41 +--- 3 files changed, 14 insertions(+), 233 deletions(-) diff --git a/packages/core/src/__tests__/rum/instrumentation/DdRumErrorTracking.test.tsx b/packages/core/src/__tests__/rum/instrumentation/DdRumErrorTracking.test.tsx index 7f66bb58e..fc34de5b0 100644 --- a/packages/core/src/__tests__/rum/instrumentation/DdRumErrorTracking.test.tsx +++ b/packages/core/src/__tests__/rum/instrumentation/DdRumErrorTracking.test.tsx @@ -6,17 +6,13 @@ import { NativeModules } from 'react-native'; -import type { - DdNativeLogsType, - DdNativeRumType -} from '../../../nativeModulesTypes'; +import type { DdNativeRumType } from '../../../nativeModulesTypes'; import { DdRumErrorTracking } from '../../../rum/instrumentation/DdRumErrorTracking'; import { BufferSingleton } from '../../../sdk/DatadogProvider/Buffer/BufferSingleton'; jest.mock('../../../utils/jsUtils'); const DdRum = NativeModules.DdRum as DdNativeRumType; -const DdLogs = NativeModules.DdLogs as DdNativeLogsType; let baseErrorHandlerCalled = false; const baseErrorHandler = (error: any, isFatal?: boolean) => { @@ -77,19 +73,6 @@ it('M intercept and send a RUM event W onGlobalError() {no message}', async () = '' ); expect(baseErrorHandlerCalled).toStrictEqual(true); - expect(DdLogs.errorWithError).toHaveBeenCalledTimes(1); - expect(DdLogs.errorWithError).toHaveBeenCalledWith( - '[object Object]', - 'Error', - '[object Object]', - 'doSomething() at ./path/to/file.js:67:3', - { - '_dd.error.raw': error, - '_dd.error.is_crash': is_fatal, - '_dd.error.source_type': 'react-native', - '_dd.error_log.is_crash': true - } - ); }); it('M intercept and send a RUM event W onGlobalError() {empty stack trace}', async () => { @@ -119,19 +102,6 @@ it('M intercept and send a RUM event W onGlobalError() {empty stack trace}', asy '' ); expect(baseErrorHandlerCalled).toStrictEqual(true); - expect(DdLogs.errorWithError).toHaveBeenCalledTimes(1); - expect(DdLogs.errorWithError).toHaveBeenCalledWith( - 'Something bad happened', - 'Error', - 'Something bad happened', - '', - { - '_dd.error.raw': error, - '_dd.error.is_crash': is_fatal, - '_dd.error.source_type': 'react-native', - '_dd.error_log.is_crash': true - } - ); }); it('M intercept and send a RUM event W onGlobalError() {Error object}', async () => { @@ -162,19 +132,6 @@ it('M intercept and send a RUM event W onGlobalError() {Error object}', async () '/packages/core/src/__tests__/rum/instrumentation/DdRumErrorTracking.test.tsx' ); expect(baseErrorHandlerCalled).toStrictEqual(true); - expect(DdLogs.errorWithError).toHaveBeenCalledTimes(1); - expect(DdLogs.errorWithError).toHaveBeenCalledWith( - 'Something bad happened', - 'Error', - 'Something bad happened', - expect.stringContaining('Error: Something bad happened'), - { - '_dd.error.raw': error, - '_dd.error.is_crash': is_fatal, - '_dd.error.source_type': 'react-native', - '_dd.error_log.is_crash': true - } - ); }); it('M intercept and send a RUM event W onGlobalError() {CustomError object}', async () => { @@ -209,19 +166,6 @@ it('M intercept and send a RUM event W onGlobalError() {CustomError object}', as '/packages/core/src/__tests__/rum/instrumentation/DdRumErrorTracking.test.tsx' ); expect(baseErrorHandlerCalled).toStrictEqual(true); - expect(DdLogs.errorWithError).toHaveBeenCalledTimes(1); - expect(DdLogs.errorWithError).toHaveBeenCalledWith( - 'Something bad happened', - 'CustomError', - 'Something bad happened', - expect.stringContaining('Error: Something bad happened'), - { - '_dd.error.raw': error, - '_dd.error.is_crash': is_fatal, - '_dd.error.source_type': 'react-native', - '_dd.error_log.is_crash': true - } - ); }); it('M intercept and send a RUM event W onGlobalError() {with source file info}', async () => { @@ -254,19 +198,6 @@ it('M intercept and send a RUM event W onGlobalError() {with source file info}', '' ); expect(baseErrorHandlerCalled).toStrictEqual(true); - expect(DdLogs.errorWithError).toHaveBeenCalledTimes(1); - expect(DdLogs.errorWithError).toHaveBeenCalledWith( - 'Something bad happened', - 'Error', - 'Something bad happened', - 'at ./path/to/file.js:1038:57', - { - '_dd.error.raw': error, - '_dd.error.is_crash': is_fatal, - '_dd.error.source_type': 'react-native', - '_dd.error_log.is_crash': true - } - ); }); it('M intercept and send a RUM event W onGlobalError() {with component stack}', async () => { @@ -301,19 +232,6 @@ it('M intercept and send a RUM event W onGlobalError() {with component stack}', '' ); expect(baseErrorHandlerCalled).toStrictEqual(true); - expect(DdLogs.errorWithError).toHaveBeenCalledTimes(1); - expect(DdLogs.errorWithError).toHaveBeenCalledWith( - 'Something bad happened', - 'Error', - 'Something bad happened', - 'doSomething() at ./path/to/file.js:67:3,nestedCall() at ./path/to/file.js:1064:9,root() at ./path/to/index.js:10:1', - { - '_dd.error.raw': error, - '_dd.error.is_crash': is_fatal, - '_dd.error.source_type': 'react-native', - '_dd.error_log.is_crash': true - } - ); }); it('M intercept and send a RUM event W onGlobalError() {with stack}', async () => { @@ -348,19 +266,6 @@ it('M intercept and send a RUM event W onGlobalError() {with stack}', async () = '' ); expect(baseErrorHandlerCalled).toStrictEqual(true); - expect(DdLogs.errorWithError).toHaveBeenCalledTimes(1); - expect(DdLogs.errorWithError).toHaveBeenCalledWith( - 'Something bad happened', - 'Error', - 'Something bad happened', - 'doSomething() at ./path/to/file.js:67:3,nestedCall() at ./path/to/file.js:1064:9,root() at ./path/to/index.js:10:1', - { - '_dd.error.raw': error, - '_dd.error.is_crash': is_fatal, - '_dd.error.source_type': 'react-native', - '_dd.error_log.is_crash': true - } - ); }); it('M intercept and send a RUM event W onGlobalError() {with stacktrace}', async () => { @@ -396,19 +301,6 @@ it('M intercept and send a RUM event W onGlobalError() {with stacktrace}', async ); expect(baseErrorHandlerCalled).toStrictEqual(true); expect(baseErrorHandlerCalled).toStrictEqual(true); - expect(DdLogs.errorWithError).toHaveBeenCalledTimes(1); - expect(DdLogs.errorWithError).toHaveBeenCalledWith( - 'Something bad happened', - 'Error', - 'Something bad happened', - 'doSomething() at ./path/to/file.js:67:3,nestedCall() at ./path/to/file.js:1064:9,root() at ./path/to/index.js:10:1', - { - '_dd.error.raw': error, - '_dd.error.is_crash': is_fatal, - '_dd.error.source_type': 'react-native', - '_dd.error_log.is_crash': true - } - ); }); it('M not report error in console handler W onGlobalError() {with console reporting handler}', async () => { @@ -450,19 +342,6 @@ it('M not report error in console handler W onGlobalError() {with console report expect(consoleReportingErrorHandler).toBeCalledTimes(1); expect(baseConsoleErrorCalled).toStrictEqual(false); expect(baseErrorHandlerCalled).toStrictEqual(true); - expect(DdLogs.errorWithError).toHaveBeenCalledTimes(1); - expect(DdLogs.errorWithError).toHaveBeenCalledWith( - 'Something bad happened', - 'Error', - 'Something bad happened', - 'doSomething() at ./path/to/file.js:67:3,nestedCall() at ./path/to/file.js:1064:9,root() at ./path/to/index.js:10:1', - { - '_dd.error.raw': error, - '_dd.error.is_crash': is_fatal, - '_dd.error.source_type': 'react-native', - '_dd.error_log.is_crash': true - } - ); }); it('M intercept and send a RUM event W onConsole() {Error with source file info}', async () => { @@ -493,17 +372,6 @@ it('M intercept and send a RUM event W onConsole() {Error with source file info} '' ); expect(baseConsoleErrorCalled).toStrictEqual(true); - expect(DdLogs.errorWithError).toHaveBeenCalledTimes(1); - expect(DdLogs.errorWithError).toHaveBeenCalledWith( - 'Oops I did it again! Something bad happened', - 'Error', - 'Oops I did it again! Something bad happened', - 'at ./path/to/file.js:1038:57', - { - '_dd.error.source_type': 'react-native', - '_dd.error_log.is_crash': true - } - ); }); it('M intercept and send a RUM event W onConsole() {Error with component stack}', async () => { @@ -536,17 +404,6 @@ it('M intercept and send a RUM event W onConsole() {Error with component stack}' '' ); expect(baseConsoleErrorCalled).toStrictEqual(true); - expect(DdLogs.errorWithError).toHaveBeenCalledTimes(1); - expect(DdLogs.errorWithError).toHaveBeenCalledWith( - 'Oops I did it again! Something bad happened', - 'Error', - 'Oops I did it again! Something bad happened', - 'doSomething() at ./path/to/file.js:67:3,nestedCall() at ./path/to/file.js:1064:9,root() at ./path/to/index.js:10:1', - { - '_dd.error.source_type': 'react-native', - '_dd.error_log.is_crash': true - } - ); }); it('M intercept and send a RUM event W onConsole() {message only}', async () => { @@ -571,17 +428,6 @@ it('M intercept and send a RUM event W onConsole() {message only}', async () => '' ); expect(baseConsoleErrorCalled).toStrictEqual(true); - expect(DdLogs.errorWithError).toHaveBeenCalledTimes(1); - expect(DdLogs.errorWithError).toHaveBeenCalledWith( - 'Something bad happened', - 'Error', - 'Something bad happened', - '', - { - '_dd.error.source_type': 'react-native', - '_dd.error_log.is_crash': true - } - ); }); it('M intercept and send a RUM event W onConsole() {Error with source file and name}', async () => { @@ -613,17 +459,6 @@ it('M intercept and send a RUM event W onConsole() {Error with source file and n '' ); expect(baseConsoleErrorCalled).toStrictEqual(true); - expect(DdLogs.errorWithError).toHaveBeenCalledTimes(1); - expect(DdLogs.errorWithError).toHaveBeenCalledWith( - 'Oops I did it again! Something bad happened', - 'CustomConsoleError', - 'Oops I did it again! Something bad happened', - 'at ./path/to/file.js:1038:57', - { - '_dd.error.source_type': 'react-native', - '_dd.error_log.is_crash': true - } - ); }); describe.each([ @@ -661,17 +496,6 @@ describe.each([ '' ); expect(baseConsoleErrorCalled).toStrictEqual(true); - expect(DdLogs.errorWithError).toHaveBeenCalledTimes(1); - expect(DdLogs.errorWithError).toHaveBeenCalledWith( - errorMessage, - 'Error', - errorMessage, - '', - { - '_dd.error.source_type': 'react-native', - '_dd.error_log.is_crash': true - } - ); }); }); @@ -704,19 +528,6 @@ it('M intercept and send a RUM event W on error() {called from RNErrorHandler}', '/packages/core/src/__tests__/rum/instrumentation/DdRumErrorTracking.test.tsx' ); expect(baseErrorHandlerCalled).toStrictEqual(true); - expect(DdLogs.errorWithError).toHaveBeenCalledTimes(1); - expect(DdLogs.errorWithError).toHaveBeenCalledWith( - 'Something bad happened', - 'Error', - 'Something bad happened', - expect.stringContaining('Error: Something bad happened'), - { - '_dd.error.raw': error, - '_dd.error.is_crash': is_fatal, - '_dd.error.source_type': 'react-native', - '_dd.error_log.is_crash': true - } - ); }); it('M intercept and send a RUM event W onConsole() {called from RNErrorHandler}', async () => { @@ -742,17 +553,6 @@ it('M intercept and send a RUM event W onConsole() {called from RNErrorHandler}' '' ); expect(baseConsoleErrorCalled).toStrictEqual(true); - expect(DdLogs.errorWithError).toHaveBeenCalledTimes(1); - expect(DdLogs.errorWithError).toHaveBeenCalledWith( - 'Oops I did it again!', - 'Error', - 'Oops I did it again!', - '', - { - '_dd.error.source_type': 'react-native', - '_dd.error_log.is_crash': true - } - ); }); /** diff --git a/packages/core/src/logs/__tests__/DdLogs.test.ts b/packages/core/src/logs/__tests__/DdLogs.test.ts index f99280bfe..6a530ec36 100644 --- a/packages/core/src/logs/__tests__/DdLogs.test.ts +++ b/packages/core/src/logs/__tests__/DdLogs.test.ts @@ -225,7 +225,7 @@ describe('DdLogs', () => { console.error('console-error-message'); expect(NativeModules.DdLogs.error).not.toHaveBeenCalled(); expect(InternalLog.log).toHaveBeenCalledWith( - 'error log dropped by log mapper: "console-error-message"', + 'Adding RUM Error “console-error-message”', 'debug' ); @@ -278,7 +278,7 @@ describe('DdLogs', () => { console.error('console-error-message'); expect(NativeModules.DdLogs.error).not.toHaveBeenCalled(); expect(InternalLog.log).toHaveBeenCalledWith( - 'Tracking error log "console-error-message"', + 'Adding RUM Error “console-error-message”', 'debug' ); }); diff --git a/packages/core/src/rum/instrumentation/DdRumErrorTracking.tsx b/packages/core/src/rum/instrumentation/DdRumErrorTracking.tsx index 04b01290c..5a45bdc65 100644 --- a/packages/core/src/rum/instrumentation/DdRumErrorTracking.tsx +++ b/packages/core/src/rum/instrumentation/DdRumErrorTracking.tsx @@ -8,7 +8,6 @@ import type { ErrorHandlerCallback } from 'react-native'; import { InternalLog } from '../../InternalLog'; import { SdkVerbosity } from '../../SdkVerbosity'; -import { DdLogs } from '../../logs/DdLogs'; import { getErrorMessage, getErrorStackTrace, @@ -71,8 +70,7 @@ export class DdRumErrorTracking { static onGlobalError = (error: any, isFatal?: boolean): void => { const message = getErrorMessage(error); const stacktrace = getErrorStackTrace(error); - const errorName = getErrorName(error); - this.reportError(message, ErrorSource.SOURCE, stacktrace, errorName, { + this.reportError(message, ErrorSource.SOURCE, stacktrace, { '_dd.error.is_crash': isFatal, '_dd.error.raw': error }).then(async () => { @@ -131,39 +129,22 @@ export class DdRumErrorTracking { }) .join(' '); - this.reportError(message, ErrorSource.CONSOLE, stack, errorName).then( - () => { - DdRumErrorTracking.defaultConsoleError.apply(console, params); - } - ); + this.reportError(message, ErrorSource.CONSOLE, stack).then(() => { + DdRumErrorTracking.defaultConsoleError.apply(console, params); + }); }; private static reportError = ( message: string, source: ErrorSource, stacktrace: string, - errorName: string, context: object = {} - ): Promise<[void, void]> => { - return Promise.all([ - DdRum.addError( - message, - source, - stacktrace, - getErrorContext(context) - ), - DdLogs.error( - message, - errorName, - message, - stacktrace, - { - ...context, - '_dd.error_log.is_crash': true - }, - undefined, - source - ) - ]); + ): Promise => { + return DdRum.addError( + message, + source, + stacktrace, + getErrorContext(context) + ); }; } From 17bb3b8ce2441f4c8e2cc483a9101ba769797c1b Mon Sep 17 00:00:00 2001 From: Sergio Barrio Date: Tue, 2 Sep 2025 17:06:28 +0200 Subject: [PATCH 059/410] Improve module wrapper singleton creation --- packages/core/src/logs/DdLogs.ts | 4 +- packages/core/src/rum/DdRum.ts | 5 +- packages/core/src/trace/DdTrace.ts | 10 ++- .../utils/__tests__/singletonUtils.test.ts | 75 +++++++++++++++++++ packages/core/src/utils/singletonUtils.ts | 12 +++ 5 files changed, 101 insertions(+), 5 deletions(-) create mode 100644 packages/core/src/utils/__tests__/singletonUtils.test.ts create mode 100644 packages/core/src/utils/singletonUtils.ts diff --git a/packages/core/src/logs/DdLogs.ts b/packages/core/src/logs/DdLogs.ts index 9ac35fa73..e00e4fc0d 100644 --- a/packages/core/src/logs/DdLogs.ts +++ b/packages/core/src/logs/DdLogs.ts @@ -10,6 +10,7 @@ import type { DdNativeLogsType } from '../nativeModulesTypes'; import { DdAttributes } from '../rum/DdAttributes'; import type { ErrorSource } from '../rum/types'; import { validateContext } from '../utils/argsUtils'; +import { getGlobalInstance } from '../utils/singletonUtils'; import { generateEventMapper } from './eventMapper'; import type { @@ -21,6 +22,7 @@ import type { RawLogWithError } from './types'; +const LOGS_MODULE = 'com.datadog.reactnative.logs'; const SDK_NOT_INITIALIZED_MESSAGE = 'DD_INTERNAL_LOG_SENT_BEFORE_SDK_INIT'; const generateEmptyPromise = () => new Promise(resolve => resolve()); @@ -240,4 +242,4 @@ class DdLogsWrapper implements DdLogsType { } } -export const DdLogs = new DdLogsWrapper(); +export const DdLogs = getGlobalInstance(LOGS_MODULE, () => new DdLogsWrapper()); diff --git a/packages/core/src/rum/DdRum.ts b/packages/core/src/rum/DdRum.ts index cbd06f4fe..fdc7a2ec8 100644 --- a/packages/core/src/rum/DdRum.ts +++ b/packages/core/src/rum/DdRum.ts @@ -13,6 +13,7 @@ import { DdSdk } from '../sdk/DdSdk'; import { GlobalState } from '../sdk/GlobalState/GlobalState'; import { validateContext } from '../utils/argsUtils'; import { getErrorContext } from '../utils/errorUtils'; +import { getGlobalInstance } from '../utils/singletonUtils'; import { DefaultTimeProvider } from '../utils/time-provider/DefaultTimeProvider'; import type { TimeProvider } from '../utils/time-provider/TimeProvider'; @@ -44,6 +45,8 @@ import type { PropagatorType } from './types'; +const RUM_MODULE = 'com.datadog.reactnative.rum'; + const generateEmptyPromise = () => new Promise(resolve => resolve()); class DdRumWrapper implements DdRumType { @@ -503,4 +506,4 @@ const isOldStopActionAPI = ( return typeof args[0] === 'object' || typeof args[0] === 'undefined'; }; -export const DdRum = new DdRumWrapper(); +export const DdRum = getGlobalInstance(RUM_MODULE, () => new DdRumWrapper()); diff --git a/packages/core/src/trace/DdTrace.ts b/packages/core/src/trace/DdTrace.ts index b106a97d8..119716914 100644 --- a/packages/core/src/trace/DdTrace.ts +++ b/packages/core/src/trace/DdTrace.ts @@ -13,8 +13,11 @@ import { } from '../sdk/DatadogProvider/Buffer/bufferNativeCall'; import type { DdTraceType } from '../types'; import { validateContext } from '../utils/argsUtils'; +import { getGlobalInstance } from '../utils/singletonUtils'; import { DefaultTimeProvider } from '../utils/time-provider/DefaultTimeProvider'; +const TRACE_MODULE = 'com.datadog.reactnative.trace'; + const timeProvider = new DefaultTimeProvider(); class DdTraceWrapper implements DdTraceType { @@ -59,6 +62,7 @@ class DdTraceWrapper implements DdTraceType { }; } -const DdTrace: DdTraceType = new DdTraceWrapper(); - -export { DdTrace }; +export const DdTrace: DdTraceType = getGlobalInstance( + TRACE_MODULE, + () => new DdTraceWrapper() +); diff --git a/packages/core/src/utils/__tests__/singletonUtils.test.ts b/packages/core/src/utils/__tests__/singletonUtils.test.ts new file mode 100644 index 000000000..f424562c6 --- /dev/null +++ b/packages/core/src/utils/__tests__/singletonUtils.test.ts @@ -0,0 +1,75 @@ +import { getGlobalInstance } from '../singletonUtils'; + +describe('singletonUtils', () => { + const createdSymbols: symbol[] = []; + const g = (globalThis as unknown) as Record; + + afterEach(() => { + for (const symbol of createdSymbols) { + delete g[symbol]; + } + + createdSymbols.length = 0; + jest.restoreAllMocks(); + }); + + it('only creates one instance for the same key', () => { + const key = 'com.datadog.reactnative.test'; + const symbol = Symbol.for(key); + createdSymbols.push(symbol); + + const objectConstructor = jest.fn(() => ({ id: 1 })); + const a = getGlobalInstance(key, objectConstructor); + const b = getGlobalInstance(key, objectConstructor); + + expect(a).toBe(b); + expect(objectConstructor).toHaveBeenCalledTimes(1); + expect(g[symbol]).toBe(a); + }); + + it('returns a pre-existing instance without creating a new one for the same key', () => { + const key = 'com.datadog.reactnative.test'; + const symbol = Symbol.for(key); + createdSymbols.push(symbol); + + const existing = { pre: true }; + g[symbol] = existing; + + const objectConstructor = jest.fn(() => ({ created: true })); + const result = getGlobalInstance(key, objectConstructor); + + expect(result).toBe(existing); + expect(objectConstructor).not.toHaveBeenCalled(); + }); + + it('creates a new instance for a different key', () => { + const keyA = 'com.datadog.reactnative.test.a'; + const keyB = 'com.datadog.reactnative.test.b'; + const symbolA = Symbol.for(keyA); + const symbolB = Symbol.for(keyB); + createdSymbols.push(symbolA, symbolB); + + const a = getGlobalInstance(keyA, () => ({ id: 'A' })); + const b = getGlobalInstance(keyB, () => ({ id: 'B' })); + + expect(a).not.toBe(b); + expect((a as any).id).toBe('A'); + expect((b as any).id).toBe('B'); + }); + + it('does not overwrite existing instance if called with a different constructor', () => { + const key = 'com.datadog.reactnative.test'; + const symbol = Symbol.for(key); + createdSymbols.push(symbol); + + const firstObjectConstructor = jest.fn(() => ({ id: 1 })); + const first = getGlobalInstance(key, firstObjectConstructor); + + const secondObjectConstructor = jest.fn(() => ({ id: 2 })); + const second = getGlobalInstance(key, secondObjectConstructor); + + expect(first).toBe(second); + expect(firstObjectConstructor).toHaveBeenCalledTimes(1); + expect(secondObjectConstructor).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/core/src/utils/singletonUtils.ts b/packages/core/src/utils/singletonUtils.ts new file mode 100644 index 000000000..9f00c2cd0 --- /dev/null +++ b/packages/core/src/utils/singletonUtils.ts @@ -0,0 +1,12 @@ +export const getGlobalInstance = ( + key: string, + objectConstructor: () => T +): T => { + const symbol = Symbol.for(key); + const g = (globalThis as unknown) as Record; + + if (!(symbol in g)) { + g[symbol] = objectConstructor(); + } + return g[symbol] as T; +}; From a957186978690997186f8f8d02aa27d98318a1b3 Mon Sep 17 00:00:00 2001 From: Sergio Barrio Date: Fri, 5 Sep 2025 17:47:29 +0200 Subject: [PATCH 060/410] Use native sdk's core instance instead of the one inside RN SDK wrapper --- .../datadog/reactnative/DatadogSDKWrapper.kt | 90 +------------------ .../com/datadog/reactnative/DatadogWrapper.kt | 48 ---------- .../reactnative/DdLogsImplementation.kt | 3 +- .../reactnative/DdSdkImplementation.kt | 9 +- .../reactnative/DdSdkNativeInitialization.kt | 13 ++- .../reactnative/DdSdkReactNativePackage.kt | 3 +- .../com/datadog/reactnative/DdTelemetry.kt | 56 ++++++++++++ .../kotlin/com/datadog/reactnative/DdSdk.kt | 3 +- .../kotlin/com/datadog/reactnative/DdSdk.kt | 5 +- .../DdSdkNativeInitializationTest.kt | 4 + .../com/datadog/reactnative/DdSdkTest.kt | 6 +- .../DdInternalTestingImplementation.kt | 2 +- .../DdInternalTestingImplementationTest.kt | 74 ++++++++------- .../DdSessionReplayImplementation.kt | 8 +- 14 files changed, 140 insertions(+), 184 deletions(-) create mode 100644 packages/core/android/src/main/kotlin/com/datadog/reactnative/DdTelemetry.kt diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogSDKWrapper.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogSDKWrapper.kt index c9865a5d7..da0841ac4 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogSDKWrapper.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogSDKWrapper.kt @@ -8,23 +8,14 @@ package com.datadog.reactnative import android.content.Context -import android.util.Log import com.datadog.android.Datadog -import com.datadog.android._InternalProxy import com.datadog.android.api.InternalLogger -import com.datadog.android.api.SdkCore import com.datadog.android.api.feature.FeatureSdkCore import com.datadog.android.core.InternalSdkCore import com.datadog.android.core.configuration.Configuration -import com.datadog.android.log.Logs -import com.datadog.android.log.LogsConfiguration import com.datadog.android.privacy.TrackingConsent import com.datadog.android.rum.GlobalRumMonitor -import com.datadog.android.rum.Rum -import com.datadog.android.rum.RumConfiguration import com.datadog.android.rum.RumMonitor -import com.datadog.android.trace.Trace -import com.datadog.android.trace.TraceConfiguration import com.datadog.android.webview.WebViewTracking import com.facebook.react.bridge.ReadableMap @@ -50,50 +41,18 @@ object DatadogSDKWrapperStorage { listener(ddCore) } } - - /** - * Sets instance of core SDK to be used to initialize features. - */ - fun setSdkCore(core: InternalSdkCore?) { - this.core = core - } - - /** - * Returns the core set by setSdkCore or the default core instance by default. - */ - fun getSdkCore(): SdkCore { - core?.let { - return it - } - Log.d( - DatadogSDKWrapperStorage::class.java.canonicalName, - "SdkCore was not set in DatadogSDKWrapperStorage, using default instance." - ) - return Datadog.getInstance() - } } internal class DatadogSDKWrapper : DatadogWrapper { override var bundleLogsWithRum = DefaultConfiguration.bundleLogsWithRum override var bundleLogsWithTraces = DefaultConfiguration.bundleLogsWithTraces - // We use Kotlin backing field here to initialize once the telemetry proxy - // and make sure it is only after SDK is initialized. - private var telemetryProxy: _InternalProxy._TelemetryProxy? = null - get() { - if (field == null && isInitialized()) { - field = Datadog._internalProxy()._telemetry - } - - return field - } - // We use Kotlin backing field here to initialize once the telemetry proxy // and make sure it is only after SDK is initialized. private var webViewProxy: WebViewTracking._InternalWebViewProxy? = null get() { if (field == null && isInitialized()) { - field = WebViewTracking._InternalWebViewProxy(DatadogSDKWrapperStorage.getSdkCore()) + field = WebViewTracking._InternalWebViewProxy(Datadog.getInstance()) } return field @@ -109,20 +68,7 @@ internal class DatadogSDKWrapper : DatadogWrapper { consent: TrackingConsent ) { val core = Datadog.initialize(context, configuration, consent) - DatadogSDKWrapperStorage.setSdkCore(core as InternalSdkCore) - DatadogSDKWrapperStorage.notifyOnInitializedListeners(core) - } - - override fun enableRum(configuration: RumConfiguration) { - Rum.enable(configuration, DatadogSDKWrapperStorage.getSdkCore()) - } - - override fun enableLogs(configuration: LogsConfiguration) { - Logs.enable(configuration, DatadogSDKWrapperStorage.getSdkCore()) - } - - override fun enableTrace(configuration: TraceConfiguration) { - Trace.enable(configuration, DatadogSDKWrapperStorage.getSdkCore()) + DatadogSDKWrapperStorage.notifyOnInitializedListeners(core as InternalSdkCore) } @Deprecated("Use setUserInfo instead; the user ID is now required.") @@ -161,34 +107,6 @@ internal class DatadogSDKWrapper : DatadogWrapper { Datadog.setTrackingConsent(trackingConsent) } - override fun sendTelemetryLog(message: String, attributes: ReadableMap, config: ReadableMap) { - val core = DatadogSDKWrapperStorage.getSdkCore() as FeatureSdkCore? - val logger = core?.internalLogger; - - val additionalProperties = attributes.toMap() - val telemetryConfig = config.toMap() - - logger?.log( - level = InternalLogger.Level.INFO, - target = InternalLogger.Target.TELEMETRY, - messageBuilder = { message }, - onlyOnce = (telemetryConfig["onlyOnce"] as? Boolean) ?: true, - additionalProperties = additionalProperties - ) - } - - override fun telemetryDebug(message: String) { - telemetryProxy?.debug(message) - } - - override fun telemetryError(message: String, stack: String?, kind: String?) { - telemetryProxy?.error(message, stack, kind) - } - - override fun telemetryError(message: String, throwable: Throwable?) { - telemetryProxy?.error(message, throwable) - } - override fun consumeWebviewEvent(message: String) { webViewProxy?.consumeWebviewEvent(message) } @@ -198,11 +116,11 @@ internal class DatadogSDKWrapper : DatadogWrapper { } override fun getRumMonitor(): RumMonitor { - return GlobalRumMonitor.get(DatadogSDKWrapperStorage.getSdkCore()) + return GlobalRumMonitor.get(Datadog.getInstance()) } override fun clearAllData() { - return Datadog.clearAllData(DatadogSDKWrapperStorage.getSdkCore()) + return Datadog.clearAllData(Datadog.getInstance()) } } diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogWrapper.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogWrapper.kt index 41e86f4d5..9ac591d80 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogWrapper.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogWrapper.kt @@ -64,33 +64,6 @@ interface DatadogWrapper { consent: TrackingConsent ) - /** - * Enables the RUM feature of the SDK. - * - * @param configuration the configuration for the RUM feature - */ - fun enableRum( - configuration: RumConfiguration - ) - - /** - * Enables the Logs feature of the SDK. - * - * @param configuration the configuration for the Logs feature - */ - fun enableLogs( - configuration: LogsConfiguration - ) - - /** - * Enables the Trace feature of the SDK. - * - * @param configuration the configuration for the Trace feature - */ - fun enableTrace( - configuration: TraceConfiguration - ) - /** * Sets the user information. * @@ -144,27 +117,6 @@ interface DatadogWrapper { */ fun setTrackingConsent(trackingConsent: TrackingConsent) - - /** - * Sends telemetry event with attributes. - */ - fun sendTelemetryLog(message: String, attributes: ReadableMap, config: ReadableMap) - - /** - * Sends telemetry debug event. - */ - fun telemetryDebug(message: String) - - /** - * Sends telemetry error. - */ - fun telemetryError(message: String, stack: String?, kind: String?) - - /** - * Sends telemetry error. - */ - fun telemetryError(message: String, throwable: Throwable?) - /** * Sends Webview events. */ diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdLogsImplementation.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdLogsImplementation.kt index 9a84496ec..2f9bceff2 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdLogsImplementation.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdLogsImplementation.kt @@ -7,6 +7,7 @@ package com.datadog.reactnative import android.util.Log as AndroidLog +import com.datadog.android.Datadog import com.datadog.android.log.Logger import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReadableMap @@ -22,7 +23,7 @@ class DdLogsImplementation( val bundleLogsWithRum = datadog.bundleLogsWithRum val bundleLogsWithTraces = datadog.bundleLogsWithTraces - logger ?: Logger.Builder(DatadogSDKWrapperStorage.getSdkCore()) + logger ?: Logger.Builder(Datadog.getInstance()) .setLogcatLogsEnabled(true) .setBundleWithRumEnabled(bundleLogsWithRum) .setBundleWithTraceEnabled(bundleLogsWithTraces) diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt index cdd6b0614..b04a2ddf3 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt @@ -24,6 +24,7 @@ import java.util.concurrent.atomic.AtomicBoolean class DdSdkImplementation( private val reactContext: ReactApplicationContext, private val datadog: DatadogWrapper = DatadogSDKWrapper(), + private val ddTelemetry: DdTelemetry = DdTelemetry(), private val uiThreadExecutor: UiThreadExecutor = ReactUiThreadExecutor() ) { internal val appContext: Context = reactContext.applicationContext @@ -39,7 +40,7 @@ class DdSdkImplementation( fun initialize(configuration: ReadableMap, promise: Promise) { val ddSdkConfiguration = configuration.asDdSdkConfiguration() - val nativeInitialization = DdSdkNativeInitialization(appContext, datadog) + val nativeInitialization = DdSdkNativeInitialization(appContext, datadog, ddTelemetry) nativeInitialization.initialize(ddSdkConfiguration) this.frameRateProvider = createFrameRateProvider(ddSdkConfiguration) @@ -145,7 +146,7 @@ class DdSdkImplementation( * @param config Configuration object, can take 'onlyOnce: Boolean' */ fun sendTelemetryLog(message: String, attributes: ReadableMap, config: ReadableMap, promise: Promise) { - datadog.sendTelemetryLog(message, attributes, config) + ddTelemetry.sendTelemetryLog(message, attributes, config) promise.resolve(null) } @@ -154,7 +155,7 @@ class DdSdkImplementation( * @param message Debug message. */ fun telemetryDebug(message: String, promise: Promise) { - datadog.telemetryDebug(message) + ddTelemetry.telemetryDebug(message) promise.resolve(null) } @@ -165,7 +166,7 @@ class DdSdkImplementation( * @param kind Error kind. */ fun telemetryError(message: String, stack: String, kind: String, promise: Promise) { - datadog.telemetryError(message, stack, kind) + ddTelemetry.telemetryError(message, stack, kind) promise.resolve(null) } diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkNativeInitialization.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkNativeInitialization.kt index 9c8e8370d..ee55d08fe 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkNativeInitialization.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkNativeInitialization.kt @@ -9,14 +9,17 @@ package com.datadog.reactnative import android.content.Context import android.content.pm.PackageManager import android.util.Log +import com.datadog.android.Datadog import com.datadog.android.DatadogSite import com.datadog.android.core.configuration.BatchProcessingLevel import com.datadog.android.core.configuration.BatchSize import com.datadog.android.core.configuration.Configuration import com.datadog.android.core.configuration.UploadFrequency import com.datadog.android.event.EventMapper +import com.datadog.android.log.Logs import com.datadog.android.log.LogsConfiguration import com.datadog.android.privacy.TrackingConsent +import com.datadog.android.rum.Rum import com.datadog.android.rum.RumConfiguration import com.datadog.android.rum._RumInternalProxy import com.datadog.android.rum.configuration.VitalsUpdateFrequency @@ -25,6 +28,7 @@ import com.datadog.android.rum.model.ActionEvent import com.datadog.android.rum.model.ResourceEvent import com.datadog.android.rum.tracking.ActivityViewTrackingStrategy import com.datadog.android.telemetry.model.TelemetryConfigurationEvent +import com.datadog.android.trace.Trace import com.datadog.android.trace.TraceConfiguration import com.google.gson.Gson import java.util.Locale @@ -37,6 +41,7 @@ import kotlin.time.Duration.Companion.seconds class DdSdkNativeInitialization internal constructor( private val appContext: Context, private val datadog: DatadogWrapper = DatadogSDKWrapper(), + private val ddTelemetry: DdTelemetry = DdTelemetry(), private val jsonFileReader: JSONFileReader = JSONFileReader() ) { internal fun initialize(ddSdkConfiguration: DdSdkConfiguration) { @@ -59,11 +64,11 @@ class DdSdkNativeInitialization internal constructor( datadog.initialize(appContext, sdkConfiguration, trackingConsent) - datadog.enableRum(rumConfiguration) + Rum.enable(rumConfiguration, Datadog.getInstance()) - datadog.enableTrace(traceConfiguration) + Logs.enable(logsConfiguration, Datadog.getInstance()) - datadog.enableLogs(logsConfiguration) + Trace.enable(traceConfiguration, Datadog.getInstance()) } private fun configureRumAndTracesForLogs(configuration: DdSdkConfiguration) { @@ -95,7 +100,7 @@ class DdSdkNativeInitialization internal constructor( try { appContext.packageManager.getPackageInfo(packageName, 0) } catch (e: PackageManager.NameNotFoundException) { - datadog.telemetryError(e.message ?: DdSdkImplementation.PACKAGE_INFO_NOT_FOUND_ERROR_MESSAGE, e) + ddTelemetry.telemetryError(e.message ?: DdSdkImplementation.PACKAGE_INFO_NOT_FOUND_ERROR_MESSAGE, e) return DdSdkImplementation.DEFAULT_APP_VERSION } diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkReactNativePackage.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkReactNativePackage.kt index bac9f49f5..3a5b022c1 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkReactNativePackage.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkReactNativePackage.kt @@ -18,9 +18,10 @@ import com.facebook.react.module.model.ReactModuleInfoProvider */ class DdSdkReactNativePackage : TurboReactPackage() { private val sdkWrapper = DatadogSDKWrapper() + private val ddTelemetry = DdTelemetry() override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? { return when (name) { - DdSdkImplementation.NAME -> DdSdk(reactContext, sdkWrapper) + DdSdkImplementation.NAME -> DdSdk(reactContext, sdkWrapper, ddTelemetry) DdRumImplementation.NAME -> DdRum(reactContext, sdkWrapper) DdTraceImplementation.NAME -> DdTrace(reactContext) DdLogsImplementation.NAME -> DdLogs(reactContext, sdkWrapper) diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdTelemetry.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdTelemetry.kt new file mode 100644 index 000000000..2d60df004 --- /dev/null +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdTelemetry.kt @@ -0,0 +1,56 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.reactnative + +import com.datadog.android.Datadog +import com.datadog.android._InternalProxy +import com.datadog.android.api.InternalLogger +import com.datadog.android.api.feature.FeatureSdkCore +import com.facebook.react.bridge.ReadableMap + +class DdTelemetry { + + // We use Kotlin backing field here to initialize once the telemetry proxy + // and make sure it is only after SDK is initialized. + private var telemetryProxy: _InternalProxy._TelemetryProxy? = null + get() { + if (field == null && Datadog.isInitialized()) { + field = Datadog._internalProxy()._telemetry + } + + return field + } + + fun sendTelemetryLog(message: String, attributes: ReadableMap, config: ReadableMap) { + val core = Datadog.getInstance() as FeatureSdkCore? + val logger = core?.internalLogger; + + val additionalProperties = attributes.toMap() + val telemetryConfig = config.toMap() + + logger?.log( + level = InternalLogger.Level.INFO, + target = InternalLogger.Target.TELEMETRY, + messageBuilder = { message }, + onlyOnce = (telemetryConfig["onlyOnce"] as? Boolean) ?: true, + additionalProperties = additionalProperties + ) + } + + fun telemetryDebug(message: String) { + telemetryProxy?.debug(message) + } + + fun telemetryError(message: String, stack: String?, kind: String?) { + telemetryProxy?.error(message, stack, kind) + } + + fun telemetryError(message: String, throwable: Throwable?) { + telemetryProxy?.error(message, throwable) + } +} + diff --git a/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdSdk.kt b/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdSdk.kt index d46e53ade..5bc470947 100644 --- a/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdSdk.kt +++ b/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdSdk.kt @@ -19,9 +19,10 @@ import com.facebook.react.modules.core.DeviceEventManagerModule class DdSdk( reactContext: ReactApplicationContext, datadogWrapper: DatadogWrapper = DatadogSDKWrapper() + ddTelemetry: DdTelemetry = DdTelemetry() ) : NativeDdSdkSpec(reactContext) { - private val implementation = DdSdkImplementation(reactContext, datadog = datadogWrapper) + private val implementation = DdSdkImplementation(reactContext, datadog = datadogWrapper, ddTelemetry) override fun getName(): String = DdSdkImplementation.NAME diff --git a/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt b/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt index b41eff1db..af8f87c29 100644 --- a/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt +++ b/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt @@ -17,10 +17,11 @@ import com.facebook.react.bridge.ReadableMap /** The entry point to initialize Datadog's features. */ class DdSdk( reactContext: ReactApplicationContext, - datadogWrapper: DatadogWrapper = DatadogSDKWrapper() + datadogWrapper: DatadogWrapper = DatadogSDKWrapper(), + ddTelemetry: DdTelemetry = DdTelemetry() ) : ReactContextBaseJavaModule(reactContext) { - private val implementation = DdSdkImplementation(reactContext, datadog = datadogWrapper) + private val implementation = DdSdkImplementation(reactContext, datadog = datadogWrapper, ddTelemetry) private var lifecycleEventListener: LifecycleEventListener? = null override fun getName(): String = DdSdkImplementation.NAME diff --git a/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkNativeInitializationTest.kt b/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkNativeInitializationTest.kt index d05d43c57..e25bfe999 100644 --- a/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkNativeInitializationTest.kt +++ b/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkNativeInitializationTest.kt @@ -48,6 +48,9 @@ internal class DdSdkNativeInitializationTest { @Mock lateinit var mockDatadog: DatadogWrapper + @Mock + lateinit var mockDdTelemetry: DdTelemetry + @Mock lateinit var mockJSONFileReader: JSONFileReader @@ -64,6 +67,7 @@ internal class DdSdkNativeInitializationTest { testedNativeInitialization = DdSdkNativeInitialization( mockContext, mockDatadog, + mockDdTelemetry, mockJSONFileReader ) } diff --git a/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt b/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt index 5cda53616..9e7d671fd 100644 --- a/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt +++ b/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt @@ -120,6 +120,9 @@ internal class DdSdkTest { @Mock lateinit var mockDatadog: DatadogWrapper + @Mock + lateinit var mockDdTelemetry: DdTelemetry + @Forgery lateinit var fakeConfiguration: DdSdkConfiguration @@ -157,9 +160,8 @@ internal class DdSdkTest { answer.getArgument(0).run() true } - testedBridgeSdk = DdSdkImplementation(mockReactContext, mockDatadog, TestUiThreadExecutor()) + testedBridgeSdk = DdSdkImplementation(mockReactContext, mockDatadog, mockDdTelemetry, TestUiThreadExecutor()) - DatadogSDKWrapperStorage.setSdkCore(null) DatadogSDKWrapperStorage.onInitializedListeners.clear() } diff --git a/packages/internal-testing-tools/android/src/main/kotlin/com/datadog/reactnative/internaltesting/DdInternalTestingImplementation.kt b/packages/internal-testing-tools/android/src/main/kotlin/com/datadog/reactnative/internaltesting/DdInternalTestingImplementation.kt index 197a9df75..df0030fb5 100644 --- a/packages/internal-testing-tools/android/src/main/kotlin/com/datadog/reactnative/internaltesting/DdInternalTestingImplementation.kt +++ b/packages/internal-testing-tools/android/src/main/kotlin/com/datadog/reactnative/internaltesting/DdInternalTestingImplementation.kt @@ -7,6 +7,7 @@ package com.datadog.reactnative.internaltesting import com.datadog.android.api.InternalLogger +import com.datadog.android.Datadog import com.datadog.android.api.context.DatadogContext import com.datadog.android.api.context.NetworkInfo import com.datadog.android.api.context.TimeInfo @@ -53,7 +54,6 @@ class DdInternalTestingImplementation { fun enable(promise: Promise) { DatadogSDKWrapperStorage.addOnInitializedListener { ddCore -> this.wrappedCore = StubSDKCore(ddCore) - DatadogSDKWrapperStorage.setSdkCore(this.wrappedCore) } promise.resolve(null) } diff --git a/packages/internal-testing-tools/android/src/test/kotlin/com/datadog/reactnative/internaltesting/DdInternalTestingImplementationTest.kt b/packages/internal-testing-tools/android/src/test/kotlin/com/datadog/reactnative/internaltesting/DdInternalTestingImplementationTest.kt index 6c278026a..c542ed6bc 100644 --- a/packages/internal-testing-tools/android/src/test/kotlin/com/datadog/reactnative/internaltesting/DdInternalTestingImplementationTest.kt +++ b/packages/internal-testing-tools/android/src/test/kotlin/com/datadog/reactnative/internaltesting/DdInternalTestingImplementationTest.kt @@ -7,6 +7,8 @@ package com.datadog.reactnative.internaltesting import android.content.Context +import com.datadog.android.Datadog +import com.datadog.android.api.SdkCore import com.datadog.android.api.context.DatadogContext import com.datadog.android.api.feature.Feature import com.datadog.android.api.feature.FeatureScope @@ -24,6 +26,7 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith import org.junit.jupiter.api.extension.Extensions import org.mockito.Mock +import org.mockito.Mockito import org.mockito.Mockito.mock import org.mockito.junit.jupiter.MockitoExtension import org.mockito.junit.jupiter.MockitoSettings @@ -57,37 +60,47 @@ internal class DdInternalTestingImplementationTest { @Test fun `M return captured events W enable()`() { - // Given - val mockFeature = MockFeature("mockFeature") - val mockFeatureScope = MockFeatureScope(mockFeature) - whenever(mockCore.getFeature(mockFeature.name)).doReturn( - mockFeatureScope - ) - whenever(mockCore.getDatadogContext()).doReturn( - mockContext - ) - - // When - testedInternalTesting.enable(mockPromise) - // Simulating DdSdkImplementation initialization - DatadogSDKWrapperStorage.setSdkCore(mockCore) - DatadogSDKWrapperStorage.notifyOnInitializedListeners(mockCore) - - val wrappedCore = DatadogSDKWrapperStorage.getSdkCore() as StubSDKCore - wrappedCore.registerFeature(mockFeature) - requireNotNull(wrappedCore.getFeature(mockFeature.name)) - .withWriteContext { _, eventBatchWriter -> - eventBatchWriter.write( - RawBatchEvent(data = "mock event for test".toByteArray()), - batchMetadata = null, - eventType = EventType.DEFAULT + Mockito.mockStatic(Datadog::class.java).use { datadogStatic -> + // Given + datadogStatic.`when` { + Datadog.getInstance() + }.thenReturn(mockCore) + + val mockFeature = MockFeature("mockFeature") + val mockFeatureScope = MockFeatureScope(mockFeature) + whenever(mockCore.getFeature(mockFeature.name)).doReturn( + mockFeatureScope + ) + whenever(mockCore.getDatadogContext()).doReturn( + mockContext + ) + + // When + testedInternalTesting.enable(mockPromise) + // Simulating DdSdkImplementation initialization + DatadogSDKWrapperStorage.notifyOnInitializedListeners(mockCore) + + val wrappedCore = Datadog.getInstance() as StubSDKCore + wrappedCore.registerFeature(mockFeature) + requireNotNull(wrappedCore.getFeature(mockFeature.name)) + .withWriteContext { _, eventBatchWriter -> + eventBatchWriter.write( + RawBatchEvent(data = "mock event for test".toByteArray()), + batchMetadata = null, + eventType = EventType.DEFAULT + ) + } + + // Then + assertThat( + wrappedCore.featureScopes[mockFeature.name] + ?.eventsWritten() + ?.first() + ) + .isEqualTo( + "mock event for test" ) - } - - // Then - assertThat(wrappedCore.featureScopes[mockFeature.name]?.eventsWritten()?.first()).isEqualTo( - "mock event for test" - ) + } } } @@ -96,6 +109,7 @@ internal class MockFeatureScope(private val feature: Feature) : FeatureScope { override fun sendEvent(event: Any) {} + @Suppress("UNCHECKED_CAST") override fun unwrap(): T { return feature as T } diff --git a/packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/DdSessionReplayImplementation.kt b/packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/DdSessionReplayImplementation.kt index ef5467bc4..cc2fd64dc 100644 --- a/packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/DdSessionReplayImplementation.kt +++ b/packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/DdSessionReplayImplementation.kt @@ -7,10 +7,10 @@ package com.datadog.reactnative.sessionreplay import android.annotation.SuppressLint +import com.datadog.android.Datadog import com.datadog.android.api.feature.FeatureSdkCore import com.datadog.android.sessionreplay.SessionReplayConfiguration import com.datadog.android.sessionreplay._SessionReplayInternalProxy -import com.datadog.reactnative.DatadogSDKWrapperStorage import com.datadog.reactnative.sessionreplay.utils.text.TextViewUtils import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReactContext @@ -40,7 +40,7 @@ class DdSessionReplayImplementation( startRecordingImmediately: Boolean, promise: Promise ) { - val sdkCore = DatadogSDKWrapperStorage.getSdkCore() as FeatureSdkCore + val sdkCore = Datadog.getInstance() as FeatureSdkCore val logger = sdkCore.internalLogger val textViewUtils = TextViewUtils.create(reactContext, logger) val internalCallback = ReactNativeInternalCallback(reactContext) @@ -68,7 +68,7 @@ class DdSessionReplayImplementation( */ fun startRecording(promise: Promise) { sessionReplayProvider().startRecording( - DatadogSDKWrapperStorage.getSdkCore() as FeatureSdkCore + Datadog.getInstance() as FeatureSdkCore ) promise.resolve(null) } @@ -78,7 +78,7 @@ class DdSessionReplayImplementation( */ fun stopRecording(promise: Promise) { sessionReplayProvider().stopRecording( - DatadogSDKWrapperStorage.getSdkCore() as FeatureSdkCore + Datadog.getInstance() as FeatureSdkCore ) promise.resolve(null) } From a61f2c4ec191c9aee665702c7cf229ff445caf49 Mon Sep 17 00:00:00 2001 From: Marco Saia Date: Mon, 22 Sep 2025 16:20:27 +0200 Subject: [PATCH 061/410] Fixed internal testing tools and unit tests --- .../com/datadog/reactnative/DatadogWrapper.kt | 8 +- .../reactnative/DdSdkNativeInitialization.kt | 2 - .../com/datadog/reactnative/DdTelemetry.kt | 40 + .../kotlin/com/datadog/reactnative/DdSdk.kt | 6 +- .../com/datadog/reactnative/DdSdkTest.kt | 2628 ++++++++++------- .../DdInternalTestingImplementation.kt | 6 + .../DdInternalTestingImplementationTest.kt | 4 +- 7 files changed, 1695 insertions(+), 999 deletions(-) diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogWrapper.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogWrapper.kt index 9ac591d80..19b25e587 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogWrapper.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogWrapper.kt @@ -18,7 +18,7 @@ import com.facebook.react.bridge.ReadableMap import java.lang.IllegalArgumentException /** - * Wrapper around [Datadog]. + * Wrapper around [com.datadog.android.Datadog]. */ @Suppress("ComplexInterface", "TooManyFunctions") interface DatadogWrapper { @@ -49,10 +49,8 @@ interface DatadogWrapper { /** * Initializes the Datadog SDK. * @param context your application context - * @param credentials your organization credentials * @param configuration the configuration for the SDK library - * @param trackingConsent as the initial state of the tracking consent flag. - * @see [Credentials] + * @param consent as the initial state of the tracking consent flag. * @see [Configuration] * @see [TrackingConsent] * @throws IllegalArgumentException if the env name is using illegal characters and your @@ -99,7 +97,7 @@ interface DatadogWrapper { /** * Sets the user information. - * @param extraUserInfo: The additional information. (To set the id, name or email please user setUserInfo). + * @param extraInfo: The additional information. (To set the id, name or email please user setUserInfo). */ fun addUserExtraInfo( extraInfo: Map diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkNativeInitialization.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkNativeInitialization.kt index ee55d08fe..4388ad5f6 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkNativeInitialization.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkNativeInitialization.kt @@ -65,9 +65,7 @@ class DdSdkNativeInitialization internal constructor( datadog.initialize(appContext, sdkConfiguration, trackingConsent) Rum.enable(rumConfiguration, Datadog.getInstance()) - Logs.enable(logsConfiguration, Datadog.getInstance()) - Trace.enable(traceConfiguration, Datadog.getInstance()) } diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdTelemetry.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdTelemetry.kt index 2d60df004..24354ce78 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdTelemetry.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdTelemetry.kt @@ -12,6 +12,13 @@ import com.datadog.android.api.InternalLogger import com.datadog.android.api.feature.FeatureSdkCore import com.facebook.react.bridge.ReadableMap +/** + * **[INTERNAL USAGE]** + * + * Utility class used by React Native modules to forward telemetry events to the Datadog SDK. + * + * This class is **public only for Datadog internal package visibility** and should not be used. + */ class DdTelemetry { // We use Kotlin backing field here to initialize once the telemetry proxy @@ -25,6 +32,15 @@ class DdTelemetry { return field } + /** + * **[INTERNAL USAGE]** + * + * Sends a telemetry log message with additional attributes and configuration options. + * + * @param message the message to log + * @param attributes additional key–value properties to include in the log + * @param config configuration options for the telemetry log (e.g. `onlyOnce` flag) + */ fun sendTelemetryLog(message: String, attributes: ReadableMap, config: ReadableMap) { val core = Datadog.getInstance() as FeatureSdkCore? val logger = core?.internalLogger; @@ -41,14 +57,38 @@ class DdTelemetry { ) } + /** + * **[INTERNAL USAGE]** + * + * Sends a debug-level telemetry message. + * + * @param message the debug message + */ fun telemetryDebug(message: String) { telemetryProxy?.debug(message) } + /** + * **[INTERNAL USAGE]** + * + * Sends an error-level telemetry message with optional details. + * + * @param message the error message + * @param stack an optional stack trace string + * @param kind an optional error kind or category + */ fun telemetryError(message: String, stack: String?, kind: String?) { telemetryProxy?.error(message, stack, kind) } + /** + * **[INTERNAL USAGE]** + * + * Sends an error-level telemetry message with an attached [Throwable]. + * + * @param message the error message + * @param throwable the throwable associated with the error + */ fun telemetryError(message: String, throwable: Throwable?) { telemetryProxy?.error(message, throwable) } diff --git a/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt b/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt index af8f87c29..17acd6d20 100644 --- a/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt +++ b/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt @@ -21,7 +21,11 @@ class DdSdk( ddTelemetry: DdTelemetry = DdTelemetry() ) : ReactContextBaseJavaModule(reactContext) { - private val implementation = DdSdkImplementation(reactContext, datadog = datadogWrapper, ddTelemetry) + private val implementation = DdSdkImplementation( + reactContext, + datadog = datadogWrapper, + ddTelemetry + ) private var lifecycleEventListener: LifecycleEventListener? = null override fun getName(): String = DdSdkImplementation.NAME diff --git a/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt b/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt index 9e7d671fd..a39485dae 100644 --- a/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt +++ b/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt @@ -16,8 +16,10 @@ import com.datadog.android.core.configuration.BatchSize import com.datadog.android.core.configuration.Configuration import com.datadog.android.core.configuration.UploadFrequency import com.datadog.android.event.EventMapper +import com.datadog.android.log.Logs import com.datadog.android.log.LogsConfiguration import com.datadog.android.privacy.TrackingConsent +import com.datadog.android.rum.Rum import com.datadog.android.rum.RumConfiguration import com.datadog.android.rum.RumPerformanceMetric import com.datadog.android.rum._RumInternalProxy @@ -27,6 +29,7 @@ import com.datadog.android.rum.model.ActionEvent import com.datadog.android.rum.model.ResourceEvent import com.datadog.android.rum.tracking.ActivityViewTrackingStrategy import com.datadog.android.telemetry.model.TelemetryConfigurationEvent +import com.datadog.android.trace.Trace import com.datadog.android.trace.TraceConfiguration import com.datadog.android.trace.TracingHeaderType import com.datadog.tools.unit.GenericAssert.Companion.assertThat @@ -160,7 +163,12 @@ internal class DdSdkTest { answer.getArgument(0).run() true } - testedBridgeSdk = DdSdkImplementation(mockReactContext, mockDatadog, mockDdTelemetry, TestUiThreadExecutor()) + testedBridgeSdk = DdSdkImplementation( + mockReactContext, + mockDatadog, + mockDdTelemetry, + TestUiThreadExecutor() + ) DatadogSDKWrapperStorage.onInitializedListeners.clear() } @@ -181,35 +189,49 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(sdkConfigCaptor.firstValue) - .hasField("coreConfig") { - it.hasFieldEqualTo("needsClearTextHttp", false) - it.hasFieldEqualTo("firstPartyHostsWithHeaderTypes", emptyMap()) + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } - .hasFieldEqualTo("clientToken", fakeConfiguration.clientToken) - .hasFieldEqualTo("env", fakeConfiguration.env) - .hasFieldEqualTo("variant", "") - .hasFieldEqualTo("crashReportsEnabled", true) - .hasFieldEqualTo( - "additionalConfig", - fakeConfiguration.additionalConfig?.filterValues { it != null }.orEmpty() - ) - assertThat(rumConfigCaptor.firstValue) - .hasFieldEqualTo("applicationId", fakeConfiguration.applicationId) + assertThat(sdkConfigCaptor.firstValue) + .hasField("coreConfig") { + it.hasFieldEqualTo("needsClearTextHttp", false) + it.hasFieldEqualTo("firstPartyHostsWithHeaderTypes", emptyMap()) + } + .hasFieldEqualTo("clientToken", fakeConfiguration.clientToken) + .hasFieldEqualTo("env", fakeConfiguration.env) + .hasFieldEqualTo("variant", "") + .hasFieldEqualTo("crashReportsEnabled", true) + .hasFieldEqualTo( + "additionalConfig", + fakeConfiguration.additionalConfig?.filterValues { it != null }.orEmpty() + ) + assertThat(rumConfigCaptor.firstValue) + .hasFieldEqualTo("applicationId", fakeConfiguration.applicationId) + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } @Test @@ -221,75 +243,104 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(sdkConfigCaptor.firstValue) - .hasField("coreConfig") { - it.hasFieldEqualTo("needsClearTextHttp", false) - it.hasFieldEqualTo("firstPartyHostsWithHeaderTypes", emptyMap()) + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } - .hasFieldEqualTo("clientToken", fakeConfiguration.clientToken) - .hasFieldEqualTo("env", fakeConfiguration.env) - .hasFieldEqualTo("variant", "") - .hasFieldEqualTo("crashReportsEnabled", false) - .hasFieldEqualTo( - "additionalConfig", - fakeConfiguration.additionalConfig?.filterValues { it != null }.orEmpty() - ) - assertThat(rumConfigCaptor.firstValue) - .hasFieldEqualTo("applicationId", fakeConfiguration.applicationId) + assertThat(sdkConfigCaptor.firstValue) + .hasField("coreConfig") { + it.hasFieldEqualTo("needsClearTextHttp", false) + it.hasFieldEqualTo("firstPartyHostsWithHeaderTypes", emptyMap()) + } + .hasFieldEqualTo("clientToken", fakeConfiguration.clientToken) + .hasFieldEqualTo("env", fakeConfiguration.env) + .hasFieldEqualTo("variant", "") + .hasFieldEqualTo("crashReportsEnabled", false) + .hasFieldEqualTo( + "additionalConfig", + fakeConfiguration.additionalConfig?.filterValues { it != null }.orEmpty() + ) + assertThat(rumConfigCaptor.firstValue) + .hasFieldEqualTo("applicationId", fakeConfiguration.applicationId) + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } @Test fun `𝕄 initialize native SDK 𝕎 initialize() {nativeCrashReportEnabled=null}`() { // Given fakeConfiguration = fakeConfiguration.copy(nativeCrashReportEnabled = false, site = null) + val sdkConfigCaptor = argumentCaptor() val rumConfigCaptor = argumentCaptor() val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(sdkConfigCaptor.firstValue) - .hasField("coreConfig") { - it.hasFieldEqualTo("needsClearTextHttp", false) - it.hasFieldEqualTo("firstPartyHostsWithHeaderTypes", emptyMap()) + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } - .hasFieldEqualTo("clientToken", fakeConfiguration.clientToken) - .hasFieldEqualTo("env", fakeConfiguration.env) - .hasFieldEqualTo("variant", "") - .hasFieldEqualTo("crashReportsEnabled", false) - .hasFieldEqualTo( - "additionalConfig", - fakeConfiguration.additionalConfig?.filterValues { it != null }.orEmpty() - ) - assertThat(rumConfigCaptor.firstValue) - .hasFieldEqualTo("applicationId", fakeConfiguration.applicationId) + assertThat(sdkConfigCaptor.firstValue) + .hasField("coreConfig") { + it.hasFieldEqualTo("needsClearTextHttp", false) + it.hasFieldEqualTo("firstPartyHostsWithHeaderTypes", emptyMap()) + } + .hasFieldEqualTo("clientToken", fakeConfiguration.clientToken) + .hasFieldEqualTo("env", fakeConfiguration.env) + .hasFieldEqualTo("variant", "") + .hasFieldEqualTo("crashReportsEnabled", false) + .hasFieldEqualTo( + "additionalConfig", + fakeConfiguration.additionalConfig?.filterValues { it != null }.orEmpty() + ) + assertThat(rumConfigCaptor.firstValue) + .hasFieldEqualTo("applicationId", fakeConfiguration.applicationId) + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } // endregion @@ -305,37 +356,50 @@ internal class DdSdkTest { val traceConfigCaptor = argumentCaptor() val expectedRumSampleRate = fakeConfiguration.sampleRate?.toFloat() ?: 100f - // When - testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(sdkConfigCaptor.firstValue) - .hasField("coreConfig") { - it.hasFieldEqualTo("needsClearTextHttp", false) - it.hasFieldEqualTo("firstPartyHostsWithHeaderTypes", emptyMap()) - } - .hasFieldEqualTo("clientToken", fakeConfiguration.clientToken) - .hasFieldEqualTo("env", fakeConfiguration.env) - .hasFieldEqualTo("variant", "") - .hasFieldEqualTo( - "additionalConfig", - fakeConfiguration.additionalConfig?.filterValues { it != null }.orEmpty() - ) - assertThat(rumConfigCaptor.firstValue) - .hasFieldEqualTo("applicationId", fakeConfiguration.applicationId) - .hasField("featureConfiguration") { - it.hasFieldEqualTo("sampleRate", expectedRumSampleRate) + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + // When + testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } + assertThat(sdkConfigCaptor.firstValue) + .hasField("coreConfig") { + it.hasFieldEqualTo("needsClearTextHttp", false) + it.hasFieldEqualTo("firstPartyHostsWithHeaderTypes", emptyMap()) + } + .hasFieldEqualTo("clientToken", fakeConfiguration.clientToken) + .hasFieldEqualTo("env", fakeConfiguration.env) + .hasFieldEqualTo("variant", "") + .hasFieldEqualTo( + "additionalConfig", + fakeConfiguration.additionalConfig?.filterValues { it != null }.orEmpty() + ) + assertThat(rumConfigCaptor.firstValue) + .hasFieldEqualTo("applicationId", fakeConfiguration.applicationId) + .hasField("featureConfiguration") { + it.hasFieldEqualTo("sampleRate", expectedRumSampleRate) + } + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } // endregion @@ -351,37 +415,50 @@ internal class DdSdkTest { val traceConfigCaptor = argumentCaptor() val expectedTelemetrySampleRate = fakeConfiguration.telemetrySampleRate?.toFloat() ?: 20f - // When - testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(sdkConfigCaptor.firstValue) - .hasField("coreConfig") { - it.hasFieldEqualTo("needsClearTextHttp", false) - it.hasFieldEqualTo("firstPartyHostsWithHeaderTypes", emptyMap()) - } - .hasFieldEqualTo("clientToken", fakeConfiguration.clientToken) - .hasFieldEqualTo("env", fakeConfiguration.env) - .hasFieldEqualTo("variant", "") - .hasFieldEqualTo( - "additionalConfig", - fakeConfiguration.additionalConfig?.filterValues { it != null }.orEmpty() - ) - assertThat(rumConfigCaptor.firstValue) - .hasFieldEqualTo("applicationId", fakeConfiguration.applicationId) - .hasField("featureConfiguration") { - it.hasFieldEqualTo("telemetrySampleRate", expectedTelemetrySampleRate) + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + // When + testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } + assertThat(sdkConfigCaptor.firstValue) + .hasField("coreConfig") { + it.hasFieldEqualTo("needsClearTextHttp", false) + it.hasFieldEqualTo("firstPartyHostsWithHeaderTypes", emptyMap()) + } + .hasFieldEqualTo("clientToken", fakeConfiguration.clientToken) + .hasFieldEqualTo("env", fakeConfiguration.env) + .hasFieldEqualTo("variant", "") + .hasFieldEqualTo( + "additionalConfig", + fakeConfiguration.additionalConfig?.filterValues { it != null }.orEmpty() + ) + assertThat(rumConfigCaptor.firstValue) + .hasFieldEqualTo("applicationId", fakeConfiguration.applicationId) + .hasField("featureConfiguration") { + it.hasFieldEqualTo("telemetrySampleRate", expectedTelemetrySampleRate) + } + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } // endregion @@ -397,31 +474,44 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(sdkConfigCaptor.firstValue) - .hasField("coreConfig") { - it.hasFieldEqualTo("needsClearTextHttp", false) - it.hasFieldEqualTo("firstPartyHostsWithHeaderTypes", emptyMap()) + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } - .hasFieldEqualTo("clientToken", fakeConfiguration.clientToken) - .hasFieldEqualTo("env", fakeConfiguration.env) - .hasFieldEqualTo("variant", "") - .hasFieldEqualTo("additionalConfig", emptyMap()) - assertThat(rumConfigCaptor.firstValue) - .hasFieldEqualTo("applicationId", fakeConfiguration.applicationId) + assertThat(sdkConfigCaptor.firstValue) + .hasField("coreConfig") { + it.hasFieldEqualTo("needsClearTextHttp", false) + it.hasFieldEqualTo("firstPartyHostsWithHeaderTypes", emptyMap()) + } + .hasFieldEqualTo("clientToken", fakeConfiguration.clientToken) + .hasFieldEqualTo("env", fakeConfiguration.env) + .hasFieldEqualTo("variant", "") + .hasFieldEqualTo("additionalConfig", emptyMap()) + assertThat(rumConfigCaptor.firstValue) + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } @Test @@ -432,34 +522,47 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(sdkConfigCaptor.firstValue) - .hasField("coreConfig") { - it.hasFieldEqualTo("needsClearTextHttp", false) - it.hasFieldEqualTo("firstPartyHostsWithHeaderTypes", emptyMap()) + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + // When + testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } - .hasFieldEqualTo("clientToken", fakeConfiguration.clientToken) - .hasFieldEqualTo("env", fakeConfiguration.env) - .hasFieldEqualTo("variant", "") - .hasFieldEqualTo( - "additionalConfig", - fakeConfiguration.additionalConfig?.filterValues { it != null }.orEmpty() - ) - assertThat(rumConfigCaptor.firstValue) - .hasFieldEqualTo("applicationId", fakeConfiguration.applicationId) + assertThat(sdkConfigCaptor.firstValue) + .hasField("coreConfig") { + it.hasFieldEqualTo("needsClearTextHttp", false) + it.hasFieldEqualTo("firstPartyHostsWithHeaderTypes", emptyMap()) + } + .hasFieldEqualTo("clientToken", fakeConfiguration.clientToken) + .hasFieldEqualTo("env", fakeConfiguration.env) + .hasFieldEqualTo("variant", "") + .hasFieldEqualTo( + "additionalConfig", + fakeConfiguration.additionalConfig?.filterValues { it != null }.orEmpty() + ) + assertThat(rumConfigCaptor.firstValue) + .hasFieldEqualTo("applicationId", fakeConfiguration.applicationId) + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } // endregion @@ -477,35 +580,49 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(sdkConfigCaptor.firstValue) - .hasField("coreConfig") { - it.hasFieldEqualTo("needsClearTextHttp", false) - it.hasFieldEqualTo("firstPartyHostsWithHeaderTypes", emptyMap()) - it.hasFieldEqualTo("site", DatadogSite.US1) + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } - .hasFieldEqualTo("clientToken", fakeConfiguration.clientToken) - .hasFieldEqualTo("env", fakeConfiguration.env) - .hasFieldEqualTo("variant", "") - .hasFieldEqualTo( - "additionalConfig", - fakeConfiguration.additionalConfig?.filterValues { it != null }.orEmpty() - ) - assertThat(rumConfigCaptor.firstValue) - .hasFieldEqualTo("applicationId", fakeConfiguration.applicationId) + assertThat(sdkConfigCaptor.firstValue) + .hasField("coreConfig") { + it.hasFieldEqualTo("needsClearTextHttp", false) + it.hasFieldEqualTo("firstPartyHostsWithHeaderTypes", emptyMap()) + it.hasFieldEqualTo("site", DatadogSite.US1) + } + .hasFieldEqualTo("clientToken", fakeConfiguration.clientToken) + .hasFieldEqualTo("env", fakeConfiguration.env) + .hasFieldEqualTo("variant", "") + .hasFieldEqualTo( + "additionalConfig", + fakeConfiguration.additionalConfig?.filterValues { it != null }.orEmpty() + ) + assertThat(rumConfigCaptor.firstValue) + .hasFieldEqualTo("applicationId", fakeConfiguration.applicationId) + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } @Test @@ -520,35 +637,49 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(sdkConfigCaptor.firstValue) - .hasField("coreConfig") { - it.hasFieldEqualTo("needsClearTextHttp", false) - it.hasFieldEqualTo("firstPartyHostsWithHeaderTypes", emptyMap()) - it.hasFieldEqualTo("site", DatadogSite.US1) + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } - .hasFieldEqualTo("clientToken", fakeConfiguration.clientToken) - .hasFieldEqualTo("env", fakeConfiguration.env) - .hasFieldEqualTo("variant", "") - .hasFieldEqualTo( - "additionalConfig", - fakeConfiguration.additionalConfig?.filterValues { it != null }.orEmpty() - ) - assertThat(rumConfigCaptor.firstValue) - .hasFieldEqualTo("applicationId", fakeConfiguration.applicationId) + assertThat(sdkConfigCaptor.firstValue) + .hasField("coreConfig") { + it.hasFieldEqualTo("needsClearTextHttp", false) + it.hasFieldEqualTo("firstPartyHostsWithHeaderTypes", emptyMap()) + it.hasFieldEqualTo("site", DatadogSite.US1) + } + .hasFieldEqualTo("clientToken", fakeConfiguration.clientToken) + .hasFieldEqualTo("env", fakeConfiguration.env) + .hasFieldEqualTo("variant", "") + .hasFieldEqualTo( + "additionalConfig", + fakeConfiguration.additionalConfig?.filterValues { it != null }.orEmpty() + ) + assertThat(rumConfigCaptor.firstValue) + .hasFieldEqualTo("applicationId", fakeConfiguration.applicationId) + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } @Test @@ -563,35 +694,49 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(sdkConfigCaptor.firstValue) - .hasField("coreConfig") { - it.hasFieldEqualTo("needsClearTextHttp", false) - it.hasFieldEqualTo("firstPartyHostsWithHeaderTypes", emptyMap()) - it.hasFieldEqualTo("site", DatadogSite.US3) + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } - .hasFieldEqualTo("clientToken", fakeConfiguration.clientToken) - .hasFieldEqualTo("env", fakeConfiguration.env) - .hasFieldEqualTo("variant", "") - .hasFieldEqualTo( - "additionalConfig", - fakeConfiguration.additionalConfig?.filterValues { it != null }.orEmpty() - ) - assertThat(rumConfigCaptor.firstValue) - .hasFieldEqualTo("applicationId", fakeConfiguration.applicationId) + assertThat(sdkConfigCaptor.firstValue) + .hasField("coreConfig") { + it.hasFieldEqualTo("needsClearTextHttp", false) + it.hasFieldEqualTo("firstPartyHostsWithHeaderTypes", emptyMap()) + it.hasFieldEqualTo("site", DatadogSite.US3) + } + .hasFieldEqualTo("clientToken", fakeConfiguration.clientToken) + .hasFieldEqualTo("env", fakeConfiguration.env) + .hasFieldEqualTo("variant", "") + .hasFieldEqualTo( + "additionalConfig", + fakeConfiguration.additionalConfig?.filterValues { it != null }.orEmpty() + ) + assertThat(rumConfigCaptor.firstValue) + .hasFieldEqualTo("applicationId", fakeConfiguration.applicationId) + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } @Test @@ -606,35 +751,49 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(sdkConfigCaptor.firstValue) - .hasField("coreConfig") { - it.hasFieldEqualTo("needsClearTextHttp", false) - it.hasFieldEqualTo("firstPartyHostsWithHeaderTypes", emptyMap()) - it.hasFieldEqualTo("site", DatadogSite.US5) + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } - .hasFieldEqualTo("clientToken", fakeConfiguration.clientToken) - .hasFieldEqualTo("env", fakeConfiguration.env) - .hasFieldEqualTo("variant", "") - .hasFieldEqualTo( - "additionalConfig", - fakeConfiguration.additionalConfig?.filterValues { it != null }.orEmpty() - ) - assertThat(rumConfigCaptor.firstValue) - .hasFieldEqualTo("applicationId", fakeConfiguration.applicationId) + assertThat(sdkConfigCaptor.firstValue) + .hasField("coreConfig") { + it.hasFieldEqualTo("needsClearTextHttp", false) + it.hasFieldEqualTo("firstPartyHostsWithHeaderTypes", emptyMap()) + it.hasFieldEqualTo("site", DatadogSite.US5) + } + .hasFieldEqualTo("clientToken", fakeConfiguration.clientToken) + .hasFieldEqualTo("env", fakeConfiguration.env) + .hasFieldEqualTo("variant", "") + .hasFieldEqualTo( + "additionalConfig", + fakeConfiguration.additionalConfig?.filterValues { it != null }.orEmpty() + ) + assertThat(rumConfigCaptor.firstValue) + .hasFieldEqualTo("applicationId", fakeConfiguration.applicationId) + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } @Test @@ -649,35 +808,49 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(sdkConfigCaptor.firstValue) - .hasField("coreConfig") { - it.hasFieldEqualTo("needsClearTextHttp", false) - it.hasFieldEqualTo("firstPartyHostsWithHeaderTypes", emptyMap()) - it.hasFieldEqualTo("site", DatadogSite.US1_FED) + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } - .hasFieldEqualTo("clientToken", fakeConfiguration.clientToken) - .hasFieldEqualTo("env", fakeConfiguration.env) - .hasFieldEqualTo("variant", "") - .hasFieldEqualTo( - "additionalConfig", - fakeConfiguration.additionalConfig?.filterValues { it != null }.orEmpty() - ) - assertThat(rumConfigCaptor.firstValue) - .hasFieldEqualTo("applicationId", fakeConfiguration.applicationId) + assertThat(sdkConfigCaptor.firstValue) + .hasField("coreConfig") { + it.hasFieldEqualTo("needsClearTextHttp", false) + it.hasFieldEqualTo("firstPartyHostsWithHeaderTypes", emptyMap()) + it.hasFieldEqualTo("site", DatadogSite.US1_FED) + } + .hasFieldEqualTo("clientToken", fakeConfiguration.clientToken) + .hasFieldEqualTo("env", fakeConfiguration.env) + .hasFieldEqualTo("variant", "") + .hasFieldEqualTo( + "additionalConfig", + fakeConfiguration.additionalConfig?.filterValues { it != null }.orEmpty() + ) + assertThat(rumConfigCaptor.firstValue) + .hasFieldEqualTo("applicationId", fakeConfiguration.applicationId) + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } @Test @@ -692,35 +865,49 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(sdkConfigCaptor.firstValue) - .hasField("coreConfig") { - it.hasFieldEqualTo("needsClearTextHttp", false) - it.hasFieldEqualTo("firstPartyHostsWithHeaderTypes", emptyMap()) - it.hasFieldEqualTo("site", DatadogSite.EU1) + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } - .hasFieldEqualTo("clientToken", fakeConfiguration.clientToken) - .hasFieldEqualTo("env", fakeConfiguration.env) - .hasFieldEqualTo("variant", "") - .hasFieldEqualTo( - "additionalConfig", - fakeConfiguration.additionalConfig?.filterValues { it != null }.orEmpty() - ) - assertThat(rumConfigCaptor.firstValue) - .hasFieldEqualTo("applicationId", fakeConfiguration.applicationId) + assertThat(sdkConfigCaptor.firstValue) + .hasField("coreConfig") { + it.hasFieldEqualTo("needsClearTextHttp", false) + it.hasFieldEqualTo("firstPartyHostsWithHeaderTypes", emptyMap()) + it.hasFieldEqualTo("site", DatadogSite.EU1) + } + .hasFieldEqualTo("clientToken", fakeConfiguration.clientToken) + .hasFieldEqualTo("env", fakeConfiguration.env) + .hasFieldEqualTo("variant", "") + .hasFieldEqualTo( + "additionalConfig", + fakeConfiguration.additionalConfig?.filterValues { it != null }.orEmpty() + ) + assertThat(rumConfigCaptor.firstValue) + .hasFieldEqualTo("applicationId", fakeConfiguration.applicationId) + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } @Test @@ -735,35 +922,49 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(sdkConfigCaptor.firstValue) - .hasField("coreConfig") { - it.hasFieldEqualTo("needsClearTextHttp", false) - it.hasFieldEqualTo("firstPartyHostsWithHeaderTypes", emptyMap()) - it.hasFieldEqualTo("site", DatadogSite.AP1) + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } - .hasFieldEqualTo("clientToken", fakeConfiguration.clientToken) - .hasFieldEqualTo("env", fakeConfiguration.env) - .hasFieldEqualTo("variant", "") - .hasFieldEqualTo( - "additionalConfig", - fakeConfiguration.additionalConfig?.filterValues { it != null }.orEmpty() - ) - assertThat(rumConfigCaptor.firstValue) - .hasFieldEqualTo("applicationId", fakeConfiguration.applicationId) + assertThat(sdkConfigCaptor.firstValue) + .hasField("coreConfig") { + it.hasFieldEqualTo("needsClearTextHttp", false) + it.hasFieldEqualTo("firstPartyHostsWithHeaderTypes", emptyMap()) + it.hasFieldEqualTo("site", DatadogSite.AP1) + } + .hasFieldEqualTo("clientToken", fakeConfiguration.clientToken) + .hasFieldEqualTo("env", fakeConfiguration.env) + .hasFieldEqualTo("variant", "") + .hasFieldEqualTo( + "additionalConfig", + fakeConfiguration.additionalConfig?.filterValues { it != null }.orEmpty() + ) + assertThat(rumConfigCaptor.firstValue) + .hasFieldEqualTo("applicationId", fakeConfiguration.applicationId) + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } @Test @@ -778,35 +979,49 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(sdkConfigCaptor.firstValue) - .hasField("coreConfig") { - it.hasFieldEqualTo("needsClearTextHttp", false) - it.hasFieldEqualTo("firstPartyHostsWithHeaderTypes", emptyMap()) - it.hasFieldEqualTo("site", DatadogSite.AP2) + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } - .hasFieldEqualTo("clientToken", fakeConfiguration.clientToken) - .hasFieldEqualTo("env", fakeConfiguration.env) - .hasFieldEqualTo("variant", "") - .hasFieldEqualTo( - "additionalConfig", - fakeConfiguration.additionalConfig?.filterValues { it != null }.orEmpty() - ) - assertThat(rumConfigCaptor.firstValue) - .hasFieldEqualTo("applicationId", fakeConfiguration.applicationId) + assertThat(sdkConfigCaptor.firstValue) + .hasField("coreConfig") { + it.hasFieldEqualTo("needsClearTextHttp", false) + it.hasFieldEqualTo("firstPartyHostsWithHeaderTypes", emptyMap()) + it.hasFieldEqualTo("site", DatadogSite.AP2) + } + .hasFieldEqualTo("clientToken", fakeConfiguration.clientToken) + .hasFieldEqualTo("env", fakeConfiguration.env) + .hasFieldEqualTo("variant", "") + .hasFieldEqualTo( + "additionalConfig", + fakeConfiguration.additionalConfig?.filterValues { it != null }.orEmpty() + ) + assertThat(rumConfigCaptor.firstValue) + .hasFieldEqualTo("applicationId", fakeConfiguration.applicationId) + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } // endregion @@ -822,19 +1037,33 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - eq(TrackingConsent.PENDING) - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + eq(TrackingConsent.PENDING) + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } + } + } finally { + rumMock.close() + logsMock.close() + traceMock.close() } } @@ -850,19 +1079,33 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - eq(TrackingConsent.PENDING) - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + eq(TrackingConsent.PENDING) + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } + } + } finally { + rumMock.close() + logsMock.close() + traceMock.close() } } @@ -878,19 +1121,33 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - eq(TrackingConsent.GRANTED) - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + eq(TrackingConsent.GRANTED) + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } + } + } finally { + rumMock.close() + logsMock.close() + traceMock.close() } } @@ -906,19 +1163,33 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - eq(TrackingConsent.NOT_GRANTED) - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + eq(TrackingConsent.NOT_GRANTED) + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } + } + } finally { + rumMock.close() + logsMock.close() + traceMock.close() } } @@ -937,24 +1208,38 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(rumConfigCaptor.firstValue) - .hasField("featureConfiguration") { - it.hasFieldEqualTo("viewTrackingStrategy", NoOpViewTrackingStrategy) + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } + assertThat(rumConfigCaptor.firstValue) + .hasField("featureConfiguration") { + it.hasFieldEqualTo("viewTrackingStrategy", NoOpViewTrackingStrategy) + } + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } @Test @@ -970,24 +1255,38 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(rumConfigCaptor.firstValue) - .hasField("featureConfiguration") { - it.hasFieldEqualTo("viewTrackingStrategy", ActivityViewTrackingStrategy(false)) + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } + assertThat(rumConfigCaptor.firstValue) + .hasField("featureConfiguration") { + it.hasFieldEqualTo("viewTrackingStrategy", ActivityViewTrackingStrategy(false)) + } + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } @Test @@ -1003,24 +1302,38 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(rumConfigCaptor.firstValue) - .hasField("featureConfiguration") { - it.hasFieldEqualTo("userActionTracking", false) + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } + assertThat(rumConfigCaptor.firstValue) + .hasField("featureConfiguration") { + it.hasFieldEqualTo("userActionTracking", false) + } + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } @Test @@ -1036,24 +1349,38 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(rumConfigCaptor.firstValue) - .hasField("featureConfiguration") { - it.hasFieldEqualTo("trackFrustrations", true) + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } + assertThat(rumConfigCaptor.firstValue) + .hasField("featureConfiguration") { + it.hasFieldEqualTo("trackFrustrations", true) + } + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } @Test @@ -1064,29 +1391,44 @@ internal class DdSdkTest { val bridgeConfiguration = configuration.copy( trackFrustrations = false ) + val sdkConfigCaptor = argumentCaptor() val rumConfigCaptor = argumentCaptor() val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(rumConfigCaptor.firstValue) - .hasField("featureConfiguration") { - it.hasFieldEqualTo("trackFrustrations", false) + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } + assertThat(rumConfigCaptor.firstValue) + .hasField("featureConfiguration") { + it.hasFieldEqualTo("trackFrustrations", false) + } + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } @Test @@ -1102,24 +1444,39 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(rumConfigCaptor.firstValue) - .hasField("featureConfiguration") { - it.hasFieldEqualTo("userActionTracking", true) + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } + + assertThat(rumConfigCaptor.firstValue) + .hasField("featureConfiguration") { + it.hasFieldEqualTo("userActionTracking", true) + } + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } @Test @@ -1177,35 +1534,49 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(sdkConfigCaptor.firstValue) - .hasField("coreConfig") { - it.hasFieldEqualTo("needsClearTextHttp", false) - it.hasFieldEqualTo("firstPartyHostsWithHeaderTypes", emptyMap()) + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } - .hasFieldEqualTo("clientToken", bridgeConfiguration.clientToken) - .hasFieldEqualTo("env", bridgeConfiguration.env) - .hasFieldEqualTo("variant", "") - .hasFieldEqualTo("service", serviceName) - .hasFieldEqualTo( - "additionalConfig", - bridgeConfiguration.additionalConfig?.filterValues { it != null }.orEmpty() - ) - assertThat(rumConfigCaptor.firstValue) - .hasFieldEqualTo("applicationId", bridgeConfiguration.applicationId) + assertThat(sdkConfigCaptor.firstValue) + .hasField("coreConfig") { + it.hasFieldEqualTo("needsClearTextHttp", false) + it.hasFieldEqualTo("firstPartyHostsWithHeaderTypes", emptyMap()) + } + .hasFieldEqualTo("clientToken", bridgeConfiguration.clientToken) + .hasFieldEqualTo("env", bridgeConfiguration.env) + .hasFieldEqualTo("variant", "") + .hasFieldEqualTo("service", serviceName) + .hasFieldEqualTo( + "additionalConfig", + bridgeConfiguration.additionalConfig?.filterValues { it != null }.orEmpty() + ) + assertThat(rumConfigCaptor.firstValue) + .hasFieldEqualTo("applicationId", bridgeConfiguration.applicationId) + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } @Test @@ -1224,31 +1595,45 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(rumConfigCaptor.firstValue) - .hasField("featureConfiguration") { rumConfig -> - rumConfig.hasField("longTaskTrackingStrategy") { longTaskTrackingStrategy -> - longTaskTrackingStrategy - .isInstanceOf( - "com.datadog.android.rum.internal.instrumentation." + - "MainLooperLongTaskStrategy" - ) - .hasFieldEqualTo("thresholdMs", threshold.toLong()) - } + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } + assertThat(rumConfigCaptor.firstValue) + .hasField("featureConfiguration") { rumConfig -> + rumConfig.hasField("longTaskTrackingStrategy") { longTaskTrackingStrategy -> + longTaskTrackingStrategy + .isInstanceOf( + "com.datadog.android.rum.internal.instrumentation." + + "MainLooperLongTaskStrategy" + ) + .hasFieldEqualTo("thresholdMs", threshold.toLong()) + } + } + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } @Test @@ -1265,24 +1650,38 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(rumConfigCaptor.firstValue) - .hasField("featureConfiguration") { rumConfig -> - rumConfig.doesNotHaveField("longTaskTrackingStrategy") + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } + assertThat(rumConfigCaptor.firstValue) + .hasField("featureConfiguration") { rumConfig -> + rumConfig.doesNotHaveField("longTaskTrackingStrategy") + } + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } @Test @@ -1326,27 +1725,41 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(sdkConfigCaptor.firstValue) - .hasField("coreConfig") { coreConfig -> - coreConfig.hasFieldEqualTo( - "firstPartyHostsWithHeaderTypes", - tracingHosts + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } + assertThat(sdkConfigCaptor.firstValue) + .hasField("coreConfig") { coreConfig -> + coreConfig.hasFieldEqualTo( + "firstPartyHostsWithHeaderTypes", + tracingHosts + ) + } + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } @Test @@ -1385,27 +1798,41 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(sdkConfigCaptor.firstValue) - .hasField("coreConfig") { coreConfig -> - coreConfig.hasFieldEqualTo( - "firstPartyHostsWithHeaderTypes", - tracingHosts + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } + assertThat(sdkConfigCaptor.firstValue) + .hasField("coreConfig") { coreConfig -> + coreConfig.hasFieldEqualTo( + "firstPartyHostsWithHeaderTypes", + tracingHosts + ) + } + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } @Test @@ -1451,27 +1878,41 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(sdkConfigCaptor.firstValue) - .hasField("coreConfig") { coreConfig -> - coreConfig.hasFieldEqualTo( - "firstPartyHostsWithHeaderTypes", - tracingHosts + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } + assertThat(sdkConfigCaptor.firstValue) + .hasField("coreConfig") { coreConfig -> + coreConfig.hasFieldEqualTo( + "firstPartyHostsWithHeaderTypes", + tracingHosts + ) + } + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } @ParameterizedTest @@ -1490,27 +1931,41 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(sdkConfigCaptor.firstValue) - .hasField("coreConfig") { coreConfig -> - coreConfig.hasFieldEqualTo( - "uploadFrequency", - expectedUploadFrequency + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } + assertThat(sdkConfigCaptor.firstValue) + .hasField("coreConfig") { coreConfig -> + coreConfig.hasFieldEqualTo( + "uploadFrequency", + expectedUploadFrequency + ) + } + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } @ParameterizedTest @@ -1529,27 +1984,41 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(sdkConfigCaptor.firstValue) - .hasField("coreConfig") { coreConfig -> - coreConfig.hasFieldEqualTo( - "batchSize", - expectedBatchSize + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } + assertThat(sdkConfigCaptor.firstValue) + .hasField("coreConfig") { coreConfig -> + coreConfig.hasFieldEqualTo( + "batchSize", + expectedBatchSize + ) + } + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } @ParameterizedTest @@ -1568,27 +2037,41 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(sdkConfigCaptor.firstValue) - .hasField("coreConfig") { coreConfig -> - coreConfig.hasFieldEqualTo( - "batchProcessingLevel", - expectedBatchSize + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } + assertThat(sdkConfigCaptor.firstValue) + .hasField("coreConfig") { coreConfig -> + coreConfig.hasFieldEqualTo( + "batchProcessingLevel", + expectedBatchSize + ) + } + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } @Test @@ -1606,24 +2089,38 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(rumConfigCaptor.firstValue) - .hasField("featureConfiguration") { - it.hasFieldEqualTo("backgroundEventTracking", trackBackgroundEvents ?: false) + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } + assertThat(rumConfigCaptor.firstValue) + .hasField("featureConfiguration") { + it.hasFieldEqualTo("backgroundEventTracking", trackBackgroundEvents ?: false) + } + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } @Test @@ -1639,28 +2136,42 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(rumConfigCaptor.firstValue) - .hasField("featureConfiguration") { - it.hasFieldEqualTo("vitalsMonitorUpdateFrequency", VitalsUpdateFrequency.RARE) + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } + + // When + testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } + assertThat(rumConfigCaptor.firstValue) + .hasField("featureConfiguration") { + it.hasFieldEqualTo("vitalsMonitorUpdateFrequency", VitalsUpdateFrequency.RARE) + } - argumentCaptor { - verify(mockChoreographer).postFrameCallback(capture()) - assertThat(firstValue).isInstanceOf(FpsFrameCallback::class.java) + argumentCaptor { + verify(mockChoreographer).postFrameCallback(capture()) + assertThat(firstValue).isInstanceOf(FpsFrameCallback::class.java) + } + } finally { + rumMock.close() + logsMock.close() + traceMock.close() } } @@ -1679,25 +2190,37 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(rumConfigCaptor.firstValue) - .hasField("featureConfiguration") { - it.hasFieldEqualTo("vitalsMonitorUpdateFrequency", VitalsUpdateFrequency.NEVER) + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } // When + testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } - verifyNoInteractions(mockChoreographer) + assertThat(rumConfigCaptor.firstValue) + .hasField("featureConfiguration") { + it.hasFieldEqualTo("vitalsMonitorUpdateFrequency", VitalsUpdateFrequency.NEVER) + } + verifyNoInteractions(mockChoreographer) + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } @Test @@ -1719,41 +2242,56 @@ internal class DdSdkTest { val traceConfigCaptor = argumentCaptor() val frameDurationNs = threshold + frameDurationOverThreshold - // When - testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) - - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(rumConfigCaptor.firstValue) - .hasField("featureConfiguration") { - it.hasFieldEqualTo("vitalsMonitorUpdateFrequency", VitalsUpdateFrequency.AVERAGE) - } - argumentCaptor { - verify(mockChoreographer).postFrameCallback(capture()) - assertThat(firstValue).isInstanceOf(FpsFrameCallback::class.java) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // When - firstValue.doFrame(timestampNs) - firstValue.doFrame(timestampNs + frameDurationNs) + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } // When + testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) - // then - verify(mockRumMonitor._getInternal()!!).updatePerformanceMetric( - RumPerformanceMetric.JS_FRAME_TIME, - frameDurationNs.toDouble() - ) - verify(mockRumMonitor._getInternal()!!, never()).addLongTask( - frameDurationNs, - "javascript" - ) + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } + } + assertThat(rumConfigCaptor.firstValue) + .hasField("featureConfiguration") { + it.hasFieldEqualTo( + "vitalsMonitorUpdateFrequency", + VitalsUpdateFrequency.AVERAGE + ) + } + argumentCaptor { + verify(mockChoreographer).postFrameCallback(capture()) + assertThat(firstValue).isInstanceOf(FpsFrameCallback::class.java) + + // When + firstValue.doFrame(timestampNs) + firstValue.doFrame(timestampNs + frameDurationNs) + + // then + verify(mockRumMonitor._getInternal()!!).updatePerformanceMetric( + RumPerformanceMetric.JS_FRAME_TIME, + frameDurationNs.toDouble() + ) + verify(mockRumMonitor._getInternal()!!, never()).addLongTask( + frameDurationNs, + "javascript" + ) + } + } finally { + rumMock.close() + logsMock.close() + traceMock.close() } } @@ -1846,25 +2384,37 @@ internal class DdSdkTest { val traceConfigCaptor = argumentCaptor() val defaultTimeBasedIdentifier = TimeBasedInitialResourceIdentifier(100) - // When - testedBridgeSdk.initialize(configuration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } // When + testedBridgeSdk.initialize(configuration.toReadableJavaOnlyMap(), mockPromise) - assertThat(rumConfigCaptor.firstValue) - .hasField("featureConfiguration") { - it.hasFieldEqualTo("initialResourceIdentifier", defaultTimeBasedIdentifier) + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } + + assertThat(rumConfigCaptor.firstValue) + .hasField("featureConfiguration") { + it.hasFieldEqualTo("initialResourceIdentifier", defaultTimeBasedIdentifier) + } + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } @Test @@ -1884,25 +2434,37 @@ internal class DdSdkTest { thresholdInSeconds.seconds.inWholeMilliseconds ) - // When - testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } // When + testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) - assertThat(rumConfigCaptor.firstValue) - .hasField("featureConfiguration") { - it.hasFieldEqualTo("initialResourceIdentifier", timeBasedIdentifier) + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } + + assertThat(rumConfigCaptor.firstValue) + .hasField("featureConfiguration") { + it.hasFieldEqualTo("initialResourceIdentifier", timeBasedIdentifier) + } + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } // endregion @@ -1925,28 +2487,42 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(sdkConfigCaptor.firstValue) - .hasFieldEqualTo( - "additionalConfig", - mapOf( - DdSdkImplementation.DD_VERSION_SUFFIX to versionSuffix, - DdSdkImplementation.DD_VERSION to mockPackageInfo.versionName + versionSuffix + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } // When + testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() ) - ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } + } + assertThat(sdkConfigCaptor.firstValue) + .hasFieldEqualTo( + "additionalConfig", + mapOf( + DdSdkImplementation.DD_VERSION_SUFFIX to versionSuffix, + DdSdkImplementation.DD_VERSION to ( + mockPackageInfo.versionName + versionSuffix + ) + ) + ) + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } // endregion @@ -1985,47 +2561,59 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(rumConfigCaptor.firstValue) - .hasField("featureConfiguration") { - val configurationMapper = it - .getActualValue>( - "telemetryConfigurationMapper" - ) - val result = configurationMapper.map(telemetryConfigurationEvent)!! - assertThat(result.telemetry.configuration.trackNativeErrors!!).isEqualTo( - trackNativeErrors - ) - assertThat(result.telemetry.configuration.trackCrossPlatformLongTasks!!) - .isEqualTo(false) - assertThat(result.telemetry.configuration.trackLongTask!!) - .isEqualTo(false) - assertThat(result.telemetry.configuration.trackNativeLongTasks!!) - .isEqualTo(false) - - assertThat(result.telemetry.configuration.initializationType!!) - .isEqualTo(initializationType) - assertThat(result.telemetry.configuration.trackInteractions!!) - .isEqualTo(trackInteractions) - assertThat(result.telemetry.configuration.trackErrors!!).isEqualTo(trackErrors) - assertThat(result.telemetry.configuration.trackResources!!) - .isEqualTo(trackNetworkRequests) - assertThat(result.telemetry.configuration.trackNetworkRequests!!) - .isEqualTo(trackNetworkRequests) + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } // When + testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } + assertThat(rumConfigCaptor.firstValue) + .hasField("featureConfiguration") { + val configurationMapper = it + .getActualValue>( + "telemetryConfigurationMapper" + ) + val result = configurationMapper.map(telemetryConfigurationEvent)!! + assertThat(result.telemetry.configuration.trackNativeErrors!!).isEqualTo( + trackNativeErrors + ) + assertThat(result.telemetry.configuration.trackCrossPlatformLongTasks!!) + .isEqualTo(false) + assertThat(result.telemetry.configuration.trackLongTask!!) + .isEqualTo(false) + assertThat(result.telemetry.configuration.trackNativeLongTasks!!) + .isEqualTo(false) + + assertThat(result.telemetry.configuration.initializationType!!) + .isEqualTo(initializationType) + assertThat(result.telemetry.configuration.trackInteractions!!) + .isEqualTo(trackInteractions) + assertThat(result.telemetry.configuration.trackErrors!!).isEqualTo(trackErrors) + assertThat(result.telemetry.configuration.trackResources!!) + .isEqualTo(trackNetworkRequests) + assertThat(result.telemetry.configuration.trackNetworkRequests!!) + .isEqualTo(trackNetworkRequests) + } + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } // endregion @@ -2042,27 +2630,39 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(rumConfigCaptor.firstValue) - .hasField("featureConfiguration") { - val resourceMapper = it - .getActualValue>("resourceEventMapper") - val notDroppedEvent = resourceMapper.map(resourceEvent) - assertThat(notDroppedEvent).isNotNull + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } // When + testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } + assertThat(rumConfigCaptor.firstValue) + .hasField("featureConfiguration") { + val resourceMapper = it + .getActualValue>("resourceEventMapper") + val notDroppedEvent = resourceMapper.map(resourceEvent) + assertThat(notDroppedEvent).isNotNull + } + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } @Test @@ -2076,27 +2676,39 @@ internal class DdSdkTest { val traceConfigCaptor = argumentCaptor() resourceEvent.context?.additionalProperties?.put("_dd.resource.drop_resource", true) - // When - testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(rumConfigCaptor.firstValue) - .hasField("featureConfiguration") { - val resourceMapper = it - .getActualValue>("resourceEventMapper") - val droppedEvent = resourceMapper.map(resourceEvent) - assertThat(droppedEvent).isNull() + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } // When + testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } + assertThat(rumConfigCaptor.firstValue) + .hasField("featureConfiguration") { + val resourceMapper = it + .getActualValue>("resourceEventMapper") + val droppedEvent = resourceMapper.map(resourceEvent) + assertThat(droppedEvent).isNull() + } + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } // endregion @@ -2113,27 +2725,39 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(rumConfigCaptor.firstValue) - .hasField("featureConfiguration") { - val actionMapper = it - .getActualValue>("actionEventMapper") - val notDroppedEvent = actionMapper.map(actionEvent) - assertThat(notDroppedEvent).isNotNull + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } // When + testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } + assertThat(rumConfigCaptor.firstValue) + .hasField("featureConfiguration") { + val actionMapper = it + .getActualValue>("actionEventMapper") + val notDroppedEvent = actionMapper.map(actionEvent) + assertThat(notDroppedEvent).isNotNull + } + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } @Test @@ -2147,27 +2771,39 @@ internal class DdSdkTest { val traceConfigCaptor = argumentCaptor() actionEvent.context?.additionalProperties?.put("_dd.action.drop_action", true) - // When - testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).initialize( - same(mockContext), - sdkConfigCaptor.capture(), - any() - ) - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } - assertThat(rumConfigCaptor.firstValue) - .hasField("featureConfiguration") { - val actionMapper = it - .getActualValue>("actionEventMapper") - val droppedEvent = actionMapper.map(actionEvent) - assertThat(droppedEvent).isNull() + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } // When + testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise) + + // Then + inOrder(mockDatadog) { + verify(mockDatadog).initialize( + same(mockContext), + sdkConfigCaptor.capture(), + any() + ) + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } + assertThat(rumConfigCaptor.firstValue) + .hasField("featureConfiguration") { + val actionMapper = it + .getActualValue>("actionEventMapper") + val droppedEvent = actionMapper.map(actionEvent) + assertThat(droppedEvent).isNull() + } + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } // endregion @@ -2580,24 +3216,36 @@ internal class DdSdkTest { val logsConfigCaptor = argumentCaptor() val traceConfigCaptor = argumentCaptor() - // When - testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) + val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java) + val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java) + val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java) - // Then - inOrder(mockDatadog) { - verify(mockDatadog).enableRum(rumConfigCaptor.capture()) - verify(mockDatadog).enableTrace(traceConfigCaptor.capture()) - verify(mockDatadog).enableLogs(logsConfigCaptor.capture()) - } + try { + rumMock.`when` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { Trace.enable(any(), any()) }.then { } // When + testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise) - assertThat(rumConfigCaptor.firstValue) - .hasField("featureConfiguration") { - it.hasFieldEqualTo("customEndpointUrl", customRumEndpoint) + // Then + inOrder(mockDatadog) { + rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) } + traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) } + logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) } } - assertThat(logsConfigCaptor.firstValue) - .hasFieldEqualTo("customEndpointUrl", customLogsEndpoint) - assertThat(traceConfigCaptor.firstValue) - .hasFieldEqualTo("customEndpointUrl", customTraceEndpoint) + + assertThat(rumConfigCaptor.firstValue) + .hasField("featureConfiguration") { + it.hasFieldEqualTo("customEndpointUrl", customRumEndpoint) + } + assertThat(logsConfigCaptor.firstValue) + .hasFieldEqualTo("customEndpointUrl", customLogsEndpoint) + assertThat(traceConfigCaptor.firstValue) + .hasFieldEqualTo("customEndpointUrl", customTraceEndpoint) + } finally { + rumMock.close() + logsMock.close() + traceMock.close() + } } @Test diff --git a/packages/internal-testing-tools/android/src/main/kotlin/com/datadog/reactnative/internaltesting/DdInternalTestingImplementation.kt b/packages/internal-testing-tools/android/src/main/kotlin/com/datadog/reactnative/internaltesting/DdInternalTestingImplementation.kt index df0030fb5..b33bef6de 100644 --- a/packages/internal-testing-tools/android/src/main/kotlin/com/datadog/reactnative/internaltesting/DdInternalTestingImplementation.kt +++ b/packages/internal-testing-tools/android/src/main/kotlin/com/datadog/reactnative/internaltesting/DdInternalTestingImplementation.kt @@ -58,6 +58,12 @@ class DdInternalTestingImplementation { promise.resolve(null) } + /** + * Get wrapped core instance. + */ + internal fun getWrappedCore(): StubSDKCore? { + return wrappedCore + } internal companion object { internal const val NAME = "DdInternalTesting" diff --git a/packages/internal-testing-tools/android/src/test/kotlin/com/datadog/reactnative/internaltesting/DdInternalTestingImplementationTest.kt b/packages/internal-testing-tools/android/src/test/kotlin/com/datadog/reactnative/internaltesting/DdInternalTestingImplementationTest.kt index c542ed6bc..4a6938f9b 100644 --- a/packages/internal-testing-tools/android/src/test/kotlin/com/datadog/reactnative/internaltesting/DdInternalTestingImplementationTest.kt +++ b/packages/internal-testing-tools/android/src/test/kotlin/com/datadog/reactnative/internaltesting/DdInternalTestingImplementationTest.kt @@ -80,7 +80,9 @@ internal class DdInternalTestingImplementationTest { // Simulating DdSdkImplementation initialization DatadogSDKWrapperStorage.notifyOnInitializedListeners(mockCore) - val wrappedCore = Datadog.getInstance() as StubSDKCore + val wrappedCore = testedInternalTesting.getWrappedCore() + requireNotNull(wrappedCore) + wrappedCore.registerFeature(mockFeature) requireNotNull(wrappedCore.getFeature(mockFeature.name)) .withWriteContext { _, eventBatchWriter -> From 5183c0a8b69ef9570f6dfb80edfa406e9c3b9814 Mon Sep 17 00:00:00 2001 From: Sergio Barrio Date: Fri, 5 Sep 2025 15:29:37 +0200 Subject: [PATCH 062/410] Remove type interdependencies between modules --- packages/core/src/{rum => }/DdAttributes.ts | 0 .../src/DdSdkReactNativeConfiguration.tsx | 2 +- .../src/__tests__/DdSdkReactNative.test.tsx | 3 +- packages/core/src/index.tsx | 3 +- packages/core/src/logs/DdLogs.ts | 5 +- .../core/src/logs/__tests__/DdLogs.test.ts | 4 +- .../src/logs/__tests__/eventMapper.test.ts | 2 +- packages/core/src/logs/eventMapper.ts | 3 +- packages/core/src/logs/types.ts | 21 +------- packages/core/src/rum/DdRum.ts | 4 +- packages/core/src/rum/__tests__/DdRum.test.ts | 3 +- .../src/rum/eventMappers/errorEventMapper.ts | 2 +- .../instrumentation/DdRumErrorTracking.tsx | 2 +- packages/core/src/rum/types.ts | 10 +--- packages/core/src/types.tsx | 49 ++++++++++++++++--- 15 files changed, 62 insertions(+), 51 deletions(-) rename packages/core/src/{rum => }/DdAttributes.ts (100%) diff --git a/packages/core/src/rum/DdAttributes.ts b/packages/core/src/DdAttributes.ts similarity index 100% rename from packages/core/src/rum/DdAttributes.ts rename to packages/core/src/DdAttributes.ts diff --git a/packages/core/src/DdSdkReactNativeConfiguration.tsx b/packages/core/src/DdSdkReactNativeConfiguration.tsx index 9be08fa28..44debb2d2 100644 --- a/packages/core/src/DdSdkReactNativeConfiguration.tsx +++ b/packages/core/src/DdSdkReactNativeConfiguration.tsx @@ -7,12 +7,12 @@ import type { ProxyConfiguration } from './ProxyConfiguration'; import type { SdkVerbosity } from './SdkVerbosity'; import { TrackingConsent } from './TrackingConsent'; -import type { LogEventMapper } from './logs/types'; import type { ActionEventMapper } from './rum/eventMappers/actionEventMapper'; import type { ErrorEventMapper } from './rum/eventMappers/errorEventMapper'; import type { ResourceEventMapper } from './rum/eventMappers/resourceEventMapper'; import type { FirstPartyHost } from './rum/types'; import { PropagatorType } from './rum/types'; +import type { LogEventMapper } from './types'; export enum VitalsUpdateFrequency { FREQUENT = 'FREQUENT', diff --git a/packages/core/src/__tests__/DdSdkReactNative.test.tsx b/packages/core/src/__tests__/DdSdkReactNative.test.tsx index 49c0bd1f2..18bf060ce 100644 --- a/packages/core/src/__tests__/DdSdkReactNative.test.tsx +++ b/packages/core/src/__tests__/DdSdkReactNative.test.tsx @@ -17,11 +17,12 @@ import { DdRum } from '../rum/DdRum'; import { DdRumErrorTracking } from '../rum/instrumentation/DdRumErrorTracking'; import { DdRumUserInteractionTracking } from '../rum/instrumentation/interactionTracking/DdRumUserInteractionTracking'; import { DdRumResourceTracking } from '../rum/instrumentation/resourceTracking/DdRumResourceTracking'; -import { ErrorSource, PropagatorType, RumActionType } from '../rum/types'; +import { PropagatorType, RumActionType } from '../rum/types'; import { AttributesSingleton } from '../sdk/AttributesSingleton/AttributesSingleton'; import { DdSdk } from '../sdk/DdSdk'; import { GlobalState } from '../sdk/GlobalState/GlobalState'; import { UserInfoSingleton } from '../sdk/UserInfoSingleton/UserInfoSingleton'; +import { ErrorSource } from '../types'; import type { DdSdkConfiguration } from '../types'; import { version as sdkVersion } from '../version'; diff --git a/packages/core/src/index.tsx b/packages/core/src/index.tsx index 9332354dc..062fecc90 100644 --- a/packages/core/src/index.tsx +++ b/packages/core/src/index.tsx @@ -37,11 +37,12 @@ import { DATADOG_GRAPH_QL_VARIABLES_HEADER } from './rum/instrumentation/resourceTracking/graphql/graphqlHeaders'; import type { FirstPartyHost } from './rum/types'; -import { ErrorSource, PropagatorType, RumActionType } from './rum/types'; +import { PropagatorType, RumActionType } from './rum/types'; import { DatadogProvider } from './sdk/DatadogProvider/DatadogProvider'; import { DdSdk } from './sdk/DdSdk'; import { FileBasedConfiguration } from './sdk/FileBasedConfiguration/FileBasedConfiguration'; import { DdTrace } from './trace/DdTrace'; +import { ErrorSource } from './types'; import { DefaultTimeProvider } from './utils/time-provider/DefaultTimeProvider'; import type { Timestamp } from './utils/time-provider/TimeProvider'; import { TimeProvider } from './utils/time-provider/TimeProvider'; diff --git a/packages/core/src/logs/DdLogs.ts b/packages/core/src/logs/DdLogs.ts index e00e4fc0d..e1936d211 100644 --- a/packages/core/src/logs/DdLogs.ts +++ b/packages/core/src/logs/DdLogs.ts @@ -4,11 +4,11 @@ * Copyright 2016-Present Datadog, Inc. */ +import { DdAttributes } from '../DdAttributes'; import { DATADOG_MESSAGE_PREFIX, InternalLog } from '../InternalLog'; import { SdkVerbosity } from '../SdkVerbosity'; import type { DdNativeLogsType } from '../nativeModulesTypes'; -import { DdAttributes } from '../rum/DdAttributes'; -import type { ErrorSource } from '../rum/types'; +import type { ErrorSource, LogEventMapper } from '../types'; import { validateContext } from '../utils/argsUtils'; import { getGlobalInstance } from '../utils/singletonUtils'; @@ -16,7 +16,6 @@ import { generateEventMapper } from './eventMapper'; import type { DdLogsType, LogArguments, - LogEventMapper, LogWithErrorArguments, NativeLogWithError, RawLogWithError diff --git a/packages/core/src/logs/__tests__/DdLogs.test.ts b/packages/core/src/logs/__tests__/DdLogs.test.ts index 6a530ec36..f7e848a0b 100644 --- a/packages/core/src/logs/__tests__/DdLogs.test.ts +++ b/packages/core/src/logs/__tests__/DdLogs.test.ts @@ -11,9 +11,9 @@ import { DdSdkReactNative } from '../../DdSdkReactNative'; import { InternalLog } from '../../InternalLog'; import { SdkVerbosity } from '../../SdkVerbosity'; import type { DdNativeLogsType } from '../../nativeModulesTypes'; -import { ErrorSource } from '../../rum/types'; +import { ErrorSource } from '../../types'; +import type { LogEventMapper } from '../../types'; import { DdLogs } from '../DdLogs'; -import type { LogEventMapper } from '../types'; jest.mock('../../InternalLog', () => { return { diff --git a/packages/core/src/logs/__tests__/eventMapper.test.ts b/packages/core/src/logs/__tests__/eventMapper.test.ts index 0999a6058..cd505f811 100644 --- a/packages/core/src/logs/__tests__/eventMapper.test.ts +++ b/packages/core/src/logs/__tests__/eventMapper.test.ts @@ -5,7 +5,7 @@ */ /* eslint-disable @typescript-eslint/ban-ts-comment */ -import { ErrorSource } from '../../rum/types'; +import { ErrorSource } from '../../types'; import { formatRawLogToLogEvent } from '../eventMapper'; describe('formatRawLogToLogEvent', () => { diff --git a/packages/core/src/logs/eventMapper.ts b/packages/core/src/logs/eventMapper.ts index 2bbd398cb..eb7b5f22c 100644 --- a/packages/core/src/logs/eventMapper.ts +++ b/packages/core/src/logs/eventMapper.ts @@ -7,10 +7,9 @@ import type { Attributes } from '../sdk/AttributesSingleton/types'; import { EventMapper } from '../sdk/EventMappers/EventMapper'; import type { UserInfo } from '../sdk/UserInfoSingleton/types'; +import type { LogEvent, LogEventMapper } from '../types'; import type { - LogEvent, - LogEventMapper, NativeLog, NativeLogWithError, RawLog, diff --git a/packages/core/src/logs/types.ts b/packages/core/src/logs/types.ts index 9c1b3cb09..18cdbc533 100644 --- a/packages/core/src/logs/types.ts +++ b/packages/core/src/logs/types.ts @@ -4,8 +4,7 @@ * Copyright 2016-Present Datadog, Inc. */ -import type { ErrorSource } from '../rum/types'; -import type { UserInfo } from '../sdk/UserInfoSingleton/types'; +import type { LogStatus, ErrorSource } from '../types'; /** * The entry point to use Datadog's Logs feature. @@ -75,24 +74,6 @@ export type NativeLogWithError = { fingerprint?: string; }; -export type LogStatus = 'debug' | 'info' | 'warn' | 'error'; - -export type LogEvent = { - message: string; - context: object; - errorKind?: string; - errorMessage?: string; - stacktrace?: string; - fingerprint?: string; - readonly source?: ErrorSource; - // readonly date: number; // TODO: RUMM-2446 & RUMM-2447 - readonly status: LogStatus; - readonly userInfo: UserInfo; - readonly attributes?: object; -}; - -export type LogEventMapper = (logEvent: LogEvent) => LogEvent | null; - export type LogArguments = [message: string, context?: object]; export type LogWithErrorArguments = [ diff --git a/packages/core/src/rum/DdRum.ts b/packages/core/src/rum/DdRum.ts index fdc7a2ec8..b32ab410b 100644 --- a/packages/core/src/rum/DdRum.ts +++ b/packages/core/src/rum/DdRum.ts @@ -5,19 +5,20 @@ */ import type { GestureResponderEvent } from 'react-native'; +import { DdAttributes } from '../DdAttributes'; import { InternalLog } from '../InternalLog'; import { SdkVerbosity } from '../SdkVerbosity'; import type { DdNativeRumType } from '../nativeModulesTypes'; import { bufferVoidNativeCall } from '../sdk/DatadogProvider/Buffer/bufferNativeCall'; import { DdSdk } from '../sdk/DdSdk'; import { GlobalState } from '../sdk/GlobalState/GlobalState'; +import type { ErrorSource } from '../types'; import { validateContext } from '../utils/argsUtils'; import { getErrorContext } from '../utils/errorUtils'; import { getGlobalInstance } from '../utils/singletonUtils'; import { DefaultTimeProvider } from '../utils/time-provider/DefaultTimeProvider'; import type { TimeProvider } from '../utils/time-provider/TimeProvider'; -import { DdAttributes } from './DdAttributes'; import { generateActionEventMapper } from './eventMappers/actionEventMapper'; import type { ActionEventMapper } from './eventMappers/actionEventMapper'; import { generateErrorEventMapper } from './eventMappers/errorEventMapper'; @@ -37,7 +38,6 @@ import { setCachedSessionId } from './sessionId/sessionIdHelper'; import type { - ErrorSource, DdRumType, RumActionType, ResourceKind, diff --git a/packages/core/src/rum/__tests__/DdRum.test.ts b/packages/core/src/rum/__tests__/DdRum.test.ts index 527492891..7e5fc24de 100644 --- a/packages/core/src/rum/__tests__/DdRum.test.ts +++ b/packages/core/src/rum/__tests__/DdRum.test.ts @@ -12,6 +12,7 @@ import { SdkVerbosity } from '../../SdkVerbosity'; import { BufferSingleton } from '../../sdk/DatadogProvider/Buffer/BufferSingleton'; import { DdSdk } from '../../sdk/DdSdk'; import { GlobalState } from '../../sdk/GlobalState/GlobalState'; +import { ErrorSource } from '../../types'; import { DdRum } from '../DdRum'; import type { ActionEventMapper } from '../eventMappers/actionEventMapper'; import type { ErrorEventMapper } from '../eventMappers/errorEventMapper'; @@ -22,7 +23,7 @@ import { TracingIdFormat } from '../instrumentation/resourceTracking/distributed import { TracingIdentifierUtils } from '../instrumentation/resourceTracking/distributedTracing/__tests__/__utils__/TracingIdentifierUtils'; import { setCachedSessionId } from '../sessionId/sessionIdHelper'; import type { FirstPartyHost } from '../types'; -import { ErrorSource, PropagatorType, RumActionType } from '../types'; +import { PropagatorType, RumActionType } from '../types'; import * as TracingContextUtils from './__utils__/TracingHeadersUtils'; diff --git a/packages/core/src/rum/eventMappers/errorEventMapper.ts b/packages/core/src/rum/eventMappers/errorEventMapper.ts index 462754d2c..6630d59a4 100644 --- a/packages/core/src/rum/eventMappers/errorEventMapper.ts +++ b/packages/core/src/rum/eventMappers/errorEventMapper.ts @@ -6,7 +6,7 @@ import type { AdditionalEventDataForMapper } from '../../sdk/EventMappers/EventMapper'; import { EventMapper } from '../../sdk/EventMappers/EventMapper'; -import type { ErrorSource } from '../types'; +import type { ErrorSource } from '../../types'; type RawError = { message: string; diff --git a/packages/core/src/rum/instrumentation/DdRumErrorTracking.tsx b/packages/core/src/rum/instrumentation/DdRumErrorTracking.tsx index 5a45bdc65..3c3ec9f65 100644 --- a/packages/core/src/rum/instrumentation/DdRumErrorTracking.tsx +++ b/packages/core/src/rum/instrumentation/DdRumErrorTracking.tsx @@ -8,6 +8,7 @@ import type { ErrorHandlerCallback } from 'react-native'; import { InternalLog } from '../../InternalLog'; import { SdkVerbosity } from '../../SdkVerbosity'; +import { ErrorSource } from '../../types'; import { getErrorMessage, getErrorStackTrace, @@ -18,7 +19,6 @@ import { } from '../../utils/errorUtils'; import { executeWithDelay } from '../../utils/jsUtils'; import { DdRum } from '../DdRum'; -import { ErrorSource } from '../types'; /** * Provides RUM auto-instrumentation feature to track errors as RUM events. diff --git a/packages/core/src/rum/types.ts b/packages/core/src/rum/types.ts index 5834123a6..fc8d07c02 100644 --- a/packages/core/src/rum/types.ts +++ b/packages/core/src/rum/types.ts @@ -4,6 +4,8 @@ * Copyright 2016-Present Datadog, Inc. */ +import type { ErrorSource } from '../types'; + import type { DatadogTracingContext } from './instrumentation/resourceTracking/distributedTracing/DatadogTracingContext'; import type { DatadogTracingIdentifier } from './instrumentation/resourceTracking/distributedTracing/DatadogTracingIdentifier'; @@ -233,14 +235,6 @@ export type ResourceKind = | 'other' | 'native'; -export enum ErrorSource { - NETWORK = 'NETWORK', - SOURCE = 'SOURCE', - CONSOLE = 'CONSOLE', - WEBVIEW = 'WEBVIEW', - CUSTOM = 'CUSTOM' -} - /** * Type of instrumentation on the host. * - DATADOG: Datadog’s propagator (`x-datadog-*`) diff --git a/packages/core/src/types.tsx b/packages/core/src/types.tsx index 4db877469..e1c5096fb 100644 --- a/packages/core/src/types.tsx +++ b/packages/core/src/types.tsx @@ -5,6 +5,7 @@ */ import type { BatchProcessingLevel } from './DdSdkReactNativeConfiguration'; +import type { UserInfo as UserInfoSingleton } from './sdk/UserInfoSingleton/types'; declare global { // eslint-disable-next-line no-var, vars-on-top @@ -118,13 +119,6 @@ export type DdSdkType = { setTrackingConsent(trackingConsent: string): Promise; }; -export type UserInfo = { - id: string; - name?: string; - email?: string; - extraInfo?: object; -}; - /** * The entry point to use Datadog's Trace feature. */ @@ -153,3 +147,44 @@ export type DdTraceType = { timestampMs?: number ): Promise; }; + +// Shared types across modules + +// Core + +export type UserInfo = { + id: string; + name?: string; + email?: string; + extraInfo?: object; +}; + +// DdLogs + +export type LogStatus = 'debug' | 'info' | 'warn' | 'error'; + +export type LogEvent = { + message: string; + context: object; + errorKind?: string; + errorMessage?: string; + stacktrace?: string; + fingerprint?: string; + readonly source?: ErrorSource; + // readonly date: number; // TODO: RUMM-2446 & RUMM-2447 + readonly status: LogStatus; + readonly userInfo: UserInfoSingleton; + readonly attributes?: object; +}; + +export type LogEventMapper = (logEvent: LogEvent) => LogEvent | null; + +// DdRum + +export enum ErrorSource { + NETWORK = 'NETWORK', + SOURCE = 'SOURCE', + CONSOLE = 'CONSOLE', + WEBVIEW = 'WEBVIEW', + CUSTOM = 'CUSTOM' +} From 2fbba1431d66327f5a3d4335f2cda66aa6be5f51 Mon Sep 17 00:00:00 2001 From: Marco Saia Date: Tue, 9 Sep 2025 12:08:35 +0200 Subject: [PATCH 063/410] iOS: Always use SDK default core instance --- .../core/ios/Sources/DatadogSDKWrapper.swift | 44 ++++++-------- .../ios/Sources/DdLogsImplementation.swift | 7 ++- .../ios/Sources/DdSdkImplementation.swift | 17 ++++-- .../Sources/DdSdkNativeInitialization.swift | 22 ++++--- packages/core/ios/Sources/DdTelemetry.swift | 49 ++++++++++++++++ .../ios/Tests/DatadogSdkWrapperTests.swift | 17 +++--- packages/core/ios/Tests/DdSdkTests.swift | 58 ++++++++----------- .../DdSessionReplayImplementation.swift | 28 ++------- .../ios/Tests/DdSessionReplayTests.swift | 6 +- .../Sources/RCTDatadogWebViewTracking.swift | 18 +++--- .../DatadogSDKReactNativeWebViewTests.swift | 14 +++-- 11 files changed, 158 insertions(+), 122 deletions(-) create mode 100644 packages/core/ios/Sources/DdTelemetry.swift diff --git a/packages/core/ios/Sources/DatadogSDKWrapper.swift b/packages/core/ios/Sources/DatadogSDKWrapper.swift index 9cbac3bd8..3c56688b0 100644 --- a/packages/core/ios/Sources/DatadogSDKWrapper.swift +++ b/packages/core/ios/Sources/DatadogSDKWrapper.swift @@ -13,11 +13,15 @@ import DatadogCrashReporting import DatadogInternal import Foundation +<<<<<<< HEAD #if os(iOS) import DatadogWebViewTracking #endif public typealias OnCoreInitializedListener = (DatadogCoreProtocol) -> Void +======= +public typealias OnSdkInitializedListener = () -> Void +>>>>>>> 0443e0ff (iOS: Always use SDK default core instance) /// Wrapper around the Datadog SDK. Use DatadogSDKWrapper.shared to access the instance. public class DatadogSDKWrapper { @@ -25,25 +29,14 @@ public class DatadogSDKWrapper { public static var shared = DatadogSDKWrapper() // Initialization callbacks - internal var onCoreInitializedListeners: [OnCoreInitializedListener] = [] - internal var loggerConfiguration = DatadogLogs.Logger.Configuration() - // Core instance - private var coreInstance: DatadogCoreProtocol? = nil + internal var onSdkInitializedListeners: [OnSdkInitializedListener] = [] - private init() { } - - public func addOnCoreInitializedListener(listener:@escaping OnCoreInitializedListener) { - onCoreInitializedListeners.append(listener) - } + internal private(set) var loggerConfiguration = DatadogLogs.Logger.Configuration() - /// This is intended for internal testing only. - public func setCoreInstance(core: DatadogCoreProtocol?) { - self.coreInstance = core - } + private init() { } - /// This is not supposed to be used in the SDK itself, rather by other SDKs like Session Replay. - public func getCoreInstance() -> DatadogCoreProtocol? { - return coreInstance + public func addOnSdkInitializedListener(listener:@escaping OnSdkInitializedListener) { + onSdkInitializedListeners.append(listener) } // SDK Wrapper @@ -52,15 +45,16 @@ public class DatadogSDKWrapper { loggerConfiguration: DatadogLogs.Logger.Configuration, trackingConsent: TrackingConsent ) -> Void { - let core = Datadog.initialize(with: coreConfiguration, trackingConsent: trackingConsent) - setCoreInstance(core: core) - for listener in onCoreInitializedListeners { - listener(core) + Datadog.initialize(with: coreConfiguration, trackingConsent: trackingConsent) + + for listener in onSdkInitializedListeners { + listener() } self.loggerConfiguration = loggerConfiguration } +<<<<<<< HEAD internal func isInitialized() -> Bool { return Datadog.isInitialized() } @@ -161,17 +155,15 @@ public class DatadogSDKWrapper { #if os(iOS) +======= +>>>>>>> 0443e0ff (iOS: Always use SDK default core instance) // Webview private var webviewMessageEmitter: InternalExtension.AbstractMessageEmitter? internal func enableWebviewTracking() { - if let core = coreInstance { - webviewMessageEmitter = WebViewTracking._internal.messageEmitter(in: core) - } else { - consolePrint("Core instance was not found when initializing Webview tracking.", .critical) - } + webviewMessageEmitter = WebViewTracking._internal.messageEmitter(in: CoreRegistry.default) } - + internal func sendWebviewMessage(body: NSString) throws { try self.webviewMessageEmitter?.send(body: body) } diff --git a/packages/core/ios/Sources/DdLogsImplementation.swift b/packages/core/ios/Sources/DdLogsImplementation.swift index 264a3d0b3..fe3fde092 100644 --- a/packages/core/ios/Sources/DdLogsImplementation.swift +++ b/packages/core/ios/Sources/DdLogsImplementation.swift @@ -5,7 +5,9 @@ */ import Foundation +import DatadogInternal import DatadogLogs +import DatadogCore @objc public class DdLogsImplementation: NSObject { @@ -20,7 +22,10 @@ public class DdLogsImplementation: NSObject { @objc public override convenience init() { - self.init({ DatadogSDKWrapper.shared.createLogger() }, { DatadogSDKWrapper.shared.isInitialized() }) + self.init( + { DatadogLogs.Logger.create(with: DatadogSDKWrapper.shared.loggerConfiguration) }, + { Datadog.isInitialized() } + ) } @objc diff --git a/packages/core/ios/Sources/DdSdkImplementation.swift b/packages/core/ios/Sources/DdSdkImplementation.swift index b4ddc59fd..973b8db9f 100644 --- a/packages/core/ios/Sources/DdSdkImplementation.swift +++ b/packages/core/ios/Sources/DdSdkImplementation.swift @@ -139,42 +139,47 @@ public class DdSdkImplementation: NSObject { public func sendTelemetryLog(message: NSString, attributes: NSDictionary, config: NSDictionary, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { let castedAttributes = castAttributesToSwift(attributes) let castedConfig = castAttributesToSwift(config) - DatadogSDKWrapper.shared.sendTelemetryLog(message: message as String, attributes: castedAttributes, config: castedConfig) + DdTelemetry.sendTelemetryLog(message: message as String, attributes: castedAttributes, config: castedConfig) resolve(nil) } @objc public func telemetryDebug(message: NSString, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { - DatadogSDKWrapper.shared.telemetryDebug(id: "datadog_react_native:\(message)", message: message as String) + DdTelemetry.telemetryDebug(id: "datadog_react_native:\(message)", message: message as String) resolve(nil) } @objc public func telemetryError(message: NSString, stack: NSString, kind: NSString, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { - DatadogSDKWrapper.shared.telemetryError(id: "datadog_react_native:\(String(describing: kind)):\(message)", message: message as String, kind: kind as String, stack: stack as String) + DdTelemetry.telemetryError(id: "datadog_react_native:\(String(describing: kind)):\(message)", message: message as String, kind: kind as String, stack: stack as String) resolve(nil) } +<<<<<<< HEAD #if os(iOS) +======= + +>>>>>>> 0443e0ff (iOS: Always use SDK default core instance) @objc public func consumeWebviewEvent(message: NSString, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { do{ try DatadogSDKWrapper.shared.sendWebviewMessage(body: message) } catch { - DatadogSDKWrapper.shared.telemetryError(id: "datadog_react_native:\(error.localizedDescription)", message: "The message being sent was:\(message)" as String, kind: "WebViewEventBridgeError" as String, stack: String(describing: error) as String) + DdTelemetry.telemetryError(id: "datadog_react_native:\(error.localizedDescription)", message: "The message being sent was:\(message)" as String, kind: "WebViewEventBridgeError" as String, stack: String(describing: error) as String) } + resolve(nil) } #endif @objc public func clearAllData(resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { - DatadogSDKWrapper.shared.clearAllData() + Datadog.clearAllData() resolve(nil) } func overrideReactNativeTelemetry(rnConfiguration: DdSdkConfiguration) -> Void { - DatadogSDKWrapper.shared.overrideTelemetryConfiguration( + DdTelemetry.overrideTelemetryConfiguration( initializationType: rnConfiguration.configurationForTelemetry?.initializationType as? String, reactNativeVersion: rnConfiguration.configurationForTelemetry?.reactNativeVersion as? String, reactVersion: rnConfiguration.configurationForTelemetry?.reactVersion as? String, diff --git a/packages/core/ios/Sources/DdSdkNativeInitialization.swift b/packages/core/ios/Sources/DdSdkNativeInitialization.swift index dec3cbcb2..4ea229871 100644 --- a/packages/core/ios/Sources/DdSdkNativeInitialization.swift +++ b/packages/core/ios/Sources/DdSdkNativeInitialization.swift @@ -33,12 +33,13 @@ public class DdSdkNativeInitialization: NSObject { } internal func initialize(sdkConfiguration: DdSdkConfiguration) { - // TODO: see if this `if` is still needed - if DatadogSDKWrapper.shared.isInitialized() { - // Initializing the SDK twice results in Global.rum and - // Global.sharedTracer to be set to no-op instances + if Datadog.isInitialized(instanceName: CoreRegistry.defaultInstanceName) { + // Initializing the SDK twice results in Global.rum and Global.sharedTracer to be set to no-op instances consolePrint("Datadog SDK is already initialized, skipping initialization.", .debug) - DatadogSDKWrapper.shared.telemetryDebug(id: "datadog_react_native: RN SDK was already initialized in native", message: "RN SDK was already initialized in native") + DdTelemetry.telemetryDebug( + id: "datadog_react_native: RN SDK was already initialized in native", + message: "RN SDK was already initialized in native" + ) RUMMonitor.shared().currentSessionID { sessionId in guard let id = sessionId else { return } @@ -81,21 +82,24 @@ public class DdSdkNativeInitialization: NSObject { func enableFeatures(sdkConfiguration: DdSdkConfiguration) { let rumConfig = buildRUMConfiguration(configuration: sdkConfiguration) - DatadogSDKWrapper.shared.enableRUM(with: rumConfig) + RUM.enable(with: rumConfig) let logsConfig = buildLogsConfiguration(configuration: sdkConfiguration) - DatadogSDKWrapper.shared.enableLogs(with: logsConfig) + Logs.enable(with: logsConfig) let traceConfig = buildTraceConfiguration(configuration: sdkConfiguration) - DatadogSDKWrapper.shared.enableTrace(with: traceConfig) + Trace.enable(with: traceConfig) if sdkConfiguration.nativeCrashReportEnabled ?? false { - DatadogSDKWrapper.shared.enableCrashReporting() + CrashReporting.enable() } +<<<<<<< HEAD #if os(iOS) DatadogSDKWrapper.shared.enableWebviewTracking() #endif +======= +>>>>>>> 0443e0ff (iOS: Always use SDK default core instance) } func buildSDKConfiguration(configuration: DdSdkConfiguration, defaultAppVersion: String = getDefaultAppVersion()) -> Datadog.Configuration { diff --git a/packages/core/ios/Sources/DdTelemetry.swift b/packages/core/ios/Sources/DdTelemetry.swift new file mode 100644 index 000000000..cb1896db8 --- /dev/null +++ b/packages/core/ios/Sources/DdTelemetry.swift @@ -0,0 +1,49 @@ + +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2025 Datadog, Inc. + */ +import DatadogCore +import DatadogInternal + +public class DdTelemetry { + public static func sendTelemetryLog(message: String, attributes: [String: any Encodable], config: [String: any Encodable]) { + let id = (config["onlyOnce"] as? Bool) == true ? message : UUID().uuidString + CoreRegistry.default.telemetry.debug(id: id, message: message, attributes: attributes) + } + + public static func telemetryDebug(id: String, message: String) { + return Datadog._internal.telemetry.debug(id: id, message: message) + } + + public static func telemetryError(id: String, message: String, kind: String?, stack: String?) { + return Datadog._internal.telemetry.error(id: id, message: message, kind: kind, stack: stack) + } + + public static func overrideTelemetryConfiguration( + initializationType: String? = nil, + reactNativeVersion: String? = nil, + reactVersion: String? = nil, + trackCrossPlatformLongTasks: Bool? = nil, + trackErrors: Bool? = nil, + trackInteractions: Bool? = nil, + trackLongTask: Bool? = nil, + trackNativeErrors: Bool? = nil, + trackNativeLongTasks: Bool? = nil, + trackNetworkRequests: Bool? = nil + ) { + CoreRegistry.default.telemetry.configuration( + initializationType: initializationType, + reactNativeVersion: reactNativeVersion, + reactVersion: reactVersion, + trackCrossPlatformLongTasks: trackCrossPlatformLongTasks, + trackErrors: trackErrors, + trackLongTask: trackLongTask, + trackNativeErrors: trackNativeErrors, + trackNativeLongTasks: trackNativeLongTasks, + trackNetworkRequests: trackNetworkRequests, + trackUserInteractions: trackInteractions + ) + } +} diff --git a/packages/core/ios/Tests/DatadogSdkWrapperTests.swift b/packages/core/ios/Tests/DatadogSdkWrapperTests.swift index 4811b4c1d..a445b2bbe 100644 --- a/packages/core/ios/Tests/DatadogSdkWrapperTests.swift +++ b/packages/core/ios/Tests/DatadogSdkWrapperTests.swift @@ -8,22 +8,23 @@ import XCTest @testable import DatadogSDKReactNative import DatadogTrace import DatadogInternal - +import DatadogRUM +import DatadogLogs internal class DatadogSdkWrapperTests: XCTestCase { override func setUp() { super.setUp() - DatadogSDKWrapper.shared.setCoreInstance(core: nil) - DatadogSDKWrapper.shared.onCoreInitializedListeners = [] + DatadogSDKWrapper.shared.onSdkInitializedListeners = [] } - func testItSetsCoreUsedForFeatures() { + func testOverrideCoreRegistryDefault() { let coreMock = MockDatadogCore() - DatadogSDKWrapper.shared.setCoreInstance(core: coreMock) + CoreRegistry.register(default: coreMock) + defer { CoreRegistry.unregisterDefault() } - DatadogSDKWrapper.shared.enableTrace(with: .init()) - DatadogSDKWrapper.shared.enableRUM(with: .init(applicationID: "app-id")) - DatadogSDKWrapper.shared.enableLogs(with: .init()) + Trace.enable(with: .init()) + RUM.enable(with: .init(applicationID: "app-id")) + Logs.enable(with: .init()) XCTAssertNotNil(coreMock.features["tracing"]) XCTAssertNotNil(coreMock.features["rum"]) diff --git a/packages/core/ios/Tests/DdSdkTests.swift b/packages/core/ios/Tests/DdSdkTests.swift index edbbba4dd..bcc3252a8 100644 --- a/packages/core/ios/Tests/DdSdkTests.swift +++ b/packages/core/ios/Tests/DdSdkTests.swift @@ -34,8 +34,7 @@ class DdSdkTests: XCTestCase { private func mockReject(args _: String?, arg _: String?, err _: Error?) {} override func tearDown() { - DatadogSDKWrapper.shared.setCoreInstance(core: nil) - DatadogSDKWrapper.shared.onCoreInitializedListeners = [] + DatadogSDKWrapper.shared.onSdkInitializedListeners = [] Datadog.internalFlushAndDeinitialize() } @@ -84,11 +83,10 @@ class DdSdkTests: XCTestCase { let bridge = DispatchQueueMock() let mockJSRefreshRateMonitor = MockJSRefreshRateMonitor() let mockListener = MockOnCoreInitializedListener() - DatadogSDKWrapper.shared.addOnCoreInitializedListener(listener: mockListener.listener) + DatadogSDKWrapper.shared.addOnSdkInitializedListener(listener: mockListener.listener) - let expectation = self.expectation(description: "Core is set when promise resolves") + let expectation = self.expectation(description: "Listener is called when promise resolves") func mockPromiseResolve(_: Any?) { - XCTAssertNotNil(mockListener.core) expectation.fulfill() } @@ -276,9 +274,9 @@ class DdSdkTests: XCTestCase { } func testSDKInitializationWithOnInitializedCallback() { - var coreFromCallback: DatadogCoreProtocol? = nil - DatadogSDKWrapper.shared.addOnCoreInitializedListener(listener: { core in - coreFromCallback = core + var isInitialized = false + DatadogSDKWrapper.shared.addOnSdkInitializedListener(listener: { + isInitialized = Datadog.isInitialized() }) DdSdkImplementation( @@ -293,14 +291,16 @@ class DdSdkTests: XCTestCase { reject: mockReject ) - XCTAssertNotNil(coreFromCallback) + XCTAssertTrue(isInitialized) } func testEnableAllFeatures() { let core = MockDatadogCore() + CoreRegistry.register(default: core) + defer { CoreRegistry.unregisterDefault() } + let configuration: DdSdkConfiguration = .mockAny() - DatadogSDKWrapper.shared.setCoreInstance(core: core) DdSdkNativeInitialization().enableFeatures( sdkConfiguration: configuration ) @@ -479,9 +479,11 @@ class DdSdkTests: XCTestCase { func testBuildConfigurationWithCrashReport() { let core = MockDatadogCore() + CoreRegistry.register(default: core) + defer { CoreRegistry.unregisterDefault() } + let configuration: DdSdkConfiguration = .mockAny(nativeCrashReportEnabled: true) - DatadogSDKWrapper.shared.setCoreInstance(core: core) DdSdkNativeInitialization().enableFeatures( sdkConfiguration: configuration ) @@ -1233,6 +1235,9 @@ class DdSdkTests: XCTestCase { func testConfigurationTelemetryOverride() throws { let core = MockDatadogCore() + CoreRegistry.register(default: core) + defer { CoreRegistry.unregisterDefault() } + let configuration: DdSdkConfiguration = .mockAny( nativeCrashReportEnabled: false, nativeLongTaskThresholdMs: 0.0, @@ -1244,7 +1249,6 @@ class DdSdkTests: XCTestCase { ] ) - DatadogSDKWrapper.shared.setCoreInstance(core: core) DdSdkImplementation().overrideReactNativeTelemetry(rnConfiguration: configuration) XCTAssertEqual(core.configuration?.initializationType, "LEGACY") @@ -1313,11 +1317,12 @@ class DdSdkTests: XCTestCase { XCTAssertTrue(bridge.isSameQueue(queue: mockJSRefreshRateMonitor.jsQueue!)) } - func testCallsOnCoreInitializedListeners() throws { + func testCallsOnSdkInitializedListeners() throws { let bridge = DispatchQueueMock() let mockJSRefreshRateMonitor = MockJSRefreshRateMonitor() let mockListener = MockOnCoreInitializedListener() - DatadogSDKWrapper.shared.addOnCoreInitializedListener(listener: mockListener.listener) + + DatadogSDKWrapper.shared.addOnSdkInitializedListener(listener: mockListener.listener) DdSdkImplementation( mainDispatchQueue: DispatchQueueMock(), @@ -1331,23 +1336,7 @@ class DdSdkTests: XCTestCase { reject: mockReject ) - XCTAssertNotNil(mockListener.core) - } - - func testConsumeWebviewEvent() throws { - let configuration: DdSdkConfiguration = .mockAny() - let core = MockDatadogCore() - - DatadogSDKWrapper.shared.setCoreInstance(core: core) - DdSdkNativeInitialization().enableFeatures( - sdkConfiguration: configuration - ) - - DdSdkImplementation().consumeWebviewEvent( - message: "{\"eventType\":\"rum\",\"event\":{\"blabla\":\"custom message\"}}", - resolve: mockResolve, reject: mockReject) - - XCTAssertNotNil(core.baggages["browser-rum-event"]) + XCTAssertTrue(mockListener.called) } func testInitialResourceThreshold() { @@ -1601,9 +1590,8 @@ extension DdSdkImplementation { } class MockOnCoreInitializedListener { - var core: DatadogCoreProtocol? - - func listener(core: DatadogCoreProtocol) { - self.core = core + var called = false + func listener() { + self.called = true } } diff --git a/packages/react-native-session-replay/ios/Sources/DdSessionReplayImplementation.swift b/packages/react-native-session-replay/ios/Sources/DdSessionReplayImplementation.swift index 45cfc2582..0fc9c6637 100644 --- a/packages/react-native-session-replay/ios/Sources/DdSessionReplayImplementation.swift +++ b/packages/react-native-session-replay/ios/Sources/DdSessionReplayImplementation.swift @@ -6,6 +6,7 @@ import Foundation @_spi(Internal) import DatadogSessionReplay +import DatadogCore import DatadogInternal import DatadogSDKReactNative import React @@ -66,8 +67,6 @@ public class DdSessionReplayImplementation: NSObject { customEndpoint: customEndpointURL ) -// let bundle = Bundle(for: DdSessionReplayImplementation.self) - var svgMap: [String: SVGData] = [:] if let bundle = Bundle.ddSessionReplayResources, @@ -92,38 +91,21 @@ public class DdSessionReplayImplementation: NSObject { fabricWrapper: fabricWrapper ) ]) - - if let core = DatadogSDKWrapper.shared.getCoreInstance() { - sessionReplay.enable( - with: sessionReplayConfiguration, - in: core - ) - } else { - consolePrint("Core instance was not found when initializing Session Replay.", .critical) - } + + sessionReplay.enable(with: sessionReplayConfiguration, in: CoreRegistry.default) resolve(nil) } @objc public func startRecording(resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) -> Void { - if let core = DatadogSDKWrapper.shared.getCoreInstance() { - sessionReplay.startRecording(in: core) - } else { - consolePrint("Core instance was not found when calling startRecording in Session Replay.", .critical) - } - + sessionReplay.startRecording(in: CoreRegistry.default) resolve(nil) } @objc public func stopRecording(resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) -> Void { - if let core = DatadogSDKWrapper.shared.getCoreInstance() { - sessionReplay.stopRecording(in: core) - } else { - consolePrint("Core instance was not found when calling stopRecording in Session Replay.", .critical) - } - + sessionReplay.stopRecording(in: CoreRegistry.default) resolve(nil) } diff --git a/packages/react-native-session-replay/ios/Tests/DdSessionReplayTests.swift b/packages/react-native-session-replay/ios/Tests/DdSessionReplayTests.swift index aa9102850..067a11df0 100644 --- a/packages/react-native-session-replay/ios/Tests/DdSessionReplayTests.swift +++ b/packages/react-native-session-replay/ios/Tests/DdSessionReplayTests.swift @@ -35,7 +35,11 @@ internal class DdSessionReplayTests: XCTestCase { override func setUp() { super.setUp() let mockDatadogCore = MockDatadogCore() - DatadogSDKWrapper.shared.setCoreInstance(core: mockDatadogCore) + CoreRegistry.register(default: mockDatadogCore) + } + + override func tearDown() { + CoreRegistry.unregisterDefault() } func testEnablesSessionReplayWithZeroReplaySampleRate() { diff --git a/packages/react-native-webview/ios/Sources/RCTDatadogWebViewTracking.swift b/packages/react-native-webview/ios/Sources/RCTDatadogWebViewTracking.swift index 97fd835b5..45f11e452 100644 --- a/packages/react-native-webview/ios/Sources/RCTDatadogWebViewTracking.swift +++ b/packages/react-native-webview/ios/Sources/RCTDatadogWebViewTracking.swift @@ -8,26 +8,27 @@ import WebKit import DatadogWebViewTracking import DatadogSDKReactNative import DatadogCore +import DatadogInternal @objc public class RCTDatadogWebViewTracking: NSObject { var webView: RCTDatadogWebView? = nil var allowedHosts: Set = Set() - var coreListener: OnCoreInitializedListener? + var onSdkInitializedListener: OnSdkInitializedListener? public override init() { super.init() - self.coreListener = { [weak self] (core: DatadogCoreProtocol) in + self.onSdkInitializedListener = { [weak self] in guard let strongSelf = self, let webView = strongSelf.webView else { return } strongSelf.enableWebViewTracking( webView: webView, allowedHosts: strongSelf.allowedHosts, - core: core + core: CoreRegistry.default ) } } - + /** Enables tracking on the given WebView. @@ -42,15 +43,16 @@ import DatadogCore guard !webView.isTrackingEnabled else { return } - if let core = DatadogSDKWrapper.shared.getCoreInstance() { - enableWebViewTracking(webView: webView, allowedHosts: allowedHosts, core: core) - } else if let coreListener = self.coreListener { - DatadogSDKWrapper.shared.addOnCoreInitializedListener(listener: coreListener) + if CoreRegistry.isRegistered(instanceName: CoreRegistry.defaultInstanceName) { + enableWebViewTracking(webView: webView, allowedHosts: allowedHosts, core: CoreRegistry.default) + } else if let onSdkInitializedListener = self.onSdkInitializedListener { + DatadogSDKWrapper.shared.addOnSdkInitializedListener(listener: onSdkInitializedListener) } else { // TODO: Report initialization problem } } + private func enableWebViewTracking( webView: RCTDatadogWebView, allowedHosts: Set, diff --git a/packages/react-native-webview/ios/Tests/DatadogSDKReactNativeWebViewTests.swift b/packages/react-native-webview/ios/Tests/DatadogSDKReactNativeWebViewTests.swift index 7ea36bda8..20f1b84fa 100644 --- a/packages/react-native-webview/ios/Tests/DatadogSDKReactNativeWebViewTests.swift +++ b/packages/react-native-webview/ios/Tests/DatadogSDKReactNativeWebViewTests.swift @@ -17,7 +17,11 @@ internal class DatadogSDKReactNativeWebViewTests: XCTestCase { override func setUp() { super.setUp() let mockDatadogCore = MockDatadogCore() - DatadogSDKWrapper.shared.setCoreInstance(core: mockDatadogCore) + CoreRegistry.register(default: mockDatadogCore) + } + + override func tearDown() { + CoreRegistry.unregisterDefault() } func testDatadogWebViewManagerReturnsDatadogWebView() { @@ -41,9 +45,10 @@ internal class DatadogSDKReactNativeWebViewTests: XCTestCase { XCTAssertFalse(view.isTrackingEnabled) } - func testDatadogWebViewTrackingIsDisabledIfCoreIsNotReady() { + func testDatadogWebViewTrackingIsDisabledIfSdkIsNotInitialized() { // Given - DatadogSDKWrapper.shared.setCoreInstance(core: nil) + CoreRegistry.unregisterDefault() + let viewManager = RCTDatadogWebViewManager() let allowedHosts = NSArray(objects: "example1.com", "example2.com") @@ -82,7 +87,7 @@ internal class DatadogSDKReactNativeWebViewTests: XCTestCase { view.addSubview(WKWebView()) - DatadogSDKWrapper.shared.setCoreInstance(core: nil) + CoreRegistry.unregisterDefault() // Given let selector = NSSelectorFromString("setupDatadogWebView:view:") @@ -92,7 +97,6 @@ internal class DatadogSDKReactNativeWebViewTests: XCTestCase { XCTAssertFalse(view.isTrackingEnabled) // When - DatadogSDKWrapper.shared.setCoreInstance(core: MockDatadogCore()) DatadogSDKWrapper.shared.callInitialize() let expectation = self.expectation(description: "WebView tracking is enabled through the listener.") From 51280318da38839cce13f33f4f8d033680412c5d Mon Sep 17 00:00:00 2001 From: Sergio Barrio Date: Mon, 8 Sep 2025 15:27:17 +0200 Subject: [PATCH 064/410] Bump native SDK dependencies to 3.0.0 --- benchmarks/android/app/build.gradle | 2 +- benchmarks/ios/Podfile.lock | 74 +++++++------- bump-native-dd-sdk.sh | 2 +- example-new-architecture/ios/Podfile.lock | 70 +++++++------- example/ios/Podfile.lock | 96 +++++++++---------- packages/core/DatadogSDKReactNative.podspec | 12 +-- packages/core/android/build.gradle | 10 +- ...DatadogSDKReactNativeSessionReplay.podspec | 2 +- .../android/build.gradle | 4 +- .../DatadogSDKReactNativeWebView.podspec | 4 +- .../react-native-webview/android/build.gradle | 2 +- 11 files changed, 139 insertions(+), 139 deletions(-) diff --git a/benchmarks/android/app/build.gradle b/benchmarks/android/app/build.gradle index 86d26791f..7dc4b3707 100644 --- a/benchmarks/android/app/build.gradle +++ b/benchmarks/android/app/build.gradle @@ -129,5 +129,5 @@ dependencies { // Benchmark tools from dd-sdk-android are used for vitals recording // Remember to bump thid alongside the main dd-sdk-android dependencies - implementation("com.datadoghq:dd-sdk-android-benchmark-internal:2.25.0") + implementation("com.datadoghq:dd-sdk-android-benchmark-internal:3.0.0") } diff --git a/benchmarks/ios/Podfile.lock b/benchmarks/ios/Podfile.lock index f42e0cd31..90043a48d 100644 --- a/benchmarks/ios/Podfile.lock +++ b/benchmarks/ios/Podfile.lock @@ -1,22 +1,22 @@ PODS: - boost (1.84.0) - - DatadogCore (2.30.2): - - DatadogInternal (= 2.30.2) - - DatadogCrashReporting (2.30.2): - - DatadogInternal (= 2.30.2) + - DatadogCore (3.0.0): + - DatadogInternal (= 3.0.0) + - DatadogCrashReporting (3.0.0): + - DatadogInternal (= 3.0.0) - PLCrashReporter (~> 1.12.0) - - DatadogInternal (2.30.2) - - DatadogLogs (2.30.2): - - DatadogInternal (= 2.30.2) - - DatadogRUM (2.30.2): - - DatadogInternal (= 2.30.2) - - DatadogSDKReactNative (2.13.2): - - DatadogCore (= 2.30.2) - - DatadogCrashReporting (= 2.30.2) - - DatadogLogs (= 2.30.2) - - DatadogRUM (= 2.30.2) - - DatadogTrace (= 2.30.2) - - DatadogWebViewTracking (= 2.30.2) + - DatadogInternal (3.0.0) + - DatadogLogs (3.0.0): + - DatadogInternal (= 3.0.0) + - DatadogRUM (3.0.0): + - DatadogInternal (= 3.0.0) + - DatadogSDKReactNative (2.12.1): + - DatadogCore (= 3.0.0) + - DatadogCrashReporting (= 3.0.0) + - DatadogLogs (= 3.0.0) + - DatadogRUM (= 3.0.0) + - DatadogTrace (= 3.0.0) + - DatadogWebViewTracking (= 3.0.0) - DoubleConversion - glog - hermes-engine @@ -39,7 +39,7 @@ PODS: - Yoga - DatadogSDKReactNativeSessionReplay (2.13.2): - DatadogSDKReactNative - - DatadogSessionReplay (= 2.30.2) + - DatadogSessionReplay (= 3.0.0) - DoubleConversion - glog - hermes-engine @@ -60,10 +60,10 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - DatadogSDKReactNativeWebView (2.13.2): - - DatadogInternal (= 2.30.2) + - DatadogSDKReactNativeWebView (2.12.1): + - DatadogInternal (= 3.0.0) - DatadogSDKReactNative - - DatadogWebViewTracking (= 2.30.2) + - DatadogWebViewTracking (= 3.0.0) - DoubleConversion - glog - hermes-engine @@ -84,13 +84,13 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - DatadogSessionReplay (2.30.2): - - DatadogInternal (= 2.30.2) - - DatadogTrace (2.30.2): - - DatadogInternal (= 2.30.2) + - DatadogSessionReplay (3.0.0): + - DatadogInternal (= 3.0.0) + - DatadogTrace (3.0.0): + - DatadogInternal (= 3.0.0) - OpenTelemetrySwiftApi (= 1.13.1) - - DatadogWebViewTracking (2.30.2): - - DatadogInternal (= 2.30.2) + - DatadogWebViewTracking (3.0.0): + - DatadogInternal (= 3.0.0) - DoubleConversion (1.1.6) - fast_float (6.1.4) - FBLazyVector (0.78.2) @@ -2070,17 +2070,17 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: boost: 7e761d76ca2ce687f7cc98e698152abd03a18f90 - DatadogCore: 8e50ad6cb68343f701707f7eeca16e0acb52ed8c - DatadogCrashReporting: 34763d4276d4fce286c7e8729cfcd2ac04dca43b - DatadogInternal: bd8672d506a7e67936ed5f7ca612169e49029b44 - DatadogLogs: d683aa9e0c9339f5ae679ead70bbdbe41cdc32f6 - DatadogRUM: 8b794aa458e6323ea9b1cef3f820fd3d092cbe27 - DatadogSDKReactNative: 4138a93c3168b4378de498052d0e00ea9560339d - DatadogSDKReactNativeSessionReplay: baa4de76d97d5f03303dde0e0db922a2fd812019 - DatadogSDKReactNativeWebView: 399e43d18902e3012c968fb9b7fd634b3936a882 - DatadogSessionReplay: 56a91d799fe34967c5ae79a222364e37d67020f5 - DatadogTrace: 3ba194791267efa09634234749111cac95abd3e5 - DatadogWebViewTracking: 8287d5ad06e992de5e46dd72a17e05c7513344be + DatadogCore: 0c39ba4ef3dee02071f235f2fb56af7e29f4ceb0 + DatadogCrashReporting: 604e65e524cb2fc22cda39fd9c9ea5fd3bddf24e + DatadogInternal: 442673a7fb5329299b489b91ed705aa2085380f7 + DatadogLogs: 0146a0e3140ba62fd42729593c0910d692aea5fd + DatadogRUM: f97fbd0290ecec5bc952fb03a6a1ae068649243f + DatadogSDKReactNative: 241bf982c16ceff03d94a58e6d005e55c47b99c6 + DatadogSDKReactNativeSessionReplay: bba36092686e3183e97c1a0c7f4ca8142582ae28 + DatadogSDKReactNativeWebView: 6da060df20e235abac533e582d9fc2b3a5070840 + DatadogSessionReplay: 0357b911c6ab42c77c93b29761ecc20d9282e971 + DatadogTrace: d714e7c456e612b7f171483d08086a59aa1c4213 + DatadogWebViewTracking: fb9591c09fca82b143f3843a7164a7e782175c53 DoubleConversion: cb417026b2400c8f53ae97020b2be961b59470cb fast_float: 06eeec4fe712a76acc9376682e4808b05ce978b6 FBLazyVector: e32d34492c519a2194ec9d7f5e7a79d11b73f91c diff --git a/bump-native-dd-sdk.sh b/bump-native-dd-sdk.sh index 8545802db..67d99a0e7 100755 --- a/bump-native-dd-sdk.sh +++ b/bump-native-dd-sdk.sh @@ -23,7 +23,7 @@ podspec_files=( "packages/react-native-webview/DatadogSDKReactNativeWebView.podspec" ) -ios_pattern="('Datadog[^']+', '~> )[0-9.]+'" +ios_pattern="('Datadog[^']+', ')[0-9.]+'" android_pattern='(com\.datadoghq:dd-sdk-android-[^:"]+):[0-9.]+' if [[ "$sdk" == "ios" ]]; then diff --git a/example-new-architecture/ios/Podfile.lock b/example-new-architecture/ios/Podfile.lock index 84b5f1d8c..cc9913ce3 100644 --- a/example-new-architecture/ios/Podfile.lock +++ b/example-new-architecture/ios/Podfile.lock @@ -1,22 +1,22 @@ PODS: - boost (1.84.0) - - DatadogCore (2.30.2): - - DatadogInternal (= 2.30.2) - - DatadogCrashReporting (2.30.2): - - DatadogInternal (= 2.30.2) + - DatadogCore (3.0.0): + - DatadogInternal (= 3.0.0) + - DatadogCrashReporting (3.0.0): + - DatadogInternal (= 3.0.0) - PLCrashReporter (~> 1.12.0) - - DatadogInternal (2.30.2) - - DatadogLogs (2.30.2): - - DatadogInternal (= 2.30.2) - - DatadogRUM (2.30.2): - - DatadogInternal (= 2.30.2) - - DatadogSDKReactNative (2.13.2): - - DatadogCore (= 2.30.2) - - DatadogCrashReporting (= 2.30.2) - - DatadogLogs (= 2.30.2) - - DatadogRUM (= 2.30.2) - - DatadogTrace (= 2.30.2) - - DatadogWebViewTracking (= 2.30.2) + - DatadogInternal (3.0.0) + - DatadogLogs (3.0.0): + - DatadogInternal (= 3.0.0) + - DatadogRUM (3.0.0): + - DatadogInternal (= 3.0.0) + - DatadogSDKReactNative (2.12.1): + - DatadogCore (= 3.0.0) + - DatadogCrashReporting (= 3.0.0) + - DatadogLogs (= 3.0.0) + - DatadogRUM (= 3.0.0) + - DatadogTrace (= 3.0.0) + - DatadogWebViewTracking (= 3.0.0) - DoubleConversion - glog - hermes-engine @@ -37,13 +37,13 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - DatadogSDKReactNative/Tests (2.13.2): - - DatadogCore (= 2.30.2) - - DatadogCrashReporting (= 2.30.2) - - DatadogLogs (= 2.30.2) - - DatadogRUM (= 2.30.2) - - DatadogTrace (= 2.30.2) - - DatadogWebViewTracking (= 2.30.2) + - DatadogSDKReactNative/Tests (2.12.1): + - DatadogCore (= 3.0.0) + - DatadogCrashReporting (= 3.0.0) + - DatadogLogs (= 3.0.0) + - DatadogRUM (= 3.0.0) + - DatadogTrace (= 3.0.0) + - DatadogWebViewTracking (= 3.0.0) - DoubleConversion - glog - hermes-engine @@ -64,11 +64,11 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - DatadogTrace (2.30.2): - - DatadogInternal (= 2.30.2) + - DatadogTrace (3.0.0): + - DatadogInternal (= 3.0.0) - OpenTelemetrySwiftApi (= 1.13.1) - - DatadogWebViewTracking (2.30.2): - - DatadogInternal (= 2.30.2) + - DatadogWebViewTracking (3.0.0): + - DatadogInternal (= 3.0.0) - DoubleConversion (1.1.6) - fast_float (6.1.4) - FBLazyVector (0.76.9) @@ -1850,14 +1850,14 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: boost: 1dca942403ed9342f98334bf4c3621f011aa7946 - DatadogCore: 8e50ad6cb68343f701707f7eeca16e0acb52ed8c - DatadogCrashReporting: 34763d4276d4fce286c7e8729cfcd2ac04dca43b - DatadogInternal: bd8672d506a7e67936ed5f7ca612169e49029b44 - DatadogLogs: d683aa9e0c9339f5ae679ead70bbdbe41cdc32f6 - DatadogRUM: 8b794aa458e6323ea9b1cef3f820fd3d092cbe27 - DatadogSDKReactNative: afd267f110a3de4e179acc0bea6a492b0b8b00cf - DatadogTrace: 3ba194791267efa09634234749111cac95abd3e5 - DatadogWebViewTracking: 8287d5ad06e992de5e46dd72a17e05c7513344be + DatadogCore: 0c39ba4ef3dee02071f235f2fb56af7e29f4ceb0 + DatadogCrashReporting: 604e65e524cb2fc22cda39fd9c9ea5fd3bddf24e + DatadogInternal: 442673a7fb5329299b489b91ed705aa2085380f7 + DatadogLogs: 0146a0e3140ba62fd42729593c0910d692aea5fd + DatadogRUM: f97fbd0290ecec5bc952fb03a6a1ae068649243f + DatadogSDKReactNative: 0a80aa75958d595a99be54d2838db53eda7a08af + DatadogTrace: d714e7c456e612b7f171483d08086a59aa1c4213 + DatadogWebViewTracking: fb9591c09fca82b143f3843a7164a7e782175c53 DoubleConversion: f16ae600a246532c4020132d54af21d0ddb2a385 fast_float: 06eeec4fe712a76acc9376682e4808b05ce978b6 FBLazyVector: 7605ea4810e0e10ae4815292433c09bf4324ba45 diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 45a7a2dea..f93f0ee07 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -1,34 +1,34 @@ PODS: - boost (1.84.0) - - DatadogCore (2.30.2): - - DatadogInternal (= 2.30.2) - - DatadogCrashReporting (2.30.2): - - DatadogInternal (= 2.30.2) + - DatadogCore (3.0.0): + - DatadogInternal (= 3.0.0) + - DatadogCrashReporting (3.0.0): + - DatadogInternal (= 3.0.0) - PLCrashReporter (~> 1.12.0) - - DatadogInternal (2.30.2) - - DatadogLogs (2.30.2): - - DatadogInternal (= 2.30.2) - - DatadogRUM (2.30.2): - - DatadogInternal (= 2.30.2) - - DatadogSDKReactNative (2.13.2): - - DatadogCore (= 2.30.2) - - DatadogCrashReporting (= 2.30.2) - - DatadogLogs (= 2.30.2) - - DatadogRUM (= 2.30.2) - - DatadogTrace (= 2.30.2) - - DatadogWebViewTracking (= 2.30.2) + - DatadogInternal (3.0.0) + - DatadogLogs (3.0.0): + - DatadogInternal (= 3.0.0) + - DatadogRUM (3.0.0): + - DatadogInternal (= 3.0.0) + - DatadogSDKReactNative (2.12.1): + - DatadogCore (= 3.0.0) + - DatadogCrashReporting (= 3.0.0) + - DatadogLogs (= 3.0.0) + - DatadogRUM (= 3.0.0) + - DatadogTrace (= 3.0.0) + - DatadogWebViewTracking (= 3.0.0) - React-Core - - DatadogSDKReactNative/Tests (2.13.2): - - DatadogCore (= 2.30.2) - - DatadogCrashReporting (= 2.30.2) - - DatadogLogs (= 2.30.2) - - DatadogRUM (= 2.30.2) - - DatadogTrace (= 2.30.2) - - DatadogWebViewTracking (= 2.30.2) + - DatadogSDKReactNative/Tests (2.12.1): + - DatadogCore (= 3.0.0) + - DatadogCrashReporting (= 3.0.0) + - DatadogLogs (= 3.0.0) + - DatadogRUM (= 3.0.0) + - DatadogTrace (= 3.0.0) + - DatadogWebViewTracking (= 3.0.0) - React-Core - DatadogSDKReactNativeSessionReplay (2.13.2): - DatadogSDKReactNative - - DatadogSessionReplay (= 2.30.2) + - DatadogSessionReplay (= 3.0.0) - DoubleConversion - glog - hermes-engine @@ -51,7 +51,7 @@ PODS: - Yoga - DatadogSDKReactNativeSessionReplay/Tests (2.13.2): - DatadogSDKReactNative - - DatadogSessionReplay (= 2.30.2) + - DatadogSessionReplay (= 3.0.0) - DoubleConversion - glog - hermes-engine @@ -73,25 +73,25 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - DatadogSDKReactNativeWebView (2.13.2): - - DatadogInternal (= 2.30.2) + - DatadogSDKReactNativeWebView (2.12.1): + - DatadogInternal (= 3.0.0) - DatadogSDKReactNative - - DatadogWebViewTracking (= 2.30.2) + - DatadogWebViewTracking (= 3.0.0) - React-Core - - DatadogSDKReactNativeWebView/Tests (2.13.2): - - DatadogInternal (= 2.30.2) + - DatadogSDKReactNativeWebView/Tests (2.12.1): + - DatadogInternal (= 3.0.0) - DatadogSDKReactNative - - DatadogWebViewTracking (= 2.30.2) + - DatadogWebViewTracking (= 3.0.0) - React-Core - react-native-webview - React-RCTText - - DatadogSessionReplay (2.30.2): - - DatadogInternal (= 2.30.2) - - DatadogTrace (2.30.2): - - DatadogInternal (= 2.30.2) + - DatadogSessionReplay (3.0.0): + - DatadogInternal (= 3.0.0) + - DatadogTrace (3.0.0): + - DatadogInternal (= 3.0.0) - OpenTelemetrySwiftApi (= 1.13.1) - - DatadogWebViewTracking (2.30.2): - - DatadogInternal (= 2.30.2) + - DatadogWebViewTracking (3.0.0): + - DatadogInternal (= 3.0.0) - DoubleConversion (1.1.6) - fast_float (6.1.4) - FBLazyVector (0.76.9) @@ -1988,17 +1988,17 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: boost: 1dca942403ed9342f98334bf4c3621f011aa7946 - DatadogCore: 8e50ad6cb68343f701707f7eeca16e0acb52ed8c - DatadogCrashReporting: 34763d4276d4fce286c7e8729cfcd2ac04dca43b - DatadogInternal: bd8672d506a7e67936ed5f7ca612169e49029b44 - DatadogLogs: d683aa9e0c9339f5ae679ead70bbdbe41cdc32f6 - DatadogRUM: 8b794aa458e6323ea9b1cef3f820fd3d092cbe27 - DatadogSDKReactNative: b04b5f9fd71aaa8a26affc43b1b0a2a43674a072 - DatadogSDKReactNativeSessionReplay: 102b0ecb56fb2baf3e8183e7c54631c5e31bf24a - DatadogSDKReactNativeWebView: 4343376f9a6be5e2af685444173f4b54e8b15d83 - DatadogSessionReplay: 56a91d799fe34967c5ae79a222364e37d67020f5 - DatadogTrace: 3ba194791267efa09634234749111cac95abd3e5 - DatadogWebViewTracking: 8287d5ad06e992de5e46dd72a17e05c7513344be + DatadogCore: 0c39ba4ef3dee02071f235f2fb56af7e29f4ceb0 + DatadogCrashReporting: 604e65e524cb2fc22cda39fd9c9ea5fd3bddf24e + DatadogInternal: 442673a7fb5329299b489b91ed705aa2085380f7 + DatadogLogs: 0146a0e3140ba62fd42729593c0910d692aea5fd + DatadogRUM: f97fbd0290ecec5bc952fb03a6a1ae068649243f + DatadogSDKReactNative: e430b3f4d7adb0fac07b61e2fb65ceacdaf82b3e + DatadogSDKReactNativeSessionReplay: 4b2a3d166a79581f18522795b40141c34cf3685d + DatadogSDKReactNativeWebView: 35dc2b9736e1aaa82b366bf6b8a8a959a9b088c5 + DatadogSessionReplay: 0357b911c6ab42c77c93b29761ecc20d9282e971 + DatadogTrace: d714e7c456e612b7f171483d08086a59aa1c4213 + DatadogWebViewTracking: fb9591c09fca82b143f3843a7164a7e782175c53 DoubleConversion: f16ae600a246532c4020132d54af21d0ddb2a385 fast_float: 06eeec4fe712a76acc9376682e4808b05ce978b6 FBLazyVector: 7605ea4810e0e10ae4815292433c09bf4324ba45 diff --git a/packages/core/DatadogSDKReactNative.podspec b/packages/core/DatadogSDKReactNative.podspec index ff3b91232..a2f41ddd1 100644 --- a/packages/core/DatadogSDKReactNative.podspec +++ b/packages/core/DatadogSDKReactNative.podspec @@ -19,14 +19,14 @@ Pod::Spec.new do |s| s.dependency "React-Core" # /!\ Remember to keep the versions in sync with DatadogSDKReactNativeSessionReplay.podspec - s.dependency 'DatadogCore', '2.30.2' - s.dependency 'DatadogLogs', '2.30.2' - s.dependency 'DatadogTrace', '2.30.2' - s.dependency 'DatadogRUM', '2.30.2' - s.dependency 'DatadogCrashReporting', '2.30.2' + s.dependency 'DatadogCore', '3.0.0' + s.dependency 'DatadogLogs', '3.0.0' + s.dependency 'DatadogTrace', '3.0.0' + s.dependency 'DatadogRUM', '3.0.0' + s.dependency 'DatadogCrashReporting', '3.0.0' # DatadogWebViewTracking is not available for tvOS - s.ios.dependency 'DatadogWebViewTracking', '2.30.2' + s.ios.dependency 'DatadogWebViewTracking', '3.0.0' s.test_spec 'Tests' do |test_spec| test_spec.source_files = 'ios/Tests/**/*.{swift,json}' diff --git a/packages/core/android/build.gradle b/packages/core/android/build.gradle index 0396ae323..a8d09d4d1 100644 --- a/packages/core/android/build.gradle +++ b/packages/core/android/build.gradle @@ -201,16 +201,16 @@ dependencies { // This breaks builds if the React Native target is below 0.76.0. as it relies on Gradle 8.5.0. // To avoid this, we enforce 1.0.0-beta01 on RN < 0.76.0 if (reactNativeMinorVersion < 76) { - implementation("com.datadoghq:dd-sdk-android-rum:2.26.2") { + implementation("com.datadoghq:dd-sdk-android-rum:3.0.0") { exclude group: "androidx.metrics", module: "metrics-performance" } implementation "androidx.metrics:metrics-performance:1.0.0-beta01" } else { - implementation "com.datadoghq:dd-sdk-android-rum:2.26.2" + implementation "com.datadoghq:dd-sdk-android-rum:3.0.0" } - implementation "com.datadoghq:dd-sdk-android-logs:2.26.2" - implementation "com.datadoghq:dd-sdk-android-trace:2.26.2" - implementation "com.datadoghq:dd-sdk-android-webview:2.26.2" + implementation "com.datadoghq:dd-sdk-android-logs:3.0.0" + implementation "com.datadoghq:dd-sdk-android-trace:3.0.0" + implementation "com.datadoghq:dd-sdk-android-webview:3.0.0" implementation "com.google.code.gson:gson:2.10.0" testImplementation "org.junit.platform:junit-platform-launcher:1.6.2" testImplementation "org.junit.jupiter:junit-jupiter-api:5.6.2" diff --git a/packages/react-native-session-replay/DatadogSDKReactNativeSessionReplay.podspec b/packages/react-native-session-replay/DatadogSDKReactNativeSessionReplay.podspec index 915fc4129..3a2d5ff48 100644 --- a/packages/react-native-session-replay/DatadogSDKReactNativeSessionReplay.podspec +++ b/packages/react-native-session-replay/DatadogSDKReactNativeSessionReplay.podspec @@ -23,7 +23,7 @@ Pod::Spec.new do |s| s.dependency "React-Core" # /!\ Remember to keep the version in sync with DatadogSDKReactNative.podspec - s.dependency 'DatadogSessionReplay', '2.30.2' + s.dependency 'DatadogSessionReplay', '3.0.0' s.dependency 'DatadogSDKReactNative' s.test_spec 'Tests' do |test_spec| diff --git a/packages/react-native-session-replay/android/build.gradle b/packages/react-native-session-replay/android/build.gradle index 5851ee598..6d6fe20b1 100644 --- a/packages/react-native-session-replay/android/build.gradle +++ b/packages/react-native-session-replay/android/build.gradle @@ -216,8 +216,8 @@ dependencies { api "com.facebook.react:react-android:$reactNativeVersion" } implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" - implementation "com.datadoghq:dd-sdk-android-session-replay:2.26.2" - implementation "com.datadoghq:dd-sdk-android-internal:2.26.2" + implementation "com.datadoghq:dd-sdk-android-session-replay:3.0.0" + implementation "com.datadoghq:dd-sdk-android-internal:3.0.0" implementation project(path: ':datadog_mobile-react-native') testImplementation "org.junit.platform:junit-platform-launcher:1.6.2" diff --git a/packages/react-native-webview/DatadogSDKReactNativeWebView.podspec b/packages/react-native-webview/DatadogSDKReactNativeWebView.podspec index 3d4668bb0..26e160bbc 100644 --- a/packages/react-native-webview/DatadogSDKReactNativeWebView.podspec +++ b/packages/react-native-webview/DatadogSDKReactNativeWebView.podspec @@ -23,8 +23,8 @@ Pod::Spec.new do |s| end # /!\ Remember to keep the version in sync with DatadogSDKReactNative.podspec - s.dependency 'DatadogWebViewTracking', '2.30.2' - s.dependency 'DatadogInternal', '2.30.2' + s.dependency 'DatadogWebViewTracking', '3.0.0' + s.dependency 'DatadogInternal', '3.0.0' s.dependency 'DatadogSDKReactNative' s.test_spec 'Tests' do |test_spec| diff --git a/packages/react-native-webview/android/build.gradle b/packages/react-native-webview/android/build.gradle index 973b3ec5f..098eae019 100644 --- a/packages/react-native-webview/android/build.gradle +++ b/packages/react-native-webview/android/build.gradle @@ -190,7 +190,7 @@ dependencies { implementation "com.facebook.react:react-android:$reactNativeVersion" } - implementation "com.datadoghq:dd-sdk-android-webview:2.26.2" + implementation "com.datadoghq:dd-sdk-android-webview:3.0.0" implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation project(path: ':datadog_mobile-react-native') From 6b871c0ca3fac354554aab75682e28d6a416413b Mon Sep 17 00:00:00 2001 From: Sergio Barrio Date: Fri, 12 Sep 2025 14:43:25 +0200 Subject: [PATCH 065/410] Remove setUser --- packages/codepush/__mocks__/react-native.ts | 3 - packages/core/__mocks__/react-native.ts | 3 - .../datadog/reactnative/DatadogSDKWrapper.kt | 10 -- .../com/datadog/reactnative/DatadogWrapper.kt | 17 -- .../reactnative/DdSdkImplementation.kt | 17 +- .../kotlin/com/datadog/reactnative/DdSdk.kt | 14 +- .../kotlin/com/datadog/reactnative/DdSdk.kt | 11 -- .../com/datadog/reactnative/DdSdkTest.kt | 158 ------------------ packages/core/ios/Sources/DdSdk.mm | 11 -- .../ios/Sources/DdSdkImplementation.swift | 15 +- packages/core/ios/Tests/DdSdkTests.swift | 132 --------------- packages/core/ios/Tests/MockRUMMonitor.swift | 16 ++ packages/core/jest/mock.js | 3 - packages/core/src/DdSdkReactNative.tsx | 25 +-- .../src/__tests__/DdSdkReactNative.test.tsx | 16 -- packages/core/src/logs/eventMapper.ts | 6 +- .../core/src/sdk/EventMappers/EventMapper.ts | 2 +- .../UserInfoSingleton/UserInfoSingleton.ts | 4 +- .../__tests__/UserInfoSingleton.test.ts | 6 +- .../core/src/sdk/UserInfoSingleton/types.ts | 5 +- packages/core/src/specs/NativeDdSdk.ts | 7 - packages/core/src/types.tsx | 10 +- .../__mocks__/react-native.ts | 3 - 23 files changed, 42 insertions(+), 452 deletions(-) diff --git a/packages/codepush/__mocks__/react-native.ts b/packages/codepush/__mocks__/react-native.ts index b35df4e31..046ced2f6 100644 --- a/packages/codepush/__mocks__/react-native.ts +++ b/packages/codepush/__mocks__/react-native.ts @@ -18,9 +18,6 @@ actualRN.NativeModules.DdSdk = { initialize: jest.fn().mockImplementation( () => new Promise(resolve => resolve()) ) as jest.MockedFunction, - setUser: jest.fn().mockImplementation( - () => new Promise(resolve => resolve()) - ) as jest.MockedFunction, setAttributes: jest.fn().mockImplementation( () => new Promise(resolve => resolve()) ) as jest.MockedFunction, diff --git a/packages/core/__mocks__/react-native.ts b/packages/core/__mocks__/react-native.ts index 9507ec33f..260fe68a7 100644 --- a/packages/core/__mocks__/react-native.ts +++ b/packages/core/__mocks__/react-native.ts @@ -18,9 +18,6 @@ actualRN.NativeModules.DdSdk = { initialize: jest.fn().mockImplementation( () => new Promise(resolve => resolve()) ) as jest.MockedFunction, - setUser: jest.fn().mockImplementation( - () => new Promise(resolve => resolve()) - ) as jest.MockedFunction, setUserInfo: jest.fn().mockImplementation( () => new Promise(resolve => resolve()) ) as jest.MockedFunction, diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogSDKWrapper.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogSDKWrapper.kt index da0841ac4..198061d14 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogSDKWrapper.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogSDKWrapper.kt @@ -71,16 +71,6 @@ internal class DatadogSDKWrapper : DatadogWrapper { DatadogSDKWrapperStorage.notifyOnInitializedListeners(core as InternalSdkCore) } - @Deprecated("Use setUserInfo instead; the user ID is now required.") - override fun setUser( - id: String?, - name: String?, - email: String?, - extraInfo: Map - ) { - Datadog.setUserInfo(id, name, email, extraInfo) - } - override fun setUserInfo( id: String, name: String?, diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogWrapper.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogWrapper.kt index 19b25e587..3ae3e6266 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogWrapper.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogWrapper.kt @@ -62,23 +62,6 @@ interface DatadogWrapper { consent: TrackingConsent ) - /** - * Sets the user information. - * - * @param id (nullable) a unique user identifier (relevant to your business domain) - * @param name (nullable) the user name or alias - * @param email (nullable) the user email - * @param extraInfo additional information. An extra information can be - * nested up to 8 levels deep. Keys using more than 8 levels will be sanitized by SDK. - */ - @Deprecated("Use setUserInfo instead; the user ID is now required.") - fun setUser( - id: String?, - name: String?, - email: String?, - extraInfo: Map - ) - /** * Sets the user information. * diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt index b04a2ddf3..7a5c6848c 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt @@ -78,21 +78,6 @@ class DdSdkImplementation( promise.resolve(null) } - /** - * Set the user information. - * @param user The user object (use builtin attributes: 'id', 'email', 'name', and/or any custom - * attribute). - */ - @Deprecated("Use setUserInfo instead; the user ID is now required.") - fun setUser(user: ReadableMap, promise: Promise) { - val extraInfo = user.toHashMap().toMutableMap() - val id = extraInfo.remove("id")?.toString() - val name = extraInfo.remove("name")?.toString() - val email = extraInfo.remove("email")?.toString() - datadog.setUser(id, name, email, extraInfo) - promise.resolve(null) - } - /** * Set the user information. * @param userInfo The user object (use builtin attributes: 'id', 'email', 'name', and any custom @@ -110,7 +95,7 @@ class DdSdkImplementation( if (id != null) { datadog.setUserInfo(id, name, email, extraInfo) } else { - datadog.setUser(null, name, email, extraInfo) + // TO DO - Log warning? } promise.resolve(null) diff --git a/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdSdk.kt b/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdSdk.kt index 5bc470947..cfafffffe 100644 --- a/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdSdk.kt +++ b/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdSdk.kt @@ -51,19 +51,7 @@ class DdSdk( /** * Set the user information. - * @param user The user object (use builtin attributes: 'id', 'email', 'name', and/or any custom - * attribute). - */ - @Deprecated("Use setUserInfo instead; the user ID is now required.") - @ReactMethod - override fun setUser(user: ReadableMap, promise: Promise) { - implementation.setUser(user, promise) - } - - /** - * Set the user information. - * @param user The user object (use builtin attributes: 'id', 'email', 'name', and any custom - * attribute inside 'extraInfo'). + * @param user The user object (use builtin attributes: 'id', 'email', 'name', and any custom * attribute inside 'extraInfo'). */ @ReactMethod override fun setUserInfo(user: ReadableMap, promise: Promise) { diff --git a/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt b/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt index 17acd6d20..97acb2ebf 100644 --- a/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt +++ b/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt @@ -75,17 +75,6 @@ class DdSdk( implementation.setAttributes(attributes, promise) } - /** - * Set the user information. - * @param user The user object (use builtin attributes: 'id', 'email', 'name', and/or any custom - * attribute). - */ - @Deprecated("Use setUserInfo instead; the user ID is now required.") - @ReactMethod - fun setUser(user: ReadableMap, promise: Promise) { - implementation.setUser(user, promise) - } - /** * Set the user information. * @param user The user object (use builtin attributes: 'id', 'email', 'name', and any custom * attribute inside 'extraInfo'). diff --git a/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt b/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt index a39485dae..8797bcc64 100644 --- a/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt +++ b/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt @@ -2810,164 +2810,6 @@ internal class DdSdkTest { // region misc - @Test - fun `𝕄 set native user info 𝕎 setUser()`( - @MapForgery( - key = AdvancedForgery(string = [StringForgery(StringForgeryType.NUMERICAL)]), - value = AdvancedForgery(string = [StringForgery(StringForgeryType.ASCII)]) - ) extraInfo: Map - ) { - // When - testedBridgeSdk.setUser(extraInfo.toReadableMap(), mockPromise) - - // Then - argumentCaptor> { - verify(mockDatadog) - .setUser( - isNull(), - isNull(), - isNull(), - capture() - ) - - assertThat(firstValue) - .containsAllEntriesOf(extraInfo) - .hasSize(extraInfo.size) - } - } - - @Test - fun `𝕄 set native user info 𝕎 setUser() {with id}`( - @StringForgery id: String, - @MapForgery( - key = AdvancedForgery(string = [StringForgery(StringForgeryType.NUMERICAL)]), - value = AdvancedForgery(string = [StringForgery(StringForgeryType.ASCII)]) - ) extraInfo: Map - ) { - // Given - val user = extraInfo.toMutableMap().also { - it.put("id", id) - } - - // When - testedBridgeSdk.setUser(user.toReadableMap(), mockPromise) - - // Then - argumentCaptor> { - verify(mockDatadog) - .setUser( - eq(id), - isNull(), - isNull(), - capture() - ) - - assertThat(firstValue) - .containsAllEntriesOf(extraInfo) - .hasSize(extraInfo.size) - } - } - - @Test - fun `𝕄 set native user info 𝕎 setUser() {with name}`( - @StringForgery name: String, - @MapForgery( - key = AdvancedForgery(string = [StringForgery(StringForgeryType.NUMERICAL)]), - value = AdvancedForgery(string = [StringForgery(StringForgeryType.ASCII)]) - ) extraInfo: Map - ) { - // Given - val user = extraInfo.toMutableMap().also { - it.put("name", name) - } - - // When - testedBridgeSdk.setUser(user.toReadableMap(), mockPromise) - - // Then - argumentCaptor> { - verify(mockDatadog) - .setUser( - isNull(), - eq(name), - isNull(), - capture() - ) - - assertThat(firstValue) - .containsAllEntriesOf(extraInfo) - .hasSize(extraInfo.size) - } - } - - @Test - fun `𝕄 set native user info 𝕎 setUser() {with email}`( - @StringForgery(regex = "\\w+@\\w+\\.[a-z]{3}") email: String, - @MapForgery( - key = AdvancedForgery(string = [StringForgery(StringForgeryType.NUMERICAL)]), - value = AdvancedForgery(string = [StringForgery(StringForgeryType.ASCII)]) - ) extraInfo: Map - ) { - // Given - val user = extraInfo.toMutableMap().also { - it.put("email", email) - } - - // When - testedBridgeSdk.setUser(user.toReadableMap(), mockPromise) - - // Then - argumentCaptor> { - verify(mockDatadog) - .setUser( - isNull(), - isNull(), - eq(email), - capture() - ) - - assertThat(firstValue) - .containsAllEntriesOf(extraInfo) - .hasSize(extraInfo.size) - } - } - - @Test - fun `𝕄 set native user info 𝕎 setUser() {with id, name and email}`( - @StringForgery id: String, - @StringForgery name: String, - @StringForgery(regex = "\\w+@\\w+\\.[a-z]{3}") email: String, - @MapForgery( - key = AdvancedForgery(string = [StringForgery(StringForgeryType.NUMERICAL)]), - value = AdvancedForgery(string = [StringForgery(StringForgeryType.ASCII)]) - ) extraInfo: Map - ) { - // Given - val user = extraInfo.toMutableMap().also { - it.put("id", id) - it.put("name", name) - it.put("email", email) - } - - // When - testedBridgeSdk.setUser(user.toReadableMap(), mockPromise) - - // Then - argumentCaptor> { - verify(mockDatadog) - .setUser( - eq(id), - eq(name), - eq(email), - capture() - ) - - assertThat(firstValue) - .containsAllEntriesOf(extraInfo) - .hasSize(extraInfo.size) - } - } - @Test fun `𝕄 set native user info 𝕎 setUserInfo() {with id}`( @StringForgery id: String diff --git a/packages/core/ios/Sources/DdSdk.mm b/packages/core/ios/Sources/DdSdk.mm index 590452c86..918a8db03 100644 --- a/packages/core/ios/Sources/DdSdk.mm +++ b/packages/core/ios/Sources/DdSdk.mm @@ -37,13 +37,6 @@ + (void)initFromNative { [self setAttributes:attributes resolve:resolve reject:reject]; } -RCT_REMAP_METHOD(setUser, withUser:(NSDictionary*)user - withResolver:(RCTPromiseResolveBlock)resolve - withRejecter:(RCTPromiseRejectBlock)reject) -{ - [self setUser:user resolve:resolve reject:reject]; -} - RCT_REMAP_METHOD(setUserInfo, withUserInfo:(NSDictionary*)userInfo withResolver:(RCTPromiseResolveBlock)resolve withRejecter:(RCTPromiseRejectBlock)reject) @@ -146,10 +139,6 @@ - (void)setTrackingConsent:(NSString *)trackingConsent resolve:(RCTPromiseResolv [self.ddSdkImplementation setTrackingConsentWithTrackingConsent:trackingConsent resolve:resolve reject:reject]; } -- (void)setUser:(NSDictionary *)user resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { - [self.ddSdkImplementation setUserWithUser:user resolve:resolve reject:reject]; -} - - (void)setUserInfo:(NSDictionary *)userInfo resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { [self.ddSdkImplementation setUserInfoWithUserInfo:userInfo resolve:resolve reject:reject]; } diff --git a/packages/core/ios/Sources/DdSdkImplementation.swift b/packages/core/ios/Sources/DdSdkImplementation.swift index 973b8db9f..b2e610635 100644 --- a/packages/core/ios/Sources/DdSdkImplementation.swift +++ b/packages/core/ios/Sources/DdSdkImplementation.swift @@ -87,18 +87,6 @@ public class DdSdkImplementation: NSObject { resolve(nil) } - @objc - public func setUser(user: NSDictionary, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { - var castedUser = castAttributesToSwift(user) - let id = castedUser.removeValue(forKey: "id") as? String - let name = castedUser.removeValue(forKey: "name") as? String - let email = castedUser.removeValue(forKey: "email") as? String - let extraInfo: [String: Encodable] = castedUser // everything what's left is an `extraInfo` - - Datadog.setUserInfo(id: id, name: name, email: email, extraInfo: extraInfo) - resolve(nil) - } - @objc public func setUserInfo(userInfo: NSDictionary, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { let castedUserInfo = castAttributesToSwift(userInfo) @@ -115,8 +103,9 @@ public class DdSdkImplementation: NSObject { if let validId = id { Datadog.setUserInfo(id: validId, name: name, email: email, extraInfo: extraInfo) } else { - Datadog.setUserInfo(name: name, email: email, extraInfo: extraInfo) + // TO DO - log warning message? } + resolve(nil) } diff --git a/packages/core/ios/Tests/DdSdkTests.swift b/packages/core/ios/Tests/DdSdkTests.swift index bcc3252a8..6be5e0994 100644 --- a/packages/core/ios/Tests/DdSdkTests.swift +++ b/packages/core/ios/Tests/DdSdkTests.swift @@ -535,85 +535,6 @@ class DdSdkTests: XCTestCase { XCTAssertEqual(ddConfig.trackFrustrations, false) } - func testSetUser() throws { - let bridge = DdSdkImplementation( - mainDispatchQueue: DispatchQueueMock(), - jsDispatchQueue: DispatchQueueMock(), - jsRefreshRateMonitor: JSRefreshRateMonitor(), - RUMMonitorProvider: { MockRUMMonitor() }, - RUMMonitorInternalProvider: { nil } - ) - bridge.initialize( - configuration: .mockAny(), - resolve: mockResolve, - reject: mockReject - ) - - bridge.setUser( - user: NSDictionary( - dictionary: [ - "id": "id_123", - "name": "John Doe", - "email": "john@doe.com", - "extra-info-1": 123, - "extra-info-2": "abc", - "extra-info-3": true, - ] - ), - resolve: mockResolve, - reject: mockReject - ) - - let ddContext = try XCTUnwrap(CoreRegistry.default as? DatadogCore).contextProvider.read() - let userInfo = try XCTUnwrap(ddContext.userInfo) - - XCTAssertEqual(userInfo.id, "id_123") - XCTAssertEqual(userInfo.name, "John Doe") - XCTAssertEqual(userInfo.email, "john@doe.com") - XCTAssertEqual(userInfo.extraInfo["extra-info-1"] as? Int64, 123) - XCTAssertEqual(userInfo.extraInfo["extra-info-2"] as? String, "abc") - XCTAssertEqual(userInfo.extraInfo["extra-info-3"] as? Bool, true) - } - - func testSetUserOptionalId() throws { - let bridge = DdSdkImplementation( - mainDispatchQueue: DispatchQueueMock(), - jsDispatchQueue: DispatchQueueMock(), - jsRefreshRateMonitor: JSRefreshRateMonitor(), - RUMMonitorProvider: { MockRUMMonitor() }, - RUMMonitorInternalProvider: { nil } - ) - bridge.initialize( - configuration: .mockAny(), - resolve: mockResolve, - reject: mockReject - ) - - bridge.setUser( - user: NSDictionary( - dictionary: [ - "name": "John Doe", - "email": "john@doe.com", - "extra-info-1": 123, - "extra-info-2": "abc", - "extra-info-3": true, - ] - ), - resolve: mockResolve, - reject: mockReject - ) - - let ddContext = try XCTUnwrap(CoreRegistry.default as? DatadogCore).contextProvider.read() - let userInfo = try XCTUnwrap(ddContext.userInfo) - - XCTAssertEqual(userInfo.id, nil) - XCTAssertEqual(userInfo.name, "John Doe") - XCTAssertEqual(userInfo.email, "john@doe.com") - XCTAssertEqual(userInfo.extraInfo["extra-info-1"] as? Int64, 123) - XCTAssertEqual(userInfo.extraInfo["extra-info-2"] as? String, "abc") - XCTAssertEqual(userInfo.extraInfo["extra-info-3"] as? Bool, true) - } - func testSetUserInfo() throws { let bridge = DdSdkImplementation( mainDispatchQueue: DispatchQueueMock(), @@ -668,59 +589,6 @@ class DdSdkTests: XCTestCase { } } - func testSetUserInfoOptionalId() throws { - let bridge = DdSdkImplementation( - mainDispatchQueue: DispatchQueueMock(), - jsDispatchQueue: DispatchQueueMock(), - jsRefreshRateMonitor: JSRefreshRateMonitor(), - RUMMonitorProvider: { MockRUMMonitor() }, - RUMMonitorInternalProvider: { nil } - ) - bridge.initialize( - configuration: .mockAny(), - resolve: mockResolve, - reject: mockReject - ) - - bridge.setUserInfo( - userInfo: NSDictionary( - dictionary: [ - "name": "John Doe", - "email": "john@doe.com", - "extraInfo": [ - "extra-info-1": 123, - "extra-info-2": "abc", - "extra-info-3": true, - "extra-info-4": [ - "nested-extra-info-1": 456 - ], - ], - ] - ), - resolve: mockResolve, - reject: mockReject - ) - - let ddContext = try XCTUnwrap(CoreRegistry.default as? DatadogCore).contextProvider.read() - let userInfo = try XCTUnwrap(ddContext.userInfo) - - XCTAssertEqual(userInfo.id, nil) - XCTAssertEqual(userInfo.name, "John Doe") - XCTAssertEqual(userInfo.email, "john@doe.com") - XCTAssertEqual(userInfo.extraInfo["extra-info-1"] as? Int64, 123) - XCTAssertEqual(userInfo.extraInfo["extra-info-2"] as? String, "abc") - XCTAssertEqual(userInfo.extraInfo["extra-info-3"] as? Bool, true) - - if let extraInfo4Encodable = userInfo.extraInfo["extra-info-4"] - as? DatadogSDKReactNative.AnyEncodable, - let extraInfo4Dict = extraInfo4Encodable.value as? [String: Int] - { - XCTAssertEqual(extraInfo4Dict, ["nested-extra-info-1": 456]) - } else { - XCTFail("extra-info-4 is not of expected type or value") - } - } - func testAddUserExtraInfo() throws { let bridge = DdSdkImplementation( mainDispatchQueue: DispatchQueueMock(), diff --git a/packages/core/ios/Tests/MockRUMMonitor.swift b/packages/core/ios/Tests/MockRUMMonitor.swift index fe17e7748..f0fa03364 100644 --- a/packages/core/ios/Tests/MockRUMMonitor.swift +++ b/packages/core/ios/Tests/MockRUMMonitor.swift @@ -10,6 +10,22 @@ @testable import DatadogSDKReactNative internal class MockRUMMonitor: RUMMonitorProtocol { + func addViewAttribute(forKey key: DatadogInternal.AttributeKey, value: any DatadogInternal.AttributeValue) { + // not implemented + } + + func addViewAttributes(_ attributes: [DatadogInternal.AttributeKey : any DatadogInternal.AttributeValue]) { + // not implemented + } + + func removeViewAttribute(forKey key: DatadogInternal.AttributeKey) { + // not implemented + } + + func removeViewAttributes(forKeys keys: [DatadogInternal.AttributeKey]) { + // not implemented + } + func currentSessionID(completion: @escaping (String?) -> Void) { // not implemented } diff --git a/packages/core/jest/mock.js b/packages/core/jest/mock.js index 9c08f5335..a8161295a 100644 --- a/packages/core/jest/mock.js +++ b/packages/core/jest/mock.js @@ -27,9 +27,6 @@ module.exports = { .fn() .mockImplementation(() => new Promise(resolve => resolve())), isInitialized: jest.fn().mockImplementation(() => true), - setUser: jest - .fn() - .mockImplementation(() => new Promise(resolve => resolve())), setUserInfo: jest .fn() .mockImplementation(() => new Promise(resolve => resolve())), diff --git a/packages/core/src/DdSdkReactNative.tsx b/packages/core/src/DdSdkReactNative.tsx index aa7e2ec36..668ae09f3 100644 --- a/packages/core/src/DdSdkReactNative.tsx +++ b/packages/core/src/DdSdkReactNative.tsx @@ -38,7 +38,6 @@ import { DdSdk } from './sdk/DdSdk'; import { FileBasedConfiguration } from './sdk/FileBasedConfiguration/FileBasedConfiguration'; import { GlobalState } from './sdk/GlobalState/GlobalState'; import { UserInfoSingleton } from './sdk/UserInfoSingleton/UserInfoSingleton'; -import type { UserInfo } from './sdk/UserInfoSingleton/types'; import { DdSdkConfiguration } from './types'; import { adaptLongTaskThreshold } from './utils/longTasksUtils'; import { version as sdkVersion } from './version'; @@ -192,22 +191,6 @@ export class DdSdkReactNative { AttributesSingleton.getInstance().setAttributes(attributes); }; - /** - * Set the user information. - * @deprecated UserInfo id property is now mandatory (please user setUserInfo instead) - * @param user: The user object (use builtin attributes: 'id', 'email', 'name', and/or any custom attribute). - * @returns a Promise. - */ - // eslint-disable-next-line @typescript-eslint/ban-types - static setUser = async (user: UserInfo): Promise => { - InternalLog.log( - `Setting user ${JSON.stringify(user)}`, - SdkVerbosity.DEBUG - ); - await DdSdk.setUser(user); - UserInfoSingleton.getInstance().setUserInfo(user); - }; - /** * Sets the user information. * @param id: A mandatory unique user identifier (relevant to your business domain). @@ -245,6 +228,14 @@ export class DdSdkReactNative { ); const userInfo = UserInfoSingleton.getInstance().getUserInfo(); + if (!userInfo) { + InternalLog.log( + 'Skipped adding User Extra Info: User Info is currently undefined. A user ID must be set before adding extra info. Please call setUserInfo() first.', + SdkVerbosity.WARN + ); + + return; + } const updatedUserInfo = { ...userInfo, extraInfo: { diff --git a/packages/core/src/__tests__/DdSdkReactNative.test.tsx b/packages/core/src/__tests__/DdSdkReactNative.test.tsx index 18bf060ce..f9405aa51 100644 --- a/packages/core/src/__tests__/DdSdkReactNative.test.tsx +++ b/packages/core/src/__tests__/DdSdkReactNative.test.tsx @@ -63,7 +63,6 @@ beforeEach(async () => { DdSdkReactNative['wasAutoInstrumented'] = false; NativeModules.DdSdk.initialize.mockClear(); NativeModules.DdSdk.setAttributes.mockClear(); - NativeModules.DdSdk.setUser.mockClear(); NativeModules.DdSdk.setTrackingConsent.mockClear(); NativeModules.DdSdk.onRUMSessionStarted.mockClear(); @@ -1064,21 +1063,6 @@ describe('DdSdkReactNative', () => { }); }); - describe('setUser', () => { - it('calls SDK method when setUser, and sets the user in UserProvider', async () => { - // GIVEN - const user = { id: 'id', foo: 'bar' }; - - // WHEN - await DdSdkReactNative.setUser(user); - - // THEN - expect(DdSdk.setUser).toHaveBeenCalledTimes(1); - expect(DdSdk.setUser).toHaveBeenCalledWith(user); - expect(UserInfoSingleton.getInstance().getUserInfo()).toEqual(user); - }); - }); - describe('setUserInfo', () => { it('calls SDK method when setUserInfo, and sets the user in UserProvider', async () => { // GIVEN diff --git a/packages/core/src/logs/eventMapper.ts b/packages/core/src/logs/eventMapper.ts index eb7b5f22c..939e882a5 100644 --- a/packages/core/src/logs/eventMapper.ts +++ b/packages/core/src/logs/eventMapper.ts @@ -31,13 +31,15 @@ export const formatRawLogToNativeEvent = ( export const formatRawLogToLogEvent = ( rawLog: RawLog | RawLogWithError, additionalInformation: { - userInfo: UserInfo; + userInfo?: UserInfo; attributes: Attributes; } ): LogEvent => { + const userInfo = additionalInformation?.userInfo; + return { ...rawLog, - userInfo: additionalInformation.userInfo, + ...(userInfo !== undefined ? { userInfo } : {}), attributes: additionalInformation.attributes }; }; diff --git a/packages/core/src/sdk/EventMappers/EventMapper.ts b/packages/core/src/sdk/EventMappers/EventMapper.ts index beafcf420..9ca252d72 100644 --- a/packages/core/src/sdk/EventMappers/EventMapper.ts +++ b/packages/core/src/sdk/EventMappers/EventMapper.ts @@ -15,7 +15,7 @@ import type { UserInfo } from '../UserInfoSingleton/types'; import { deepClone } from './utils/deepClone'; export type AdditionalEventDataForMapper = { - userInfo: UserInfo; + userInfo?: UserInfo; attributes: Attributes; }; diff --git a/packages/core/src/sdk/UserInfoSingleton/UserInfoSingleton.ts b/packages/core/src/sdk/UserInfoSingleton/UserInfoSingleton.ts index c3862aabc..26392d794 100644 --- a/packages/core/src/sdk/UserInfoSingleton/UserInfoSingleton.ts +++ b/packages/core/src/sdk/UserInfoSingleton/UserInfoSingleton.ts @@ -7,13 +7,13 @@ import type { UserInfo } from './types'; class UserInfoProvider { - private userInfo: UserInfo = {}; + private userInfo: UserInfo | undefined = undefined; setUserInfo = (userInfo: UserInfo) => { this.userInfo = userInfo; }; - getUserInfo = (): UserInfo => { + getUserInfo = (): UserInfo | undefined => { return this.userInfo; }; } diff --git a/packages/core/src/sdk/UserInfoSingleton/__tests__/UserInfoSingleton.test.ts b/packages/core/src/sdk/UserInfoSingleton/__tests__/UserInfoSingleton.test.ts index 78a722c01..1f7ae84e7 100644 --- a/packages/core/src/sdk/UserInfoSingleton/__tests__/UserInfoSingleton.test.ts +++ b/packages/core/src/sdk/UserInfoSingleton/__tests__/UserInfoSingleton.test.ts @@ -9,6 +9,7 @@ import { UserInfoSingleton } from '../UserInfoSingleton'; describe('UserInfoSingleton', () => { it('sets, returns and resets the user info', () => { UserInfoSingleton.getInstance().setUserInfo({ + id: 'test', email: 'user@mail.com', extraInfo: { loggedIn: true @@ -16,6 +17,7 @@ describe('UserInfoSingleton', () => { }); expect(UserInfoSingleton.getInstance().getUserInfo()).toEqual({ + id: 'test', email: 'user@mail.com', extraInfo: { loggedIn: true @@ -24,6 +26,8 @@ describe('UserInfoSingleton', () => { UserInfoSingleton.reset(); - expect(UserInfoSingleton.getInstance().getUserInfo()).toEqual({}); + expect(UserInfoSingleton.getInstance().getUserInfo()).toEqual( + undefined + ); }); }); diff --git a/packages/core/src/sdk/UserInfoSingleton/types.ts b/packages/core/src/sdk/UserInfoSingleton/types.ts index 97a03ae7f..dd14eb150 100644 --- a/packages/core/src/sdk/UserInfoSingleton/types.ts +++ b/packages/core/src/sdk/UserInfoSingleton/types.ts @@ -5,11 +5,8 @@ */ export type UserInfo = { - readonly id?: string /** @deprecated To be made mandatory when removing DdSdkReactnative.setUser */; + readonly id: string; readonly name?: string; readonly email?: string; readonly extraInfo?: Record; - readonly [ - key: string - ]: unknown /** @deprecated To be removed alongside DdSdkReactnative.setUser */; }; diff --git a/packages/core/src/specs/NativeDdSdk.ts b/packages/core/src/specs/NativeDdSdk.ts index 6f1ce82a5..bbf2572ee 100644 --- a/packages/core/src/specs/NativeDdSdk.ts +++ b/packages/core/src/specs/NativeDdSdk.ts @@ -31,13 +31,6 @@ export interface Spec extends TurboModule { */ setAttributes(attributes: Object): Promise; - /** - * Set the user information. - * @deprecated: Use setUserInfo instead - * @param user: The user object (use builtin attributes: 'id', 'email', 'name', and/or any custom attribute). - */ - setUser(user: Object): Promise; - /** * Set the user information. * @param user: The user object (use builtin attributes: 'id', 'email', 'name', and any custom attribute under extraInfo). diff --git a/packages/core/src/types.tsx b/packages/core/src/types.tsx index e1c5096fb..fe1a5895c 100644 --- a/packages/core/src/types.tsx +++ b/packages/core/src/types.tsx @@ -5,7 +5,6 @@ */ import type { BatchProcessingLevel } from './DdSdkReactNativeConfiguration'; -import type { UserInfo as UserInfoSingleton } from './sdk/UserInfoSingleton/types'; declare global { // eslint-disable-next-line no-var, vars-on-top @@ -90,13 +89,6 @@ export type DdSdkType = { */ setAttributes(attributes: object): Promise; - /** - * Sets the user information. - * @deprecated UserInfo id property is now mandatory (please user setUserInfo instead) - * @param user: The user object (use builtin attributes: 'id', 'email', 'name', and/or any custom attribute). - */ - setUser(user: object): Promise; - /** * Sets the user information. * @param id: A unique user identifier (relevant to your business domain) @@ -173,7 +165,7 @@ export type LogEvent = { readonly source?: ErrorSource; // readonly date: number; // TODO: RUMM-2446 & RUMM-2447 readonly status: LogStatus; - readonly userInfo: UserInfoSingleton; + readonly userInfo?: UserInfo; readonly attributes?: object; }; diff --git a/packages/react-native-apollo-client/__mocks__/react-native.ts b/packages/react-native-apollo-client/__mocks__/react-native.ts index b35df4e31..046ced2f6 100644 --- a/packages/react-native-apollo-client/__mocks__/react-native.ts +++ b/packages/react-native-apollo-client/__mocks__/react-native.ts @@ -18,9 +18,6 @@ actualRN.NativeModules.DdSdk = { initialize: jest.fn().mockImplementation( () => new Promise(resolve => resolve()) ) as jest.MockedFunction, - setUser: jest.fn().mockImplementation( - () => new Promise(resolve => resolve()) - ) as jest.MockedFunction, setAttributes: jest.fn().mockImplementation( () => new Promise(resolve => resolve()) ) as jest.MockedFunction, From 75bd308b581d4ba2217625401e414e4209cfdc54 Mon Sep 17 00:00:00 2001 From: Sergio Barrio Date: Fri, 12 Sep 2025 17:09:36 +0200 Subject: [PATCH 066/410] Update Tracer imports for Android to remove opentracing dependencies --- .../reactnative/DdTraceImplementation.kt | 39 ++++++++++--------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdTraceImplementation.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdTraceImplementation.kt index 901ec3f6a..3ed77eb75 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdTraceImplementation.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdTraceImplementation.kt @@ -6,30 +6,28 @@ package com.datadog.reactnative -import com.datadog.android.trace.AndroidTracer -import com.datadog.android.trace.Trace -import com.datadog.android.trace.TraceConfiguration +import com.datadog.android.Datadog +import com.datadog.android.trace.DatadogTracing import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReadableMap -import io.opentracing.Scope -import io.opentracing.Span -import io.opentracing.Tracer -import io.opentracing.util.GlobalTracer +import com.datadog.android.trace.api.scope.DatadogScope +import com.datadog.android.trace.api.span.DatadogSpan +import com.datadog.android.trace.api.tracer.DatadogTracer +import com.datadog.android.trace.GlobalDatadogTracer import java.util.concurrent.TimeUnit /** * The entry point to use Datadog's Trace feature. */ class DdTraceImplementation( - private val tracerProvider: () -> Tracer = { - val tracer = AndroidTracer.Builder().build() - GlobalTracer.registerIfAbsent(tracer) - - GlobalTracer.get() + private val tracerProvider: () -> DatadogTracer = { + val tracer = DatadogTracing.newTracerBuilder(Datadog.getInstance()).build() + GlobalDatadogTracer.registerIfAbsent(tracer) + GlobalDatadogTracer.get() } ) { - private val spanMap: MutableMap = mutableMapOf() - private val scopeMap: MutableMap = mutableMapOf() + private val spanMap: MutableMap = mutableMapOf() + private val scopeMap: MutableMap = mutableMapOf() // lazy here is on purpose. The thing is that this class will be instantiated even // before Sdk.initialize is called, but Tracer can be created only after SDK is initialized. @@ -47,15 +45,18 @@ class DdTraceImplementation( .start() // This is required for traces to be able to be bundled with logs. - val scope = tracer.scopeManager().activate(span) - + val scope = tracer.activateSpan(span) val spanContext = span.context() span.setTags(context.toHashMap()) span.setTags(GlobalState.globalAttributes) - val spanId = spanContext.toSpanId() + val spanId = spanContext.spanId.toString() + spanMap[spanId] = span - scopeMap[spanId] = scope + if (scope != null) { + scopeMap[spanId] = scope + } + promise.resolve(spanId) } @@ -82,7 +83,7 @@ class DdTraceImplementation( promise.resolve(null) } - private fun Span.setTags(tags: Map) { + private fun DatadogSpan.setTags(tags: Map) { for ((key, value) in tags) { when (value) { is Boolean -> setTag(key, value) From 07bad39940eb9e2ce3ddda7ee5bc52e1be6bd849 Mon Sep 17 00:00:00 2001 From: Sergio Barrio Date: Thu, 18 Sep 2025 10:13:53 +0200 Subject: [PATCH 067/410] Fix android tests --- .../com/datadog/reactnative/DdTraceTest.kt | 84 ++++++++++--------- .../com/datadog/tools/unit/MockRumMonitor.kt | 10 +-- 2 files changed, 47 insertions(+), 47 deletions(-) diff --git a/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdTraceTest.kt b/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdTraceTest.kt index 16d459a57..8c22f88e1 100644 --- a/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdTraceTest.kt +++ b/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdTraceTest.kt @@ -6,6 +6,12 @@ package com.datadog.reactnative +import com.datadog.android.trace.api.scope.DatadogScope +import com.datadog.android.trace.api.span.DatadogSpan +import com.datadog.android.trace.api.span.DatadogSpanBuilder +import com.datadog.android.trace.api.span.DatadogSpanContext +import com.datadog.android.trace.api.trace.DatadogTraceId +import com.datadog.android.trace.api.tracer.DatadogTracer import com.datadog.tools.unit.toReadableMap import com.facebook.react.bridge.Promise import fr.xgouchet.elmyr.annotation.AdvancedForgery @@ -15,11 +21,6 @@ import fr.xgouchet.elmyr.annotation.MapForgery import fr.xgouchet.elmyr.annotation.StringForgery import fr.xgouchet.elmyr.annotation.StringForgeryType import fr.xgouchet.elmyr.junit5.ForgeExtension -import io.opentracing.Scope -import io.opentracing.ScopeManager -import io.opentracing.Span -import io.opentracing.SpanContext -import io.opentracing.Tracer import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Assumptions.assumeTrue @@ -51,22 +52,19 @@ internal class DdTraceTest { lateinit var testedTrace: DdTraceImplementation @Mock - lateinit var mockTracer: Tracer + lateinit var mockTracer: DatadogTracer @Mock - lateinit var mockSpanBuilder: Tracer.SpanBuilder + lateinit var mockSpanBuilder: DatadogSpanBuilder @Mock - lateinit var mockSpanContext: SpanContext + lateinit var mockSpanContext: DatadogSpanContext @Mock - lateinit var mockScopeManager: ScopeManager + lateinit var mockSpan: DatadogSpan @Mock - lateinit var mockSpan: Span - - @Mock - lateinit var mockScope: Scope + lateinit var mockScope: DatadogScope @StringForgery lateinit var fakeOperation: String @@ -74,11 +72,11 @@ internal class DdTraceTest { @DoubleForgery(1000000000000.0, 2000000000000.0) var fakeTimestamp: Double = 0.0 - @StringForgery(type = StringForgeryType.HEXADECIMAL) - lateinit var fakeSpanId: String + @LongForgery(100L, 2000L) + var fakeSpanId: Long = 0 - @StringForgery(type = StringForgeryType.HEXADECIMAL) - lateinit var fakeTraceId: String + @Mock + lateinit var fakeTraceId: DatadogTraceId @MapForgery( key = AdvancedForgery(string = [StringForgery()]), @@ -102,7 +100,6 @@ internal class DdTraceTest { @BeforeEach fun `set up`() { whenever(mockTracer.buildSpan(fakeOperation)) doReturn mockSpanBuilder - whenever(mockTracer.scopeManager()) doReturn mockScopeManager whenever( mockSpanBuilder.withStartTimestamp( fakeTimestamp.toLong() * 1000 @@ -110,9 +107,9 @@ internal class DdTraceTest { ) doReturn mockSpanBuilder whenever(mockSpanBuilder.start()) doReturn mockSpan whenever(mockSpan.context()) doReturn mockSpanContext - whenever(mockSpanContext.toSpanId()) doReturn fakeSpanId - whenever(mockSpanContext.toTraceId()) doReturn fakeTraceId - whenever(mockScopeManager.activate(mockSpan)) doReturn mockScope + whenever(mockSpanContext.spanId) doReturn fakeSpanId + whenever(mockSpanContext.traceId) doReturn fakeTraceId + whenever(mockTracer.activateSpan(mockSpan)) doReturn mockScope testedTrace = DdTraceImplementation(tracerProvider = { mockTracer }) } @@ -133,7 +130,7 @@ internal class DdTraceTest { ) // Then - assertThat(lastResolvedValue).isEqualTo(fakeSpanId) + assertThat(lastResolvedValue.toString()).isEqualTo(fakeSpanId.toString()) } @Test @@ -154,18 +151,20 @@ internal class DdTraceTest { fakeTimestamp, mockPromise ) - val id = lastResolvedValue - testedTrace.finishSpan(id as String, fakeContext.toReadableMap(), endTimestamp, mockPromise) + val id = lastResolvedValue.toString() + testedTrace.finishSpan(id, fakeContext.toReadableMap(), endTimestamp, mockPromise) // Then - assertThat(id).isEqualTo(fakeSpanId) + assertThat(id).isEqualTo(fakeSpanId.toString()) verify(mockSpan).finish(endTimestamp.toLong() * 1000) } @Test fun `M do nothing W startSpan() + finishSpan() with unknown id`( @LongForgery(100L, 2000L) duration: Long, - @StringForgery(type = StringForgeryType.HEXADECIMAL) otherSpanId: String + @StringForgery(type = StringForgeryType.HEXADECIMAL) + @LongForgery(100L, 2000L) + otherSpanId: Long ) { // Given assumeTrue(otherSpanId != fakeSpanId) @@ -178,11 +177,16 @@ internal class DdTraceTest { fakeTimestamp, mockPromise ) - val id = lastResolvedValue - testedTrace.finishSpan(otherSpanId, fakeContext.toReadableMap(), endTimestamp, mockPromise) + val id = lastResolvedValue.toString() + testedTrace.finishSpan( + otherSpanId.toString(), + fakeContext.toReadableMap(), + endTimestamp, + mockPromise + ) // Then - assertThat(id).isEqualTo(fakeSpanId) + assertThat(id).isEqualTo(fakeSpanId.toString()) verify(mockSpan, never()).finish(any()) } @@ -200,7 +204,7 @@ internal class DdTraceTest { fakeTimestamp, mockPromise ) - val id = lastResolvedValue + val id = lastResolvedValue.toString() testedTrace.finishSpan( id as String, emptyMap().toReadableMap(), @@ -209,7 +213,7 @@ internal class DdTraceTest { ) // Then - assertThat(id).isEqualTo(fakeSpanId) + assertThat(id).isEqualTo(fakeSpanId.toString()) verify(mockSpan).context() verify(mockSpan).finish(endTimestamp.toLong() * 1000) fakeContext.forEach { @@ -232,11 +236,11 @@ internal class DdTraceTest { fakeTimestamp, mockPromise ) - val id = lastResolvedValue - testedTrace.finishSpan(id as String, fakeContext.toReadableMap(), endTimestamp, mockPromise) + val id = lastResolvedValue.toString() + testedTrace.finishSpan(id, fakeContext.toReadableMap(), endTimestamp, mockPromise) // Then - assertThat(id).isEqualTo(fakeSpanId) + assertThat(id).isEqualTo(fakeSpanId.toString()) verify(mockSpan).context() verify(mockSpan).finish(endTimestamp.toLong() * 1000) fakeContext.forEach { @@ -262,16 +266,16 @@ internal class DdTraceTest { fakeTimestamp, mockPromise ) - val id = lastResolvedValue + val id = lastResolvedValue.toString() testedTrace.finishSpan( - id as String, + id, emptyMap().toReadableMap(), endTimestamp, mockPromise ) // Then - assertThat(id).isEqualTo(fakeSpanId) + assertThat(id).isEqualTo(fakeSpanId.toString()) verify(mockSpan).context() verify(mockSpan).finish(endTimestamp.toLong() * 1000) fakeContext.forEach { @@ -298,14 +302,14 @@ internal class DdTraceTest { fakeTimestamp, mockPromise ) - val id = lastResolvedValue + val id = lastResolvedValue.toString() fakeGlobalState.forEach { (k, v) -> GlobalState.addAttribute(k, v) } - testedTrace.finishSpan(id as String, fakeContext.toReadableMap(), endTimestamp, mockPromise) + testedTrace.finishSpan(id, fakeContext.toReadableMap(), endTimestamp, mockPromise) // Then - assertThat(id).isEqualTo(fakeSpanId) + assertThat(id).isEqualTo(fakeSpanId.toString()) verify(mockSpan).context() verify(mockSpan).finish(endTimestamp.toLong() * 1000) expectedAttributes.forEach { diff --git a/packages/core/android/src/test/kotlin/com/datadog/tools/unit/MockRumMonitor.kt b/packages/core/android/src/test/kotlin/com/datadog/tools/unit/MockRumMonitor.kt index a2e79d630..7c5585edd 100644 --- a/packages/core/android/src/test/kotlin/com/datadog/tools/unit/MockRumMonitor.kt +++ b/packages/core/android/src/test/kotlin/com/datadog/tools/unit/MockRumMonitor.kt @@ -29,6 +29,8 @@ class MockRumMonitor : RumMonitor { override fun addAttribute(key: String, value: Any?) {} + override fun addViewAttributes(attributes: Map) {} + override fun addError( message: String, source: RumErrorSource, @@ -61,6 +63,7 @@ class MockRumMonitor : RumMonitor { override fun getCurrentSessionId(callback: (String?) -> Unit) {} override fun removeAttribute(key: String) {} + override fun removeViewAttributes(attributes: Collection) {} override fun startAction( type: RumActionType, @@ -75,13 +78,6 @@ class MockRumMonitor : RumMonitor { attributes: Map ) {} - override fun startResource( - key: String, - method: String, - url: String, - attributes: Map - ) {} - override fun startView( key: Any, name: String, From f3a7954c64e23ac6bd6fb97736acf316bc852c52 Mon Sep 17 00:00:00 2001 From: Sergio Barrio Date: Thu, 18 Sep 2025 10:50:27 +0200 Subject: [PATCH 068/410] Fix iOS tests --- packages/core/ios/Tests/DdSdkTests.swift | 2 +- packages/core/ios/Tests/RUMMocks.swift | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/core/ios/Tests/DdSdkTests.swift b/packages/core/ios/Tests/DdSdkTests.swift index 6be5e0994..555ce4549 100644 --- a/packages/core/ios/Tests/DdSdkTests.swift +++ b/packages/core/ios/Tests/DdSdkTests.swift @@ -738,7 +738,7 @@ class DdSdkTests: XCTestCase { XCTAssertEqual(actualFirstPartyHosts, expectedFirstPartyHosts) XCTAssertEqual(actualTracingSamplingRate, 66) - XCTAssertEqual(actualTraceContextInjection, .all) + XCTAssertEqual(actualTraceContextInjection, .sampled) } func testBuildTelemetrySampleRate() { diff --git a/packages/core/ios/Tests/RUMMocks.swift b/packages/core/ios/Tests/RUMMocks.swift index 6a6fd94e7..01d0f9d0d 100644 --- a/packages/core/ios/Tests/RUMMocks.swift +++ b/packages/core/ios/Tests/RUMMocks.swift @@ -213,14 +213,14 @@ extension RUMActionID: RandomMockable { } } -extension RUMDevice.RUMDeviceType: RandomMockable { - static func mockRandom() -> RUMDevice.RUMDeviceType { +extension Device.DeviceType: RandomMockable { + static func mockRandom() -> Device.DeviceType { return [.mobile, .desktop, .tablet, .tv, .gamingConsole, .bot, .other].randomElement()! } } -extension RUMDevice: RandomMockable { - static func mockRandom() -> RUMDevice { +extension Device: RandomMockable { + static func mockRandom() -> Device { return .init( architecture: .mockRandom(), brand: .mockRandom(), @@ -231,8 +231,8 @@ extension RUMDevice: RandomMockable { } } -extension RUMOperatingSystem: RandomMockable { - static func mockRandom() -> RUMOperatingSystem { +extension OperatingSystem: RandomMockable { + static func mockRandom() -> OperatingSystem { return .init( build: .mockRandom(length: 5), name: .mockRandom(length: 5), From a19a7316fa5953718696b8f9fc92872a22144e08 Mon Sep 17 00:00:00 2001 From: Sergio Barrio Date: Fri, 19 Sep 2025 11:45:00 +0200 Subject: [PATCH 069/410] Bump Native SDKs to 3.1.0 --- benchmarks/android/app/build.gradle | 2 +- benchmarks/ios/Podfile.lock | 70 +++++++-------- example-new-architecture/ios/Podfile.lock | 66 +++++++------- example/ios/Podfile.lock | 88 +++++++++---------- packages/core/DatadogSDKReactNative.podspec | 12 +-- packages/core/android/build.gradle | 10 +-- .../com/datadog/tools/unit/MockRumMonitor.kt | 23 +++++ packages/core/ios/Tests/DdLogsTests.swift | 2 + ...DatadogSDKReactNativeSessionReplay.podspec | 2 +- .../android/build.gradle | 4 +- .../DatadogSDKReactNativeWebView.podspec | 4 +- .../react-native-webview/android/build.gradle | 2 +- 12 files changed, 155 insertions(+), 130 deletions(-) diff --git a/benchmarks/android/app/build.gradle b/benchmarks/android/app/build.gradle index 7dc4b3707..04c240bd0 100644 --- a/benchmarks/android/app/build.gradle +++ b/benchmarks/android/app/build.gradle @@ -129,5 +129,5 @@ dependencies { // Benchmark tools from dd-sdk-android are used for vitals recording // Remember to bump thid alongside the main dd-sdk-android dependencies - implementation("com.datadoghq:dd-sdk-android-benchmark-internal:3.0.0") + implementation("com.datadoghq:dd-sdk-android-benchmark-internal:3.1.0") } diff --git a/benchmarks/ios/Podfile.lock b/benchmarks/ios/Podfile.lock index 90043a48d..1be186d5f 100644 --- a/benchmarks/ios/Podfile.lock +++ b/benchmarks/ios/Podfile.lock @@ -1,22 +1,22 @@ PODS: - boost (1.84.0) - - DatadogCore (3.0.0): - - DatadogInternal (= 3.0.0) - - DatadogCrashReporting (3.0.0): - - DatadogInternal (= 3.0.0) + - DatadogCore (3.1.0): + - DatadogInternal (= 3.1.0) + - DatadogCrashReporting (3.1.0): + - DatadogInternal (= 3.1.0) - PLCrashReporter (~> 1.12.0) - - DatadogInternal (3.0.0) - - DatadogLogs (3.0.0): - - DatadogInternal (= 3.0.0) - - DatadogRUM (3.0.0): - - DatadogInternal (= 3.0.0) + - DatadogInternal (3.1.0) + - DatadogLogs (3.1.0): + - DatadogInternal (= 3.1.0) + - DatadogRUM (3.1.0): + - DatadogInternal (= 3.1.0) - DatadogSDKReactNative (2.12.1): - - DatadogCore (= 3.0.0) - - DatadogCrashReporting (= 3.0.0) - - DatadogLogs (= 3.0.0) - - DatadogRUM (= 3.0.0) - - DatadogTrace (= 3.0.0) - - DatadogWebViewTracking (= 3.0.0) + - DatadogCore (= 3.1.0) + - DatadogCrashReporting (= 3.1.0) + - DatadogLogs (= 3.1.0) + - DatadogRUM (= 3.1.0) + - DatadogTrace (= 3.1.0) + - DatadogWebViewTracking (= 3.1.0) - DoubleConversion - glog - hermes-engine @@ -39,7 +39,7 @@ PODS: - Yoga - DatadogSDKReactNativeSessionReplay (2.13.2): - DatadogSDKReactNative - - DatadogSessionReplay (= 3.0.0) + - DatadogSessionReplay (= 3.1.0) - DoubleConversion - glog - hermes-engine @@ -61,9 +61,9 @@ PODS: - ReactCommon/turbomodule/core - Yoga - DatadogSDKReactNativeWebView (2.12.1): - - DatadogInternal (= 3.0.0) + - DatadogInternal (= 3.1.0) - DatadogSDKReactNative - - DatadogWebViewTracking (= 3.0.0) + - DatadogWebViewTracking (= 3.1.0) - DoubleConversion - glog - hermes-engine @@ -84,13 +84,13 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - DatadogSessionReplay (3.0.0): - - DatadogInternal (= 3.0.0) - - DatadogTrace (3.0.0): - - DatadogInternal (= 3.0.0) + - DatadogSessionReplay (3.1.0): + - DatadogInternal (= 3.1.0) + - DatadogTrace (3.1.0): + - DatadogInternal (= 3.1.0) - OpenTelemetrySwiftApi (= 1.13.1) - - DatadogWebViewTracking (3.0.0): - - DatadogInternal (= 3.0.0) + - DatadogWebViewTracking (3.1.0): + - DatadogInternal (= 3.1.0) - DoubleConversion (1.1.6) - fast_float (6.1.4) - FBLazyVector (0.78.2) @@ -2070,17 +2070,17 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: boost: 7e761d76ca2ce687f7cc98e698152abd03a18f90 - DatadogCore: 0c39ba4ef3dee02071f235f2fb56af7e29f4ceb0 - DatadogCrashReporting: 604e65e524cb2fc22cda39fd9c9ea5fd3bddf24e - DatadogInternal: 442673a7fb5329299b489b91ed705aa2085380f7 - DatadogLogs: 0146a0e3140ba62fd42729593c0910d692aea5fd - DatadogRUM: f97fbd0290ecec5bc952fb03a6a1ae068649243f - DatadogSDKReactNative: 241bf982c16ceff03d94a58e6d005e55c47b99c6 - DatadogSDKReactNativeSessionReplay: bba36092686e3183e97c1a0c7f4ca8142582ae28 - DatadogSDKReactNativeWebView: 6da060df20e235abac533e582d9fc2b3a5070840 - DatadogSessionReplay: 0357b911c6ab42c77c93b29761ecc20d9282e971 - DatadogTrace: d714e7c456e612b7f171483d08086a59aa1c4213 - DatadogWebViewTracking: fb9591c09fca82b143f3843a7164a7e782175c53 + DatadogCore: d2f51c7fb4308cf3c25e55e2e7242e5d558ee71d + DatadogCrashReporting: f636f1d1c534572c0b0abcdc59df244c884d825d + DatadogInternal: 7837b2ce3d525d429682532eeda697b181299fdc + DatadogLogs: 250894b5a99da5b924a019049c0d0326823cdbd6 + DatadogRUM: 0d2a60e1abb8aacfb8827ef84f6d5deb4d5026c8 + DatadogSDKReactNative: 8e0f39de38621d4d7ed961a74d8a216fd3a38321 + DatadogSDKReactNativeSessionReplay: f9288c8e981dcc65d1f727b01421ee9a7601e75f + DatadogSDKReactNativeWebView: 993527f6c5d38e0fcc4804a6a60c334dd199dc5b + DatadogSessionReplay: 6bc71888e2b41dd0de3325f06f0c0b3cee0e6df4 + DatadogTrace: f59e933074cd285ad7e9f5af991f8fe04b095991 + DatadogWebViewTracking: 9bc92b4147aeed47eb1911451f651094aa6dd6c1 DoubleConversion: cb417026b2400c8f53ae97020b2be961b59470cb fast_float: 06eeec4fe712a76acc9376682e4808b05ce978b6 FBLazyVector: e32d34492c519a2194ec9d7f5e7a79d11b73f91c diff --git a/example-new-architecture/ios/Podfile.lock b/example-new-architecture/ios/Podfile.lock index cc9913ce3..0378f5d72 100644 --- a/example-new-architecture/ios/Podfile.lock +++ b/example-new-architecture/ios/Podfile.lock @@ -1,22 +1,22 @@ PODS: - boost (1.84.0) - - DatadogCore (3.0.0): - - DatadogInternal (= 3.0.0) - - DatadogCrashReporting (3.0.0): - - DatadogInternal (= 3.0.0) + - DatadogCore (3.1.0): + - DatadogInternal (= 3.1.0) + - DatadogCrashReporting (3.1.0): + - DatadogInternal (= 3.1.0) - PLCrashReporter (~> 1.12.0) - - DatadogInternal (3.0.0) - - DatadogLogs (3.0.0): - - DatadogInternal (= 3.0.0) - - DatadogRUM (3.0.0): - - DatadogInternal (= 3.0.0) + - DatadogInternal (3.1.0) + - DatadogLogs (3.1.0): + - DatadogInternal (= 3.1.0) + - DatadogRUM (3.1.0): + - DatadogInternal (= 3.1.0) - DatadogSDKReactNative (2.12.1): - - DatadogCore (= 3.0.0) - - DatadogCrashReporting (= 3.0.0) - - DatadogLogs (= 3.0.0) - - DatadogRUM (= 3.0.0) - - DatadogTrace (= 3.0.0) - - DatadogWebViewTracking (= 3.0.0) + - DatadogCore (= 3.1.0) + - DatadogCrashReporting (= 3.1.0) + - DatadogLogs (= 3.1.0) + - DatadogRUM (= 3.1.0) + - DatadogTrace (= 3.1.0) + - DatadogWebViewTracking (= 3.1.0) - DoubleConversion - glog - hermes-engine @@ -38,12 +38,12 @@ PODS: - ReactCommon/turbomodule/core - Yoga - DatadogSDKReactNative/Tests (2.12.1): - - DatadogCore (= 3.0.0) - - DatadogCrashReporting (= 3.0.0) - - DatadogLogs (= 3.0.0) - - DatadogRUM (= 3.0.0) - - DatadogTrace (= 3.0.0) - - DatadogWebViewTracking (= 3.0.0) + - DatadogCore (= 3.1.0) + - DatadogCrashReporting (= 3.1.0) + - DatadogLogs (= 3.1.0) + - DatadogRUM (= 3.1.0) + - DatadogTrace (= 3.1.0) + - DatadogWebViewTracking (= 3.1.0) - DoubleConversion - glog - hermes-engine @@ -64,11 +64,11 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - DatadogTrace (3.0.0): - - DatadogInternal (= 3.0.0) + - DatadogTrace (3.1.0): + - DatadogInternal (= 3.1.0) - OpenTelemetrySwiftApi (= 1.13.1) - - DatadogWebViewTracking (3.0.0): - - DatadogInternal (= 3.0.0) + - DatadogWebViewTracking (3.1.0): + - DatadogInternal (= 3.1.0) - DoubleConversion (1.1.6) - fast_float (6.1.4) - FBLazyVector (0.76.9) @@ -1850,14 +1850,14 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: boost: 1dca942403ed9342f98334bf4c3621f011aa7946 - DatadogCore: 0c39ba4ef3dee02071f235f2fb56af7e29f4ceb0 - DatadogCrashReporting: 604e65e524cb2fc22cda39fd9c9ea5fd3bddf24e - DatadogInternal: 442673a7fb5329299b489b91ed705aa2085380f7 - DatadogLogs: 0146a0e3140ba62fd42729593c0910d692aea5fd - DatadogRUM: f97fbd0290ecec5bc952fb03a6a1ae068649243f - DatadogSDKReactNative: 0a80aa75958d595a99be54d2838db53eda7a08af - DatadogTrace: d714e7c456e612b7f171483d08086a59aa1c4213 - DatadogWebViewTracking: fb9591c09fca82b143f3843a7164a7e782175c53 + DatadogCore: d2f51c7fb4308cf3c25e55e2e7242e5d558ee71d + DatadogCrashReporting: f636f1d1c534572c0b0abcdc59df244c884d825d + DatadogInternal: 7837b2ce3d525d429682532eeda697b181299fdc + DatadogLogs: 250894b5a99da5b924a019049c0d0326823cdbd6 + DatadogRUM: 0d2a60e1abb8aacfb8827ef84f6d5deb4d5026c8 + DatadogSDKReactNative: 069ea9876220b2d09b0f4b180ce571b1b6ecbb35 + DatadogTrace: f59e933074cd285ad7e9f5af991f8fe04b095991 + DatadogWebViewTracking: 9bc92b4147aeed47eb1911451f651094aa6dd6c1 DoubleConversion: f16ae600a246532c4020132d54af21d0ddb2a385 fast_float: 06eeec4fe712a76acc9376682e4808b05ce978b6 FBLazyVector: 7605ea4810e0e10ae4815292433c09bf4324ba45 diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index f93f0ee07..94d93b3c2 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -1,34 +1,34 @@ PODS: - boost (1.84.0) - - DatadogCore (3.0.0): - - DatadogInternal (= 3.0.0) - - DatadogCrashReporting (3.0.0): - - DatadogInternal (= 3.0.0) + - DatadogCore (3.1.0): + - DatadogInternal (= 3.1.0) + - DatadogCrashReporting (3.1.0): + - DatadogInternal (= 3.1.0) - PLCrashReporter (~> 1.12.0) - - DatadogInternal (3.0.0) - - DatadogLogs (3.0.0): - - DatadogInternal (= 3.0.0) - - DatadogRUM (3.0.0): - - DatadogInternal (= 3.0.0) + - DatadogInternal (3.1.0) + - DatadogLogs (3.1.0): + - DatadogInternal (= 3.1.0) + - DatadogRUM (3.1.0): + - DatadogInternal (= 3.1.0) - DatadogSDKReactNative (2.12.1): - - DatadogCore (= 3.0.0) - - DatadogCrashReporting (= 3.0.0) - - DatadogLogs (= 3.0.0) - - DatadogRUM (= 3.0.0) - - DatadogTrace (= 3.0.0) - - DatadogWebViewTracking (= 3.0.0) + - DatadogCore (= 3.1.0) + - DatadogCrashReporting (= 3.1.0) + - DatadogLogs (= 3.1.0) + - DatadogRUM (= 3.1.0) + - DatadogTrace (= 3.1.0) + - DatadogWebViewTracking (= 3.1.0) - React-Core - DatadogSDKReactNative/Tests (2.12.1): - - DatadogCore (= 3.0.0) - - DatadogCrashReporting (= 3.0.0) - - DatadogLogs (= 3.0.0) - - DatadogRUM (= 3.0.0) - - DatadogTrace (= 3.0.0) - - DatadogWebViewTracking (= 3.0.0) + - DatadogCore (= 3.1.0) + - DatadogCrashReporting (= 3.1.0) + - DatadogLogs (= 3.1.0) + - DatadogRUM (= 3.1.0) + - DatadogTrace (= 3.1.0) + - DatadogWebViewTracking (= 3.1.0) - React-Core - DatadogSDKReactNativeSessionReplay (2.13.2): - DatadogSDKReactNative - - DatadogSessionReplay (= 3.0.0) + - DatadogSessionReplay (= 3.1.0) - DoubleConversion - glog - hermes-engine @@ -51,7 +51,7 @@ PODS: - Yoga - DatadogSDKReactNativeSessionReplay/Tests (2.13.2): - DatadogSDKReactNative - - DatadogSessionReplay (= 3.0.0) + - DatadogSessionReplay (= 3.1.0) - DoubleConversion - glog - hermes-engine @@ -74,24 +74,24 @@ PODS: - ReactCommon/turbomodule/core - Yoga - DatadogSDKReactNativeWebView (2.12.1): - - DatadogInternal (= 3.0.0) + - DatadogInternal (= 3.1.0) - DatadogSDKReactNative - - DatadogWebViewTracking (= 3.0.0) + - DatadogWebViewTracking (= 3.1.0) - React-Core - DatadogSDKReactNativeWebView/Tests (2.12.1): - - DatadogInternal (= 3.0.0) + - DatadogInternal (= 3.1.0) - DatadogSDKReactNative - - DatadogWebViewTracking (= 3.0.0) + - DatadogWebViewTracking (= 3.1.0) - React-Core - react-native-webview - React-RCTText - - DatadogSessionReplay (3.0.0): - - DatadogInternal (= 3.0.0) - - DatadogTrace (3.0.0): - - DatadogInternal (= 3.0.0) + - DatadogSessionReplay (3.1.0): + - DatadogInternal (= 3.1.0) + - DatadogTrace (3.1.0): + - DatadogInternal (= 3.1.0) - OpenTelemetrySwiftApi (= 1.13.1) - - DatadogWebViewTracking (3.0.0): - - DatadogInternal (= 3.0.0) + - DatadogWebViewTracking (3.1.0): + - DatadogInternal (= 3.1.0) - DoubleConversion (1.1.6) - fast_float (6.1.4) - FBLazyVector (0.76.9) @@ -1988,17 +1988,17 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: boost: 1dca942403ed9342f98334bf4c3621f011aa7946 - DatadogCore: 0c39ba4ef3dee02071f235f2fb56af7e29f4ceb0 - DatadogCrashReporting: 604e65e524cb2fc22cda39fd9c9ea5fd3bddf24e - DatadogInternal: 442673a7fb5329299b489b91ed705aa2085380f7 - DatadogLogs: 0146a0e3140ba62fd42729593c0910d692aea5fd - DatadogRUM: f97fbd0290ecec5bc952fb03a6a1ae068649243f - DatadogSDKReactNative: e430b3f4d7adb0fac07b61e2fb65ceacdaf82b3e - DatadogSDKReactNativeSessionReplay: 4b2a3d166a79581f18522795b40141c34cf3685d - DatadogSDKReactNativeWebView: 35dc2b9736e1aaa82b366bf6b8a8a959a9b088c5 - DatadogSessionReplay: 0357b911c6ab42c77c93b29761ecc20d9282e971 - DatadogTrace: d714e7c456e612b7f171483d08086a59aa1c4213 - DatadogWebViewTracking: fb9591c09fca82b143f3843a7164a7e782175c53 + DatadogCore: d2f51c7fb4308cf3c25e55e2e7242e5d558ee71d + DatadogCrashReporting: f636f1d1c534572c0b0abcdc59df244c884d825d + DatadogInternal: 7837b2ce3d525d429682532eeda697b181299fdc + DatadogLogs: 250894b5a99da5b924a019049c0d0326823cdbd6 + DatadogRUM: 0d2a60e1abb8aacfb8827ef84f6d5deb4d5026c8 + DatadogSDKReactNative: af351a4e1ce08124c290c52de94b0062a166cc67 + DatadogSDKReactNativeSessionReplay: dcbd55d9d0f2b86026996a8b7ec9654922d5dfe1 + DatadogSDKReactNativeWebView: 096ac87eb753b6a217b93441983264b9837c3b7e + DatadogSessionReplay: 6bc71888e2b41dd0de3325f06f0c0b3cee0e6df4 + DatadogTrace: f59e933074cd285ad7e9f5af991f8fe04b095991 + DatadogWebViewTracking: 9bc92b4147aeed47eb1911451f651094aa6dd6c1 DoubleConversion: f16ae600a246532c4020132d54af21d0ddb2a385 fast_float: 06eeec4fe712a76acc9376682e4808b05ce978b6 FBLazyVector: 7605ea4810e0e10ae4815292433c09bf4324ba45 diff --git a/packages/core/DatadogSDKReactNative.podspec b/packages/core/DatadogSDKReactNative.podspec index a2f41ddd1..c0d235304 100644 --- a/packages/core/DatadogSDKReactNative.podspec +++ b/packages/core/DatadogSDKReactNative.podspec @@ -19,14 +19,14 @@ Pod::Spec.new do |s| s.dependency "React-Core" # /!\ Remember to keep the versions in sync with DatadogSDKReactNativeSessionReplay.podspec - s.dependency 'DatadogCore', '3.0.0' - s.dependency 'DatadogLogs', '3.0.0' - s.dependency 'DatadogTrace', '3.0.0' - s.dependency 'DatadogRUM', '3.0.0' - s.dependency 'DatadogCrashReporting', '3.0.0' + s.dependency 'DatadogCore', '3.1.0' + s.dependency 'DatadogLogs', '3.1.0' + s.dependency 'DatadogTrace', '3.1.0' + s.dependency 'DatadogRUM', '3.1.0' + s.dependency 'DatadogCrashReporting', '3.1.0' # DatadogWebViewTracking is not available for tvOS - s.ios.dependency 'DatadogWebViewTracking', '3.0.0' + s.ios.dependency 'DatadogWebViewTracking', '3.1.0' s.test_spec 'Tests' do |test_spec| test_spec.source_files = 'ios/Tests/**/*.{swift,json}' diff --git a/packages/core/android/build.gradle b/packages/core/android/build.gradle index a8d09d4d1..59bb75cc3 100644 --- a/packages/core/android/build.gradle +++ b/packages/core/android/build.gradle @@ -201,16 +201,16 @@ dependencies { // This breaks builds if the React Native target is below 0.76.0. as it relies on Gradle 8.5.0. // To avoid this, we enforce 1.0.0-beta01 on RN < 0.76.0 if (reactNativeMinorVersion < 76) { - implementation("com.datadoghq:dd-sdk-android-rum:3.0.0") { + implementation("com.datadoghq:dd-sdk-android-rum:3.1.0") { exclude group: "androidx.metrics", module: "metrics-performance" } implementation "androidx.metrics:metrics-performance:1.0.0-beta01" } else { - implementation "com.datadoghq:dd-sdk-android-rum:3.0.0" + implementation "com.datadoghq:dd-sdk-android-rum:3.1.0" } - implementation "com.datadoghq:dd-sdk-android-logs:3.0.0" - implementation "com.datadoghq:dd-sdk-android-trace:3.0.0" - implementation "com.datadoghq:dd-sdk-android-webview:3.0.0" + implementation "com.datadoghq:dd-sdk-android-logs:3.1.0" + implementation "com.datadoghq:dd-sdk-android-trace:3.1.0" + implementation "com.datadoghq:dd-sdk-android-webview:3.1.0" implementation "com.google.code.gson:gson:2.10.0" testImplementation "org.junit.platform:junit-platform-launcher:1.6.2" testImplementation "org.junit.jupiter:junit-jupiter-api:5.6.2" diff --git a/packages/core/android/src/test/kotlin/com/datadog/tools/unit/MockRumMonitor.kt b/packages/core/android/src/test/kotlin/com/datadog/tools/unit/MockRumMonitor.kt index 7c5585edd..702cc2533 100644 --- a/packages/core/android/src/test/kotlin/com/datadog/tools/unit/MockRumMonitor.kt +++ b/packages/core/android/src/test/kotlin/com/datadog/tools/unit/MockRumMonitor.kt @@ -13,6 +13,7 @@ import com.datadog.android.rum.RumMonitor import com.datadog.android.rum.RumResourceKind import com.datadog.android.rum.RumResourceMethod import com.datadog.android.rum._RumInternalProxy +import com.datadog.android.rum.featureoperations.FailureReason class MockRumMonitor : RumMonitor { override var debug = false @@ -123,4 +124,26 @@ class MockRumMonitor : RumMonitor { key: Any, attributes: Map ) {} + + @ExperimentalRumApi + override fun startFeatureOperation( + name: String, + operationKey: String?, + attributes: Map + ) {} + + @ExperimentalRumApi + override fun succeedFeatureOperation( + name: String, + operationKey: String?, + attributes: Map + ) {} + + @ExperimentalRumApi + override fun failFeatureOperation( + name: String, + operationKey: String?, + failureReason: FailureReason, + attributes: Map + ) {} } diff --git a/packages/core/ios/Tests/DdLogsTests.swift b/packages/core/ios/Tests/DdLogsTests.swift index 2d9fdebac..60640e807 100644 --- a/packages/core/ios/Tests/DdLogsTests.swift +++ b/packages/core/ios/Tests/DdLogsTests.swift @@ -463,6 +463,8 @@ private class MockNativeLogger: LoggerProtocol { } extension MockNativeLogger: InternalLoggerProtocol { + func critical(message: String, error: (any Error)?, attributes: [String : any Encodable]?, completionHandler: @escaping DatadogInternal.CompletionHandler) {} + func log(level: DatadogLogs.LogLevel, message: String, errorKind: String?, errorMessage: String?, stackTrace: String?, attributes: [String : Encodable]?) { receivedMethodCalls.append(MethodCall( kind: MockNativeLogger.MethodCall.Kind(from: level), diff --git a/packages/react-native-session-replay/DatadogSDKReactNativeSessionReplay.podspec b/packages/react-native-session-replay/DatadogSDKReactNativeSessionReplay.podspec index 3a2d5ff48..6a5d0b78f 100644 --- a/packages/react-native-session-replay/DatadogSDKReactNativeSessionReplay.podspec +++ b/packages/react-native-session-replay/DatadogSDKReactNativeSessionReplay.podspec @@ -23,7 +23,7 @@ Pod::Spec.new do |s| s.dependency "React-Core" # /!\ Remember to keep the version in sync with DatadogSDKReactNative.podspec - s.dependency 'DatadogSessionReplay', '3.0.0' + s.dependency 'DatadogSessionReplay', '3.1.0' s.dependency 'DatadogSDKReactNative' s.test_spec 'Tests' do |test_spec| diff --git a/packages/react-native-session-replay/android/build.gradle b/packages/react-native-session-replay/android/build.gradle index 6d6fe20b1..a0d77f2ff 100644 --- a/packages/react-native-session-replay/android/build.gradle +++ b/packages/react-native-session-replay/android/build.gradle @@ -216,8 +216,8 @@ dependencies { api "com.facebook.react:react-android:$reactNativeVersion" } implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" - implementation "com.datadoghq:dd-sdk-android-session-replay:3.0.0" - implementation "com.datadoghq:dd-sdk-android-internal:3.0.0" + implementation "com.datadoghq:dd-sdk-android-session-replay:3.1.0" + implementation "com.datadoghq:dd-sdk-android-internal:3.1.0" implementation project(path: ':datadog_mobile-react-native') testImplementation "org.junit.platform:junit-platform-launcher:1.6.2" diff --git a/packages/react-native-webview/DatadogSDKReactNativeWebView.podspec b/packages/react-native-webview/DatadogSDKReactNativeWebView.podspec index 26e160bbc..080a853d8 100644 --- a/packages/react-native-webview/DatadogSDKReactNativeWebView.podspec +++ b/packages/react-native-webview/DatadogSDKReactNativeWebView.podspec @@ -23,8 +23,8 @@ Pod::Spec.new do |s| end # /!\ Remember to keep the version in sync with DatadogSDKReactNative.podspec - s.dependency 'DatadogWebViewTracking', '3.0.0' - s.dependency 'DatadogInternal', '3.0.0' + s.dependency 'DatadogWebViewTracking', '3.1.0' + s.dependency 'DatadogInternal', '3.1.0' s.dependency 'DatadogSDKReactNative' s.test_spec 'Tests' do |test_spec| diff --git a/packages/react-native-webview/android/build.gradle b/packages/react-native-webview/android/build.gradle index 098eae019..87ca1b7e7 100644 --- a/packages/react-native-webview/android/build.gradle +++ b/packages/react-native-webview/android/build.gradle @@ -190,7 +190,7 @@ dependencies { implementation "com.facebook.react:react-android:$reactNativeVersion" } - implementation "com.datadoghq:dd-sdk-android-webview:3.0.0" + implementation "com.datadoghq:dd-sdk-android-webview:3.1.0" implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation project(path: ':datadog_mobile-react-native') From 3821b5b85d1b6b970ee6a2065309c47e4649b77a Mon Sep 17 00:00:00 2001 From: Sergio Barrio Date: Fri, 19 Sep 2025 14:33:51 +0200 Subject: [PATCH 070/410] Fix internaltTestingTools tests --- .../DdInternalTestingImplementation.kt | 45 ++++++++------- .../DdInternalTestingImplementationTest.kt | 55 ++++++++++++------- 2 files changed, 60 insertions(+), 40 deletions(-) diff --git a/packages/internal-testing-tools/android/src/main/kotlin/com/datadog/reactnative/internaltesting/DdInternalTestingImplementation.kt b/packages/internal-testing-tools/android/src/main/kotlin/com/datadog/reactnative/internaltesting/DdInternalTestingImplementation.kt index b33bef6de..0d548aa72 100644 --- a/packages/internal-testing-tools/android/src/main/kotlin/com/datadog/reactnative/internaltesting/DdInternalTestingImplementation.kt +++ b/packages/internal-testing-tools/android/src/main/kotlin/com/datadog/reactnative/internaltesting/DdInternalTestingImplementation.kt @@ -6,11 +6,13 @@ package com.datadog.reactnative.internaltesting +import androidx.annotation.WorkerThread import com.datadog.android.api.InternalLogger import com.datadog.android.Datadog import com.datadog.android.api.context.DatadogContext import com.datadog.android.api.context.NetworkInfo import com.datadog.android.api.context.TimeInfo +import com.datadog.android.api.feature.EventWriteScope import com.datadog.android.api.feature.Feature import com.datadog.android.api.feature.FeatureScope import com.datadog.android.api.storage.EventBatchWriter @@ -112,53 +114,54 @@ internal class FeatureScopeInterceptor( private val featureScope: FeatureScope, private val core: InternalSdkCore, ) : FeatureScope by featureScope { - private val eventsBatchInterceptor = EventBatchInterceptor() + private val eventWriteScopeInterceptor = EventWriteScopeInterceptor() fun eventsWritten(): List { - return eventsBatchInterceptor.events + return eventWriteScopeInterceptor.events } fun clearData() { - eventsBatchInterceptor.clearData() + eventWriteScopeInterceptor.clearData() } // region FeatureScope override fun withWriteContext( - forceNewBatch: Boolean, - callback: (DatadogContext, EventBatchWriter) -> Unit + withFeatureContexts: Set, + callback: (datadogContext: DatadogContext, write: EventWriteScope) -> Unit ) { - featureScope.withWriteContext(forceNewBatch, callback) + featureScope.withWriteContext(withFeatureContexts, callback) core.getDatadogContext()?.let { - callback(it, eventsBatchInterceptor) + callback(it, eventWriteScopeInterceptor) } } // endregion } - -internal class EventBatchInterceptor: EventBatchWriter { +internal class EventWriteScopeInterceptor : EventWriteScope { internal val events = mutableListOf() - override fun currentMetadata(): ByteArray? { - return null - } - fun clearData() { events.clear() } - override fun write( - event: RawBatchEvent, - batchMetadata: ByteArray?, - eventType: EventType - ): Boolean { - val eventContent = String(event.data) + private val writer = object : EventBatchWriter { + override fun currentMetadata(): ByteArray? = null - events += eventContent + override fun write( + event: RawBatchEvent, + batchMetadata: ByteArray?, + eventType: EventType + ): Boolean { + events += String(event.data) + return true + } + } - return true + override fun invoke(p1: (EventBatchWriter) -> Unit) { + p1(writer) } } + diff --git a/packages/internal-testing-tools/android/src/test/kotlin/com/datadog/reactnative/internaltesting/DdInternalTestingImplementationTest.kt b/packages/internal-testing-tools/android/src/test/kotlin/com/datadog/reactnative/internaltesting/DdInternalTestingImplementationTest.kt index 4a6938f9b..d25db9274 100644 --- a/packages/internal-testing-tools/android/src/test/kotlin/com/datadog/reactnative/internaltesting/DdInternalTestingImplementationTest.kt +++ b/packages/internal-testing-tools/android/src/test/kotlin/com/datadog/reactnative/internaltesting/DdInternalTestingImplementationTest.kt @@ -10,9 +10,9 @@ import android.content.Context import com.datadog.android.Datadog import com.datadog.android.api.SdkCore import com.datadog.android.api.context.DatadogContext +import com.datadog.android.api.feature.EventWriteScope import com.datadog.android.api.feature.Feature import com.datadog.android.api.feature.FeatureScope -import com.datadog.android.api.storage.EventBatchWriter import com.datadog.android.api.storage.EventType import com.datadog.android.api.storage.RawBatchEvent import com.datadog.android.api.storage.datastore.DataStoreHandler @@ -85,23 +85,27 @@ internal class DdInternalTestingImplementationTest { wrappedCore.registerFeature(mockFeature) requireNotNull(wrappedCore.getFeature(mockFeature.name)) - .withWriteContext { _, eventBatchWriter -> - eventBatchWriter.write( - RawBatchEvent(data = "mock event for test".toByteArray()), - batchMetadata = null, - eventType = EventType.DEFAULT + .withWriteContext { _, writeScope -> + writeScope { + val rawBatchEvent = + RawBatchEvent(data = "mock event for test".toByteArray()) + it.write( + rawBatchEvent, + batchMetadata = null, + eventType = EventType.DEFAULT + ) + } + + // Then + assertThat( + wrappedCore.featureScopes[mockFeature.name] + ?.eventsWritten() + ?.first() ) + .isEqualTo( + "mock event for test" + ) } - - // Then - assertThat( - wrappedCore.featureScopes[mockFeature.name] - ?.eventsWritten() - ?.first() - ) - .isEqualTo( - "mock event for test" - ) } } } @@ -116,10 +120,23 @@ internal class MockFeatureScope(private val feature: Feature) : FeatureScope { return feature as T } + override fun withContext( + withFeatureContexts: Set, + callback: (datadogContext: DatadogContext) -> Unit + ) { + } + override fun withWriteContext( - forceNewBatch: Boolean, - callback: (DatadogContext, EventBatchWriter) -> Unit - ) {} + withFeatureContexts: Set, + callback: (datadogContext: DatadogContext, write: EventWriteScope) -> Unit + ) { + } + + override fun getWriteContextSync( + withFeatureContexts: Set + ): Pair? { + return TODO("Provide the return value") + } } internal class MockFeature(override val name: String) : Feature { From 89ed52e4f98427ef7168846833f4c4caf350a3c6 Mon Sep 17 00:00:00 2001 From: Sergio Barrio Date: Fri, 5 Sep 2025 17:47:29 +0200 Subject: [PATCH 071/410] Use native sdk's core instance instead of the one inside RN SDK wrapper --- .../com/datadog/reactnative/DdSdkNativeInitialization.kt | 2 ++ .../src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt | 6 +----- .../src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt | 7 +------ 3 files changed, 4 insertions(+), 11 deletions(-) diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkNativeInitialization.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkNativeInitialization.kt index 4388ad5f6..ee55d08fe 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkNativeInitialization.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkNativeInitialization.kt @@ -65,7 +65,9 @@ class DdSdkNativeInitialization internal constructor( datadog.initialize(appContext, sdkConfiguration, trackingConsent) Rum.enable(rumConfiguration, Datadog.getInstance()) + Logs.enable(logsConfiguration, Datadog.getInstance()) + Trace.enable(traceConfiguration, Datadog.getInstance()) } diff --git a/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt b/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt index 97acb2ebf..3c86e28b4 100644 --- a/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt +++ b/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt @@ -21,11 +21,7 @@ class DdSdk( ddTelemetry: DdTelemetry = DdTelemetry() ) : ReactContextBaseJavaModule(reactContext) { - private val implementation = DdSdkImplementation( - reactContext, - datadog = datadogWrapper, - ddTelemetry - ) + private val implementation = DdSdkImplementation(reactContext, datadog = datadogWrapper, ddTelemetry) private var lifecycleEventListener: LifecycleEventListener? = null override fun getName(): String = DdSdkImplementation.NAME diff --git a/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt b/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt index 8797bcc64..c9f91738f 100644 --- a/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt +++ b/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt @@ -163,12 +163,7 @@ internal class DdSdkTest { answer.getArgument(0).run() true } - testedBridgeSdk = DdSdkImplementation( - mockReactContext, - mockDatadog, - mockDdTelemetry, - TestUiThreadExecutor() - ) + testedBridgeSdk = DdSdkImplementation(mockReactContext, mockDatadog, mockDdTelemetry, TestUiThreadExecutor()) DatadogSDKWrapperStorage.onInitializedListeners.clear() } From 2340b9b77fcf4c307556c859cf2a9c83aa455718 Mon Sep 17 00:00:00 2001 From: Marco Saia Date: Mon, 22 Sep 2025 16:20:27 +0200 Subject: [PATCH 072/410] Fixed internal testing tools and unit tests --- .../com/datadog/reactnative/DdSdkNativeInitialization.kt | 2 -- .../src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt | 6 +++++- .../src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt | 7 ++++++- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkNativeInitialization.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkNativeInitialization.kt index ee55d08fe..4388ad5f6 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkNativeInitialization.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkNativeInitialization.kt @@ -65,9 +65,7 @@ class DdSdkNativeInitialization internal constructor( datadog.initialize(appContext, sdkConfiguration, trackingConsent) Rum.enable(rumConfiguration, Datadog.getInstance()) - Logs.enable(logsConfiguration, Datadog.getInstance()) - Trace.enable(traceConfiguration, Datadog.getInstance()) } diff --git a/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt b/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt index 3c86e28b4..97acb2ebf 100644 --- a/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt +++ b/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt @@ -21,7 +21,11 @@ class DdSdk( ddTelemetry: DdTelemetry = DdTelemetry() ) : ReactContextBaseJavaModule(reactContext) { - private val implementation = DdSdkImplementation(reactContext, datadog = datadogWrapper, ddTelemetry) + private val implementation = DdSdkImplementation( + reactContext, + datadog = datadogWrapper, + ddTelemetry + ) private var lifecycleEventListener: LifecycleEventListener? = null override fun getName(): String = DdSdkImplementation.NAME diff --git a/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt b/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt index c9f91738f..8797bcc64 100644 --- a/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt +++ b/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt @@ -163,7 +163,12 @@ internal class DdSdkTest { answer.getArgument(0).run() true } - testedBridgeSdk = DdSdkImplementation(mockReactContext, mockDatadog, mockDdTelemetry, TestUiThreadExecutor()) + testedBridgeSdk = DdSdkImplementation( + mockReactContext, + mockDatadog, + mockDdTelemetry, + TestUiThreadExecutor() + ) DatadogSDKWrapperStorage.onInitializedListeners.clear() } From e3acea928fad1b4e6085505a9b5ad8028d12d931 Mon Sep 17 00:00:00 2001 From: Sergio Barrio Date: Fri, 19 Sep 2025 17:37:31 +0200 Subject: [PATCH 073/410] Expose clearUserInfo API --- packages/core/__mocks__/react-native.ts | 3 + .../datadog/reactnative/DatadogSDKWrapper.kt | 4 ++ .../com/datadog/reactnative/DatadogWrapper.kt | 5 ++ .../reactnative/DdSdkImplementation.kt | 10 ++- .../kotlin/com/datadog/reactnative/DdSdk.kt | 8 +++ .../kotlin/com/datadog/reactnative/DdSdk.kt | 8 +++ .../com/datadog/reactnative/DdSdkTest.kt | 11 +++ packages/core/ios/Sources/DdSdk.mm | 12 +++- .../ios/Sources/DdSdkImplementation.swift | 8 ++- packages/core/ios/Tests/DdSdkTests.swift | 67 +++++++++++++++++++ packages/core/jest/mock.js | 3 + packages/core/src/DdSdkReactNative.tsx | 10 +++ .../src/__tests__/DdSdkReactNative.test.tsx | 26 +++++++ .../UserInfoSingleton/UserInfoSingleton.ts | 4 ++ .../__tests__/UserInfoSingleton.test.ts | 55 ++++++++++++--- packages/core/src/specs/NativeDdSdk.ts | 5 ++ packages/core/src/types.tsx | 5 ++ 17 files changed, 230 insertions(+), 14 deletions(-) diff --git a/packages/core/__mocks__/react-native.ts b/packages/core/__mocks__/react-native.ts index 260fe68a7..0e85e65ae 100644 --- a/packages/core/__mocks__/react-native.ts +++ b/packages/core/__mocks__/react-native.ts @@ -24,6 +24,9 @@ actualRN.NativeModules.DdSdk = { addUserExtraInfo: jest.fn().mockImplementation( () => new Promise(resolve => resolve()) ) as jest.MockedFunction, + clearUserInfo: jest.fn().mockImplementation( + () => new Promise(resolve => resolve()) + ) as jest.MockedFunction, setAttributes: jest.fn().mockImplementation( () => new Promise(resolve => resolve()) ) as jest.MockedFunction, diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogSDKWrapper.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogSDKWrapper.kt index 198061d14..f781687eb 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogSDKWrapper.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogSDKWrapper.kt @@ -86,6 +86,10 @@ internal class DatadogSDKWrapper : DatadogWrapper { Datadog.addUserProperties(extraInfo) } + override fun clearUserInfo() { + Datadog.clearUserInfo() + } + override fun addRumGlobalAttributes(attributes: Map) { val rumMonitor = this.getRumMonitor() for (attribute in attributes) { diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogWrapper.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogWrapper.kt index 3ae3e6266..49d606b35 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogWrapper.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogWrapper.kt @@ -86,6 +86,11 @@ interface DatadogWrapper { extraInfo: Map ) + /** + * Clears the user information. + */ + fun clearUserInfo() + /** * Adds global attributes. * diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt index 7a5c6848c..7adcf7438 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt @@ -102,7 +102,7 @@ class DdSdkImplementation( } /** - * Sets the user information. + * Sets the user extra information. * @param userExtraInfo: The additional information. (To set the id, name or email please user setUserInfo). */ fun addUserExtraInfo( @@ -114,6 +114,14 @@ class DdSdkImplementation( promise.resolve(null) } + /** + * Clears the user information. + */ + fun clearUserInfo(promise: Promise) { + datadog.clearUserInfo() + promise.resolve(null) + } + /** * Set the tracking consent regarding the data collection. * @param trackingConsent Consent, which can take one of the following values: 'pending', diff --git a/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdSdk.kt b/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdSdk.kt index cfafffffe..4e4668a3e 100644 --- a/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdSdk.kt +++ b/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdSdk.kt @@ -67,6 +67,14 @@ class DdSdk( implementation.addUserExtraInfo(extraInfo, promise) } + /** + * Clears the user information. + */ + @ReactMethod + override fun clearUserInfo(promise: Promise) { + implementation.clearUserInfo(promise) + } + /** * Set the tracking consent regarding the data collection. * @param trackingConsent Consent, which can take one of the following values: 'pending', diff --git a/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt b/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt index 97acb2ebf..0ebdd37fb 100644 --- a/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt +++ b/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt @@ -93,6 +93,14 @@ class DdSdk( implementation.addUserExtraInfo(extraInfo, promise) } + /** + * Clears the user information. + */ + @ReactMethod + fun clearUserInfo(promise: Promise) { + implementation.clearUserInfo(promise) + } + /** * Set the tracking consent regarding the data collection. * @param trackingConsent Consent, which can take one of the following values: 'pending', diff --git a/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt b/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt index 8797bcc64..f917bb847 100644 --- a/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt +++ b/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt @@ -2954,6 +2954,17 @@ internal class DdSdkTest { } } + @Test + fun `𝕄 clear user info 𝕎 clearUserInfo()`() { + // When + testedBridgeSdk.clearUserInfo(mockPromise) + + // Then + argumentCaptor> { + verify(mockDatadog).clearUserInfo() + } + } + @Test fun `𝕄 set RUM attributes 𝕎 setAttributes`( @MapForgery( diff --git a/packages/core/ios/Sources/DdSdk.mm b/packages/core/ios/Sources/DdSdk.mm index 918a8db03..674cd0fbc 100644 --- a/packages/core/ios/Sources/DdSdk.mm +++ b/packages/core/ios/Sources/DdSdk.mm @@ -51,6 +51,12 @@ + (void)initFromNative { [self addUserExtraInfo:extraInfo resolve:resolve reject:reject]; } +RCT_EXPORT_METHOD(clearUserInfo:(RCTPromiseResolveBlock)resolve + withRejecter:(RCTPromiseRejectBlock)reject) +{ + [self clearUserInfo:resolve reject:reject]; +} + RCT_REMAP_METHOD(setTrackingConsent, withTrackingConsent:(NSString*)trackingConsent withResolver:(RCTPromiseResolveBlock)resolve withRejecter:(RCTPromiseRejectBlock)reject) @@ -81,7 +87,7 @@ + (void)initFromNative { [self consumeWebviewEvent:message resolve:resolve reject:reject]; } -RCT_REMAP_METHOD(clearAllData, withResolver:(RCTPromiseResolveBlock)resolve +RCT_EXPORT_METHOD(clearAllData:(RCTPromiseResolveBlock)resolve withRejecter:(RCTPromiseRejectBlock)reject) { [self clearAllData:resolve reject:reject]; @@ -143,6 +149,10 @@ - (void)setUserInfo:(NSDictionary *)userInfo resolve:(RCTPromiseResolveBlock)res [self.ddSdkImplementation setUserInfoWithUserInfo:userInfo resolve:resolve reject:reject]; } +- (void)clearUserInfo:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + [self.ddSdkImplementation clearUserInfoWithResolve:resolve reject:reject]; +} + -(void)addUserExtraInfo:(NSDictionary *)extraInfo resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { [self.ddSdkImplementation addUserExtraInfoWithExtraInfo:extraInfo resolve:resolve reject:reject]; } diff --git a/packages/core/ios/Sources/DdSdkImplementation.swift b/packages/core/ios/Sources/DdSdkImplementation.swift index b2e610635..03c630b70 100644 --- a/packages/core/ios/Sources/DdSdkImplementation.swift +++ b/packages/core/ios/Sources/DdSdkImplementation.swift @@ -108,7 +108,7 @@ public class DdSdkImplementation: NSObject { resolve(nil) } - + @objc public func addUserExtraInfo(extraInfo: NSDictionary, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { let castedExtraInfo = castAttributesToSwift(extraInfo) @@ -117,6 +117,12 @@ public class DdSdkImplementation: NSObject { resolve(nil) } + @objc + public func clearUserInfo(resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { + Datadog.clearUserInfo() + resolve(nil) + } + @objc public func setTrackingConsent(trackingConsent: NSString, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { Datadog.set(trackingConsent: (trackingConsent as NSString?).asTrackingConsent()) diff --git a/packages/core/ios/Tests/DdSdkTests.swift b/packages/core/ios/Tests/DdSdkTests.swift index 555ce4549..efbf57b96 100644 --- a/packages/core/ios/Tests/DdSdkTests.swift +++ b/packages/core/ios/Tests/DdSdkTests.swift @@ -651,6 +651,73 @@ class DdSdkTests: XCTestCase { XCTFail("extra-info-4 is not of expected type or value") } } + + func testClearUserInfo() throws { + let bridge = DdSdkImplementation( + mainDispatchQueue: DispatchQueueMock(), + jsDispatchQueue: DispatchQueueMock(), + jsRefreshRateMonitor: JSRefreshRateMonitor(), + RUMMonitorProvider: { MockRUMMonitor() }, + RUMMonitorInternalProvider: { nil } + ) + bridge.initialize( + configuration: .mockAny(), + resolve: mockResolve, + reject: mockReject + ) + + bridge.setUserInfo( + userInfo: NSDictionary( + dictionary: [ + "id": "id_123", + "name": "John Doe", + "email": "john@doe.com", + "extraInfo": [ + "extra-info-1": 123, + "extra-info-2": "abc", + "extra-info-3": true, + "extra-info-4": [ + "nested-extra-info-1": 456 + ], + ], + ] + ), + resolve: mockResolve, + reject: mockReject + ) + + var ddContext = try XCTUnwrap(CoreRegistry.default as? DatadogCore).contextProvider.read() + var userInfo = try XCTUnwrap(ddContext.userInfo) + + XCTAssertEqual(userInfo.id, "id_123") + XCTAssertEqual(userInfo.name, "John Doe") + XCTAssertEqual(userInfo.email, "john@doe.com") + XCTAssertEqual(userInfo.extraInfo["extra-info-1"] as? Int64, 123) + XCTAssertEqual(userInfo.extraInfo["extra-info-2"] as? String, "abc") + XCTAssertEqual(userInfo.extraInfo["extra-info-3"] as? Bool, true) + + if let extraInfo4Encodable = userInfo.extraInfo["extra-info-4"] + as? DatadogSDKReactNative.AnyEncodable, + let extraInfo4Dict = extraInfo4Encodable.value as? [String: Int] + { + XCTAssertEqual(extraInfo4Dict, ["nested-extra-info-1": 456]) + } else { + XCTFail("extra-info-4 is not of expected type or value") + } + + bridge.clearUserInfo(resolve: mockResolve, reject: mockReject) + + ddContext = try XCTUnwrap(CoreRegistry.default as? DatadogCore).contextProvider.read() + userInfo = try XCTUnwrap(ddContext.userInfo) + + XCTAssertEqual(userInfo.id, nil) + XCTAssertEqual(userInfo.name, nil) + XCTAssertEqual(userInfo.email, nil) + XCTAssertEqual(userInfo.extraInfo["extra-info-1"] as? Int64, nil) + XCTAssertEqual(userInfo.extraInfo["extra-info-2"] as? String, nil) + XCTAssertEqual(userInfo.extraInfo["extra-info-3"] as? Bool, nil) + XCTAssertEqual(userInfo.extraInfo["extra-info-4"] as? [String: Int], nil) + } func testSettingAttributes() { let rumMonitorMock = MockRUMMonitor() diff --git a/packages/core/jest/mock.js b/packages/core/jest/mock.js index a8161295a..8e154c4cd 100644 --- a/packages/core/jest/mock.js +++ b/packages/core/jest/mock.js @@ -33,6 +33,9 @@ module.exports = { addUserExtraInfo: jest .fn() .mockImplementation(() => new Promise(resolve => resolve())), + clearUserInfo: jest + .fn() + .mockImplementation(() => new Promise(resolve => resolve())), setAttributes: jest .fn() .mockImplementation(() => new Promise(resolve => resolve())), diff --git a/packages/core/src/DdSdkReactNative.tsx b/packages/core/src/DdSdkReactNative.tsx index 668ae09f3..1838542df 100644 --- a/packages/core/src/DdSdkReactNative.tsx +++ b/packages/core/src/DdSdkReactNative.tsx @@ -214,6 +214,16 @@ export class DdSdkReactNative { UserInfoSingleton.getInstance().setUserInfo(userInfo); }; + /** + * Clears the user information. + * @returns a Promise. + */ + static clearUserInfo = async (): Promise => { + InternalLog.log('Clearing user info', SdkVerbosity.DEBUG); + await DdSdk.clearUserInfo(); + UserInfoSingleton.getInstance().clearUserInfo(); + }; + /** * Set the user information. * @param extraUserInfo: The additional information. (To set the id, name or email please user setUserInfo). diff --git a/packages/core/src/__tests__/DdSdkReactNative.test.tsx b/packages/core/src/__tests__/DdSdkReactNative.test.tsx index f9405aa51..57ff5d0ed 100644 --- a/packages/core/src/__tests__/DdSdkReactNative.test.tsx +++ b/packages/core/src/__tests__/DdSdkReactNative.test.tsx @@ -1112,6 +1112,32 @@ describe('DdSdkReactNative', () => { }); }); + describe('clearUserInfo', () => { + it('calls SDK method when clearUserInfo, and clears the user in UserProvider', async () => { + // GIVEN + const userInfo = { + id: 'id', + name: 'name', + email: 'email', + extraInfo: { + foo: 'bar' + } + }; + + await DdSdkReactNative.setUserInfo(userInfo); + + // WHEN + await DdSdkReactNative.clearUserInfo(); + + // THEN + expect(DdSdk.clearUserInfo).toHaveBeenCalledTimes(1); + expect(DdSdk.setUserInfo).toHaveBeenCalled(); + expect(UserInfoSingleton.getInstance().getUserInfo()).toEqual( + undefined + ); + }); + }); + describe('setTrackingConsent', () => { it('calls SDK method when setTrackingConsent', async () => { // GIVEN diff --git a/packages/core/src/sdk/UserInfoSingleton/UserInfoSingleton.ts b/packages/core/src/sdk/UserInfoSingleton/UserInfoSingleton.ts index 26392d794..3ce23614b 100644 --- a/packages/core/src/sdk/UserInfoSingleton/UserInfoSingleton.ts +++ b/packages/core/src/sdk/UserInfoSingleton/UserInfoSingleton.ts @@ -16,6 +16,10 @@ class UserInfoProvider { getUserInfo = (): UserInfo | undefined => { return this.userInfo; }; + + clearUserInfo = () => { + this.userInfo = undefined; + }; } export class UserInfoSingleton { diff --git a/packages/core/src/sdk/UserInfoSingleton/__tests__/UserInfoSingleton.test.ts b/packages/core/src/sdk/UserInfoSingleton/__tests__/UserInfoSingleton.test.ts index 1f7ae84e7..f8e7276d6 100644 --- a/packages/core/src/sdk/UserInfoSingleton/__tests__/UserInfoSingleton.test.ts +++ b/packages/core/src/sdk/UserInfoSingleton/__tests__/UserInfoSingleton.test.ts @@ -7,27 +7,60 @@ import { UserInfoSingleton } from '../UserInfoSingleton'; describe('UserInfoSingleton', () => { - it('sets, returns and resets the user info', () => { + beforeEach(() => { + UserInfoSingleton.reset(); + }); + + it('returns undefined by default', () => { + expect(UserInfoSingleton.getInstance().getUserInfo()).toBeUndefined(); + }); + + it('stores and returns user info after setUserInfo', () => { + const info = { + id: 'test', + email: 'user@mail.com', + extraInfo: { loggedIn: true } + }; + + UserInfoSingleton.getInstance().setUserInfo(info); + + expect(UserInfoSingleton.getInstance().getUserInfo()).toEqual(info); + }); + + it('clears user info with clearUserInfo', () => { UserInfoSingleton.getInstance().setUserInfo({ id: 'test', email: 'user@mail.com', - extraInfo: { - loggedIn: true - } + extraInfo: { loggedIn: true } }); - expect(UserInfoSingleton.getInstance().getUserInfo()).toEqual({ + UserInfoSingleton.getInstance().clearUserInfo(); + + expect(UserInfoSingleton.getInstance().getUserInfo()).toBeUndefined(); + }); + + it('reset() replaces the provider and clears stored user info', () => { + const instanceBefore = UserInfoSingleton.getInstance(); + + UserInfoSingleton.getInstance().setUserInfo({ id: 'test', email: 'user@mail.com', - extraInfo: { - loggedIn: true - } + extraInfo: { loggedIn: true } }); UserInfoSingleton.reset(); - expect(UserInfoSingleton.getInstance().getUserInfo()).toEqual( - undefined - ); + const instanceAfter = UserInfoSingleton.getInstance(); + + expect(instanceAfter).not.toBe(instanceBefore); + + expect(instanceAfter.getUserInfo()).toBeUndefined(); + }); + + it('getInstance returns the same provider between calls (singleton behavior)', () => { + const a = UserInfoSingleton.getInstance(); + const b = UserInfoSingleton.getInstance(); + + expect(a).toBe(b); }); }); diff --git a/packages/core/src/specs/NativeDdSdk.ts b/packages/core/src/specs/NativeDdSdk.ts index bbf2572ee..a2ce1120e 100644 --- a/packages/core/src/specs/NativeDdSdk.ts +++ b/packages/core/src/specs/NativeDdSdk.ts @@ -37,6 +37,11 @@ export interface Spec extends TurboModule { */ setUserInfo(user: Object): Promise; + /** + * Clears the user information. + */ + clearUserInfo(): Promise; + /** * Add custom attributes to the current user information * @param extraInfo: The extraInfo object containing additionall custom attributes diff --git a/packages/core/src/types.tsx b/packages/core/src/types.tsx index fe1a5895c..bad19d429 100644 --- a/packages/core/src/types.tsx +++ b/packages/core/src/types.tsx @@ -98,6 +98,11 @@ export type DdSdkType = { */ setUserInfo(userInfo: UserInfo): Promise; + /** + * Clears the user information. + */ + clearUserInfo(): Promise; + /** * Add additional user information. * @param extraUserInfo: The additional information. (To set the id, name or email please user setUserInfo). From a3fc308332aeaabaf30a8d8e64095cb1c9592fa1 Mon Sep 17 00:00:00 2001 From: Sergio Barrio Date: Mon, 29 Sep 2025 12:20:44 +0200 Subject: [PATCH 074/410] Update attribute API --- benchmarks/src/testSetup/monitor.ts | 5 +- example/src/ddUtils.tsx | 4 +- packages/codepush/__mocks__/react-native.ts | 13 +- packages/core/__mocks__/react-native.ts | 13 +- .../datadog/reactnative/DatadogSDKWrapper.kt | 17 ++- .../com/datadog/reactnative/DatadogWrapper.kt | 22 ++++ .../reactnative/DdSdkImplementation.kt | 51 ++++++- .../kotlin/com/datadog/reactnative/DdSdk.kt | 39 +++++- .../kotlin/com/datadog/reactnative/DdSdk.kt | 37 +++++- .../com/datadog/reactnative/DdSdkTest.kt | 118 ++++++++++++++++- packages/core/ios/Sources/AnyEncodable.swift | 21 ++- packages/core/ios/Sources/DdSdk.mm | 42 +++++- .../ios/Sources/DdSdkImplementation.swift | 31 ++++- packages/core/ios/Sources/GlobalState.swift | 2 +- packages/core/ios/Tests/DdSdkTests.swift | 124 +++++++++++++++++- packages/core/ios/Tests/MockRUMMonitor.swift | 12 +- packages/core/jest/mock.js | 11 +- packages/core/src/DdSdkReactNative.tsx | 53 +++++++- .../src/__tests__/DdSdkReactNative.test.tsx | 68 +++++++++- .../AttributesSingleton.ts | 26 +++- .../__tests__/AttributesSingleton.test.ts | 60 +++++++-- packages/core/src/specs/NativeDdSdk.ts | 23 +++- packages/core/src/types.tsx | 21 ++- .../__mocks__/react-native.ts | 4 +- 24 files changed, 742 insertions(+), 75 deletions(-) diff --git a/benchmarks/src/testSetup/monitor.ts b/benchmarks/src/testSetup/monitor.ts index c7cc23473..93ea0fccd 100644 --- a/benchmarks/src/testSetup/monitor.ts +++ b/benchmarks/src/testSetup/monitor.ts @@ -4,8 +4,7 @@ * Copyright 2016-Present Datadog, Inc. */ -import { DefaultTimeProvider, RumActionType } from "@datadog/mobile-react-native"; -import { ErrorSource } from "@datadog/mobile-react-native/lib/typescript/rum/types"; +import { DefaultTimeProvider, ErrorSource, RumActionType } from "@datadog/mobile-react-native"; import type { DdRumType, ResourceKind } from "@datadog/mobile-react-native/lib/typescript/rum/types"; import type { GestureResponderEvent } from "react-native/types"; @@ -72,4 +71,4 @@ export const Monitor: Pick { DdLogs.info('The RN Sdk was properly initialized') DdSdkReactNative.setUserInfo({id: "1337", name: "Xavier", email: "xg@example.com", extraInfo: { type: "premium" } }) - DdSdkReactNative.setAttributes({campaign: "ad-network"}) + DdSdkReactNative.addAttributes({campaign: "ad-network"}) }); } diff --git a/packages/codepush/__mocks__/react-native.ts b/packages/codepush/__mocks__/react-native.ts index 046ced2f6..0c8189840 100644 --- a/packages/codepush/__mocks__/react-native.ts +++ b/packages/codepush/__mocks__/react-native.ts @@ -18,9 +18,18 @@ actualRN.NativeModules.DdSdk = { initialize: jest.fn().mockImplementation( () => new Promise(resolve => resolve()) ) as jest.MockedFunction, - setAttributes: jest.fn().mockImplementation( + addAttribute: jest.fn().mockImplementation( () => new Promise(resolve => resolve()) - ) as jest.MockedFunction, + ) as jest.MockedFunction, + removeAttribute: jest.fn().mockImplementation( + () => new Promise(resolve => resolve()) + ) as jest.MockedFunction, + addAttributes: jest.fn().mockImplementation( + () => new Promise(resolve => resolve()) + ) as jest.MockedFunction, + removeAttributes: jest.fn().mockImplementation( + () => new Promise(resolve => resolve()) + ) as jest.MockedFunction, setTrackingConsent: jest.fn().mockImplementation( () => new Promise(resolve => resolve()) ) as jest.MockedFunction, diff --git a/packages/core/__mocks__/react-native.ts b/packages/core/__mocks__/react-native.ts index 0e85e65ae..24e3f80c7 100644 --- a/packages/core/__mocks__/react-native.ts +++ b/packages/core/__mocks__/react-native.ts @@ -27,9 +27,18 @@ actualRN.NativeModules.DdSdk = { clearUserInfo: jest.fn().mockImplementation( () => new Promise(resolve => resolve()) ) as jest.MockedFunction, - setAttributes: jest.fn().mockImplementation( + addAttribute: jest.fn().mockImplementation( () => new Promise(resolve => resolve()) - ) as jest.MockedFunction, + ) as jest.MockedFunction, + removeAttribute: jest.fn().mockImplementation( + () => new Promise(resolve => resolve()) + ) as jest.MockedFunction, + addAttributes: jest.fn().mockImplementation( + () => new Promise(resolve => resolve()) + ) as jest.MockedFunction, + removeAttributes: jest.fn().mockImplementation( + () => new Promise(resolve => resolve()) + ) as jest.MockedFunction, setTrackingConsent: jest.fn().mockImplementation( () => new Promise(resolve => resolve()) ) as jest.MockedFunction, diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogSDKWrapper.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogSDKWrapper.kt index f781687eb..06151d834 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogSDKWrapper.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogSDKWrapper.kt @@ -89,11 +89,24 @@ internal class DatadogSDKWrapper : DatadogWrapper { override fun clearUserInfo() { Datadog.clearUserInfo() } + + override fun addRumGlobalAttribute(key: String, value: Any?) { + this.getRumMonitor().addAttribute(key, value) + } + + override fun removeRumGlobalAttribute(key: String) { + this.getRumMonitor().removeAttribute(key) + } override fun addRumGlobalAttributes(attributes: Map) { - val rumMonitor = this.getRumMonitor() for (attribute in attributes) { - rumMonitor.addAttribute(attribute.key, attribute.value) + this.addRumGlobalAttribute(attribute.key, attribute.value) + } + } + + override fun removeRumGlobalAttributes(keys: Array) { + for (key in keys) { + this.removeRumGlobalAttribute(key) } } diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogWrapper.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogWrapper.kt index 49d606b35..d6395b18b 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogWrapper.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogWrapper.kt @@ -91,6 +91,21 @@ interface DatadogWrapper { */ fun clearUserInfo() + + /** Adds a global attribute. + * + * @param key: Key that identifies the attribute. + * @param value: Value linked to the attribute. + */ + fun addRumGlobalAttribute(key: String, value: Any?) + + /** + * Removes a global attribute. + * + * @param key: Key that identifies the attribute. + */ + fun removeRumGlobalAttribute(key: String) + /** * Adds global attributes. * @@ -98,6 +113,13 @@ interface DatadogWrapper { */ fun addRumGlobalAttributes(attributes: Map) + /** + * Removes global attributes. + * + * @param keys Keys linked to the attributes to be removed + */ + fun removeRumGlobalAttributes(keys: Array) + /** * Sets tracking consent. */ diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt index 7adcf7438..ed545d9e1 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt @@ -14,6 +14,7 @@ import com.datadog.android.rum.configuration.VitalsUpdateFrequency import com.facebook.react.bridge.LifecycleEventListener import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.ReadableArray import com.facebook.react.bridge.ReadableMap import java.util.Locale import java.util.concurrent.TimeUnit @@ -66,11 +67,35 @@ class DdSdkImplementation( } /** - * Sets the global context (set of attributes) attached with all future Logs, Spans and RUM + * Sets a specific attribute in the global context attached with all future Logs, Spans and RUM. + * + * @param key: Key that identifies the attribute. + * @param value: Value linked to the attribute. + */ + fun addAttribute(key: String, value: ReadableMap, promise: Promise) { + val attributeValue = value.toMap()["value"] + datadog.addRumGlobalAttribute(key, attributeValue) + GlobalState.addAttribute(key, attributeValue) + promise.resolve(null) + } + + /** + * Removes an attribute from the global context attached with all future Logs, Spans and RUM events. + * @param key: They key associated with the attribute to be removed. + */ + fun removeAttribute(key: String, promise: Promise) { + datadog.removeRumGlobalAttribute(key) + GlobalState.removeAttribute(key) + promise.resolve(null) + } + + + /** + * Adds a set of attributes to the global context that is attached with all future Logs, Spans and RUM * events. - * @param attributes The global context attributes. + * @param attributes: The global context attributes. */ - fun setAttributes(attributes: ReadableMap, promise: Promise) { + fun addAttributes(attributes: ReadableMap, promise: Promise) { datadog.addRumGlobalAttributes(attributes.toHashMap()) for ((k,v) in attributes.toHashMap()) { GlobalState.addAttribute(k, v) @@ -78,6 +103,26 @@ class DdSdkImplementation( promise.resolve(null) } + /** + * Removes a set of attributes from the global context that is attached with all future Logs, Spans and RUM + * events. + * @param keys: They keys associated with the attributes to be removed. + */ + fun removeAttributes(keys: ReadableArray, promise: Promise) { + val keysArray = mutableListOf() + for (i in 0 until keys.size()) { + val key: String = keys.getString(i) + keysArray.add(key) + } + val keysStringArray = keysArray.toTypedArray() + + datadog.removeRumGlobalAttributes(keysStringArray) + for (key in keysStringArray) { + GlobalState.removeAttribute(key) + } + promise.resolve(null) + } + /** * Set the user information. * @param userInfo The user object (use builtin attributes: 'id', 'email', 'name', and any custom diff --git a/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdSdk.kt b/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdSdk.kt index 4e4668a3e..a9d430081 100644 --- a/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdSdk.kt +++ b/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdSdk.kt @@ -12,13 +12,14 @@ import com.facebook.react.bridge.LifecycleEventListener import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.ReactMethod +import com.facebook.react.bridge.ReadableArray import com.facebook.react.bridge.ReadableMap import com.facebook.react.modules.core.DeviceEventManagerModule /** The entry point to initialize Datadog's features. */ class DdSdk( reactContext: ReactApplicationContext, - datadogWrapper: DatadogWrapper = DatadogSDKWrapper() + datadogWrapper: DatadogWrapper = DatadogSDKWrapper(), ddTelemetry: DdTelemetry = DdTelemetry() ) : NativeDdSdkSpec(reactContext) { @@ -40,13 +41,43 @@ class DdSdk( } /** - * Sets the global context (set of attributes) attached with all future Logs, Spans and RUM + * Sets a specific attribute in the global context attached with all future Logs, Spans and RUM + * + * @param key: Key that identifies the attribute. + * @param value: Value linked to the attribute. + */ + @ReactMethod + override fun addAttribute(key: String, value: ReadableMap, promise: Promise) { + implementation.addAttribute(key, value, promise) + } + + /** + * Removes an attribute from the context attached with all future Logs, Spans and RUM events. + * @param key: They key associated with the attribute to be removed. + */ + @ReactMethod + override fun removeAttribute(key: String, promise: Promise) { + implementation.removeAttribute(key, promise) + } + + /** + * Adds a set of attributes to the global context that is attached with all future Logs, Spans and RUM * events. * @param attributes The global context attributes. */ @ReactMethod - override fun setAttributes(attributes: ReadableMap, promise: Promise) { - implementation.setAttributes(attributes, promise) + override fun addAttributes(attributes: ReadableMap, promise: Promise) { + implementation.addAttributes(attributes, promise) + } + + /** + * Removes a set of attributes from the global context that is attached with all future Logs, Spans and RUM + * events. + * @param keys: They keys associated with the attributes to be removed. + */ + @ReactMethod + override fun removeAttributes(keys: ReadableArray, promise: Promise) { + implementation.removeAttributes(keys, promise) } /** diff --git a/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt b/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt index 0ebdd37fb..958ba521b 100644 --- a/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt +++ b/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt @@ -12,6 +12,7 @@ import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.ReactContextBaseJavaModule import com.facebook.react.bridge.ReactMethod +import com.facebook.react.bridge.ReadableArray import com.facebook.react.bridge.ReadableMap /** The entry point to initialize Datadog's features. */ @@ -66,13 +67,43 @@ class DdSdk( } /** - * Sets the global context (set of attributes) attached with all future Logs, Spans and RUM + * Sets a specific attribute in the global context attached with all future Logs, Spans and RUM + * + * @param key: Key that identifies the attribute. + * @param value: Value linked to the attribute. + */ + @ReactMethod + fun addAttribute(key: String, value: ReadableMap, promise: Promise) { + implementation.addAttribute(key, value, promise) + } + + /** + * Removes an attribute from the context attached with all future Logs, Spans and RUM events. + * @param key: They key associated with the attribute to be removed. + */ + @ReactMethod + fun removeAttribute(key: String, promise: Promise) { + implementation.removeAttribute(key, promise) + } + + /** + * Adds a set of attributes to the global context that is attached with all future Logs, Spans and RUM * events. * @param attributes The global context attributes. */ @ReactMethod - fun setAttributes(attributes: ReadableMap, promise: Promise) { - implementation.setAttributes(attributes, promise) + fun addAttributes(attributes: ReadableMap, promise: Promise) { + implementation.addAttributes(attributes, promise) + } + + /** + * Removes a set of attributes from the global context that is attached with all future Logs, Spans and RUM + * events. + * @param keys: They keys associated with the attributes to be removed. + */ + @ReactMethod + fun removeAttributes(keys: ReadableArray, promise: Promise) { + implementation.removeAttributes(keys, promise) } /** diff --git a/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt b/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt index f917bb847..ae6ded89c 100644 --- a/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt +++ b/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt @@ -40,6 +40,7 @@ import com.datadog.tools.unit.setStaticValue import com.datadog.tools.unit.toReadableArray import com.datadog.tools.unit.toReadableJavaOnlyMap import com.datadog.tools.unit.toReadableMap +import com.facebook.react.bridge.JavaOnlyMap import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.ReadableMap @@ -78,7 +79,6 @@ import org.mockito.kotlin.doReturn import org.mockito.kotlin.doThrow import org.mockito.kotlin.eq import org.mockito.kotlin.inOrder -import org.mockito.kotlin.isNotNull import org.mockito.kotlin.isNull import org.mockito.kotlin.mock import org.mockito.kotlin.never @@ -2966,28 +2966,96 @@ internal class DdSdkTest { } @Test - fun `𝕄 set RUM attributes 𝕎 setAttributes`( + fun `M set Rum attribute W addAttribute`( + @StringForgery(type = StringForgeryType.NUMERICAL) key: String, + @StringForgery(type = StringForgeryType.ASCII) value: String + ) { + // When + val attributeMap = JavaOnlyMap().apply { + putString("value", value) + } + testedBridgeSdk.addAttribute(key, attributeMap, mockPromise) + + // Then + verify(mockDatadog).addRumGlobalAttribute(key, value) + } + + @Test + fun `M set GlobalState attribute W addAttribute`( + @StringForgery(type = StringForgeryType.NUMERICAL) key: String, + @StringForgery(type = StringForgeryType.ASCII) value: String + ) { + // When + val attributeMap = JavaOnlyMap().apply { + putString("value", value) + } + testedBridgeSdk.addAttribute(key, attributeMap, mockPromise) + + // Then + assertThat(GlobalState.globalAttributes).containsEntry(key, value) + } + + @Test + fun `M remove Rum attribute W removeAttribute`( + @StringForgery(type = StringForgeryType.NUMERICAL) key: String, + @StringForgery(type = StringForgeryType.ASCII) value: String + ) { + // Given + val attributeMap = JavaOnlyMap().apply { + putString("value", value) + } + testedBridgeSdk.addAttribute(key, attributeMap, mockPromise) + assertThat(GlobalState.globalAttributes).containsEntry(key, value) + + // When + testedBridgeSdk.removeAttribute(key, mockPromise) + + // Then + verify(mockDatadog).removeRumGlobalAttribute(key) + } + + @Test + fun `M remove GlobalState attribute W removeAttribute`( + @StringForgery(type = StringForgeryType.NUMERICAL) key: String, + @StringForgery(type = StringForgeryType.ASCII) value: String + ) { + // Given + val attributeMap = JavaOnlyMap().apply { + putString("value", value) + } + testedBridgeSdk.addAttribute(key, attributeMap, mockPromise) + assertThat(GlobalState.globalAttributes).containsEntry(key, value) + + // When + testedBridgeSdk.removeAttribute(key, mockPromise) + + // Then + assertThat(GlobalState.globalAttributes).doesNotContainEntry(key, value) + } + + @Test + fun `𝕄 set RUM attributes 𝕎 addAttributes`( @MapForgery( key = AdvancedForgery(string = [StringForgery(StringForgeryType.NUMERICAL)]), value = AdvancedForgery(string = [StringForgery(StringForgeryType.ASCII)]) ) customAttributes: Map ) { // When - testedBridgeSdk.setAttributes(customAttributes.toReadableMap(), mockPromise) + testedBridgeSdk.addAttributes(customAttributes.toReadableMap(), mockPromise) // Then verify(mockDatadog).addRumGlobalAttributes(customAttributes) } @Test - fun `𝕄 set GlobalState attributes 𝕎 setAttributes`( + fun `𝕄 set GlobalState attributes 𝕎 addAttributes`( @MapForgery( key = AdvancedForgery(string = [StringForgery(StringForgeryType.NUMERICAL)]), value = AdvancedForgery(string = [StringForgery(StringForgeryType.ASCII)]) ) customAttributes: Map ) { // When - testedBridgeSdk.setAttributes(customAttributes.toReadableMap(), mockPromise) + testedBridgeSdk.addAttributes(customAttributes.toReadableMap(), mockPromise) // Then customAttributes.forEach { (k, v) -> @@ -2995,6 +3063,46 @@ internal class DdSdkTest { } } + @Test + fun `𝕄 remove RUM attributes 𝕎 removeAttributes`( + @MapForgery( + key = AdvancedForgery(string = [StringForgery(StringForgeryType.NUMERICAL)]), + value = AdvancedForgery(string = [StringForgery(StringForgeryType.ASCII)]) + ) customAttributes: Map + ) { + // Given + testedBridgeSdk.addAttributes(customAttributes.toReadableMap(), mockPromise) + verify(mockDatadog).addRumGlobalAttributes(customAttributes) + + // When + val keys = customAttributes.keys.toReadableArray() + testedBridgeSdk.removeAttributes(keys, mockPromise) + + // Then + verify(mockDatadog).removeRumGlobalAttributes(customAttributes.keys.toTypedArray()) + } + + @Test + fun `𝕄 remve GlobalState attributes 𝕎 removeAttributes`( + @MapForgery( + key = AdvancedForgery(string = [StringForgery(StringForgeryType.NUMERICAL)]), + value = AdvancedForgery(string = [StringForgery(StringForgeryType.ASCII)]) + ) customAttributes: Map + ) { + // Given + testedBridgeSdk.addAttributes(customAttributes.toReadableMap(), mockPromise) + verify(mockDatadog).addRumGlobalAttributes(customAttributes) + + // When + val keys = customAttributes.keys.toReadableArray() + testedBridgeSdk.removeAttributes(keys, mockPromise) + + // Then + customAttributes.forEach { (k, v) -> + assertThat(GlobalState.globalAttributes).doesNotContainEntry(k, v) + } + } + @Test fun `𝕄 build Granted consent 𝕎 buildTrackingConsent {granted}`(forge: Forge) { // When diff --git a/packages/core/ios/Sources/AnyEncodable.swift b/packages/core/ios/Sources/AnyEncodable.swift index 39821af87..7fac7bb3b 100644 --- a/packages/core/ios/Sources/AnyEncodable.swift +++ b/packages/core/ios/Sources/AnyEncodable.swift @@ -14,18 +14,25 @@ internal func castAttributesToSwift(_ attributes: [String: Any]) -> [String: Enc var casted: [String: Encodable] = [:] attributes.forEach { key, value in - if let castedValue = castByPreservingTypeInformation(attributeValue: value) { - // If possible, cast attribute by preserving its type information - casted[key] = castedValue - } else { - // Otherwise, cast by preserving its encoded value (and loosing type information) - casted[key] = castByPreservingEncodedValue(attributeValue: value) - } + casted[key] = castValueToSwift(value) } return casted } +internal func castValueToSwift(_ value: Any) -> Encodable { + var casted: Encodable + if let castedValue = castByPreservingTypeInformation(attributeValue: value) { + // If possible, cast attribute by preserving its type information + casted = castedValue + } else { + // Otherwise, cast by preserving its encoded value (and loosing type information) + casted = castByPreservingEncodedValue(attributeValue: value) + } + + return casted +} + /// Casts `Any` value to `Encodable` by preserving its type information. private func castByPreservingTypeInformation(attributeValue: Any) -> Encodable? { switch attributeValue { diff --git a/packages/core/ios/Sources/DdSdk.mm b/packages/core/ios/Sources/DdSdk.mm index 674cd0fbc..06736a69e 100644 --- a/packages/core/ios/Sources/DdSdk.mm +++ b/packages/core/ios/Sources/DdSdk.mm @@ -30,11 +30,33 @@ + (void)initFromNative { [self initialize:configuration resolve:resolve reject:reject]; } -RCT_REMAP_METHOD(setAttributes, withAttributes:(NSDictionary*)attributes +RCT_EXPORT_METHOD(addAttribute:(NSString*) key + withValue:(NSDictionary*) value + withResolver:(RCTPromiseResolveBlock)resolve + withRejecter:(RCTPromiseRejectBlock)reject) +{ + [self addAttribute:key value:value resolve:resolve reject:reject]; +} + +RCT_EXPORT_METHOD(removeAttribute:(NSString*) key + withResolver:(RCTPromiseResolveBlock)resolve + withRejecter:(RCTPromiseRejectBlock)reject) +{ + [self removeAttribute:key resolve:resolve reject:reject]; +} + +RCT_REMAP_METHOD(addAttributes, withAttributes:(NSDictionary*)attributes withResolver:(RCTPromiseResolveBlock)resolve withRejecter:(RCTPromiseRejectBlock)reject) { - [self setAttributes:attributes resolve:resolve reject:reject]; + [self addAttributes:attributes resolve:resolve reject:reject]; +} + +RCT_REMAP_METHOD(removeAttributes, withKeys:(NSArray *)keys + withResolver:(RCTPromiseResolveBlock)resolve + withRejecter:(RCTPromiseRejectBlock)reject) +{ + [self removeAttributes:keys resolve:resolve reject:reject]; } RCT_REMAP_METHOD(setUserInfo, withUserInfo:(NSDictionary*)userInfo @@ -137,8 +159,20 @@ - (void)initialize:(NSDictionary *)configuration resolve:(RCTPromiseResolveBlock [self.ddSdkImplementation initializeWithConfiguration:configuration resolve:resolve reject:reject]; } -- (void)setAttributes:(NSDictionary *)attributes resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { - [self.ddSdkImplementation setAttributesWithAttributes:attributes resolve:resolve reject:reject]; +- (void)addAttribute:(NSString *)key value:(NSDictionary *)value resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + [self.ddSdkImplementation addAttributeWithKey:key value:value resolve:resolve reject:reject]; +} + +- (void)removeAttribute:(NSString *)key resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + [self.ddSdkImplementation removeAttributeWithKey:key resolve:resolve reject:reject]; +} + +- (void)addAttributes:(NSDictionary *)attributes resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + [self.ddSdkImplementation addAttributesWithAttributes:attributes resolve:resolve reject:reject]; +} + +- (void)removeAttributes:(NSArray *)keys resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + [self.ddSdkImplementation removeAttributesWithKeys:keys resolve:resolve reject:reject]; } - (void)setTrackingConsent:(NSString *)trackingConsent resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { diff --git a/packages/core/ios/Sources/DdSdkImplementation.swift b/packages/core/ios/Sources/DdSdkImplementation.swift index 03c630b70..8aae5e10d 100644 --- a/packages/core/ios/Sources/DdSdkImplementation.swift +++ b/packages/core/ios/Sources/DdSdkImplementation.swift @@ -75,14 +75,43 @@ public class DdSdkImplementation: NSObject { resolve(nil) } + + @objc + public func addAttribute(key: AttributeKey, value: NSDictionary, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { + if let attributeValue = value.object(forKey: "value") { + let castedValue = castValueToSwift(attributeValue) + RUMMonitorProvider().addAttribute(forKey: key, value: castedValue) + GlobalState.addAttribute(forKey: key, value: castedValue) + } + + resolve(nil) + } + + @objc + public func removeAttribute(key: AttributeKey, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { + RUMMonitorProvider().removeAttribute(forKey: key) + GlobalState.removeAttribute(key: key) + + resolve(nil) + } @objc - public func setAttributes(attributes: NSDictionary, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { + public func addAttributes(attributes: NSDictionary, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { let castedAttributes = castAttributesToSwift(attributes) for (key, value) in castedAttributes { RUMMonitorProvider().addAttribute(forKey: key, value: value) GlobalState.addAttribute(forKey: key, value: value) } + + resolve(nil) + } + + @objc + public func removeAttributes(keys: [AttributeKey], resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { + RUMMonitorProvider().removeAttributes(forKeys: keys) + for (key) in keys { + GlobalState.removeAttribute(key: key) + } resolve(nil) } diff --git a/packages/core/ios/Sources/GlobalState.swift b/packages/core/ios/Sources/GlobalState.swift index b932803a1..a758bf0ef 100644 --- a/packages/core/ios/Sources/GlobalState.swift +++ b/packages/core/ios/Sources/GlobalState.swift @@ -15,7 +15,7 @@ internal struct GlobalState { } internal static func removeAttribute(key: String) { - GlobalState.globalAttributes.removeValue(forKey: key) + GlobalState.globalAttributes[key] = nil } } diff --git a/packages/core/ios/Tests/DdSdkTests.swift b/packages/core/ios/Tests/DdSdkTests.swift index efbf57b96..adbb57da9 100644 --- a/packages/core/ios/Tests/DdSdkTests.swift +++ b/packages/core/ios/Tests/DdSdkTests.swift @@ -651,7 +651,7 @@ class DdSdkTests: XCTestCase { XCTFail("extra-info-4 is not of expected type or value") } } - + func testClearUserInfo() throws { let bridge = DdSdkImplementation( mainDispatchQueue: DispatchQueueMock(), @@ -704,12 +704,12 @@ class DdSdkTests: XCTestCase { } else { XCTFail("extra-info-4 is not of expected type or value") } - + bridge.clearUserInfo(resolve: mockResolve, reject: mockReject) - + ddContext = try XCTUnwrap(CoreRegistry.default as? DatadogCore).contextProvider.read() userInfo = try XCTUnwrap(ddContext.userInfo) - + XCTAssertEqual(userInfo.id, nil) XCTAssertEqual(userInfo.name, nil) XCTAssertEqual(userInfo.email, nil) @@ -719,7 +719,59 @@ class DdSdkTests: XCTestCase { XCTAssertEqual(userInfo.extraInfo["extra-info-4"] as? [String: Int], nil) } - func testSettingAttributes() { + func testRemovingAttribute() { + let rumMonitorMock = MockRUMMonitor() + let bridge = DdSdkImplementation( + mainDispatchQueue: DispatchQueueMock(), + jsDispatchQueue: DispatchQueueMock(), + jsRefreshRateMonitor: JSRefreshRateMonitor(), + RUMMonitorProvider: { rumMonitorMock }, + RUMMonitorInternalProvider: { nil } + ) + + bridge.initialize( + configuration: .mockAny(), + resolve: mockResolve, + reject: mockReject + ) + + bridge.addAttributes( + attributes: NSDictionary( + dictionary: [ + "attribute-1": 123, + "attribute-2": "abc", + ] + ), + resolve: mockResolve, + reject: mockReject + ) + + XCTAssertEqual(rumMonitorMock.addedAttributes["attribute-1"] as? Int64, 123) + XCTAssertEqual(rumMonitorMock.addedAttributes["attribute-2"] as? String, "abc") + + XCTAssertEqual(GlobalState.globalAttributes["attribute-1"] as? Int64, 123) + XCTAssertEqual(GlobalState.globalAttributes["attribute-2"] as? String, "abc") + + bridge.removeAttribute(key: "attribute-1", resolve: mockResolve, reject: mockReject) + + XCTAssertEqual(rumMonitorMock.addedAttributes["attribute-1"] as? Int64, nil) + XCTAssertEqual(rumMonitorMock.addedAttributes["attribute-2"] as? String, "abc") + + XCTAssertEqual(GlobalState.globalAttributes["attribute-1"] as? Int64, nil) + XCTAssertEqual(GlobalState.globalAttributes["attribute-2"] as? String, "abc") + + bridge.removeAttribute(key: "attribute-2", resolve: mockResolve, reject: mockReject) + + XCTAssertEqual(rumMonitorMock.addedAttributes["attribute-1"] as? Int64, nil) + XCTAssertEqual(rumMonitorMock.addedAttributes["attribute-2"] as? String, nil) + + XCTAssertEqual(GlobalState.globalAttributes["attribute-1"] as? Int64, nil) + XCTAssertEqual(GlobalState.globalAttributes["attribute-2"] as? String, nil) + + GlobalState.globalAttributes.removeAll() + } + + func testAddingAttributes() { let rumMonitorMock = MockRUMMonitor() let bridge = DdSdkImplementation( mainDispatchQueue: DispatchQueueMock(), @@ -734,7 +786,7 @@ class DdSdkTests: XCTestCase { reject: mockReject ) - bridge.setAttributes( + bridge.addAttributes( attributes: NSDictionary( dictionary: [ "attribute-1": 123, @@ -757,6 +809,66 @@ class DdSdkTests: XCTestCase { GlobalState.globalAttributes.removeAll() } + func testRemovingAttributes() { + let rumMonitorMock = MockRUMMonitor() + let bridge = DdSdkImplementation( + mainDispatchQueue: DispatchQueueMock(), + jsDispatchQueue: DispatchQueueMock(), + jsRefreshRateMonitor: JSRefreshRateMonitor(), + RUMMonitorProvider: { rumMonitorMock }, + RUMMonitorInternalProvider: { nil } + ) + bridge.initialize( + configuration: .mockAny(), + resolve: mockResolve, + reject: mockReject + ) + + bridge.addAttributes( + attributes: NSDictionary( + dictionary: [ + "attribute-1": 123, + "attribute-2": "abc", + "attribute-3": true, + ] + ), + resolve: mockResolve, + reject: mockReject + ) + + XCTAssertEqual(rumMonitorMock.addedAttributes["attribute-1"] as? Int64, 123) + XCTAssertEqual(rumMonitorMock.addedAttributes["attribute-2"] as? String, "abc") + XCTAssertEqual(rumMonitorMock.addedAttributes["attribute-3"] as? Bool, true) + + XCTAssertEqual(GlobalState.globalAttributes["attribute-1"] as? Int64, 123) + XCTAssertEqual(GlobalState.globalAttributes["attribute-2"] as? String, "abc") + XCTAssertEqual(GlobalState.globalAttributes["attribute-3"] as? Bool, true) + + bridge.removeAttributes( + keys: ["attribute-1", "attribute-2"], resolve: mockResolve, reject: mockReject) + + XCTAssertEqual(rumMonitorMock.addedAttributes["attribute-1"] as? Int64, nil) + XCTAssertEqual(rumMonitorMock.addedAttributes["attribute-2"] as? String, nil) + XCTAssertEqual(rumMonitorMock.addedAttributes["attribute-3"] as? Bool, true) + + XCTAssertEqual(GlobalState.globalAttributes["attribute-1"] as? Int64, nil) + XCTAssertEqual(GlobalState.globalAttributes["attribute-2"] as? String, nil) + XCTAssertEqual(GlobalState.globalAttributes["attribute-3"] as? Bool, true) + + bridge.removeAttributes(keys: ["attribute-3"], resolve: mockResolve, reject: mockReject) + + XCTAssertEqual(rumMonitorMock.addedAttributes["attribute-1"] as? Int64, nil) + XCTAssertEqual(rumMonitorMock.addedAttributes["attribute-2"] as? String, nil) + XCTAssertEqual(rumMonitorMock.addedAttributes["attribute-3"] as? Bool, nil) + + XCTAssertEqual(GlobalState.globalAttributes["attribute-1"] as? Int64, nil) + XCTAssertEqual(GlobalState.globalAttributes["attribute-2"] as? String, nil) + XCTAssertEqual(GlobalState.globalAttributes["attribute-3"] as? Bool, nil) + + GlobalState.globalAttributes.removeAll() + + } + func testBuildLongTaskThreshold() { let configuration: DdSdkConfiguration = .mockAny(nativeLongTaskThresholdMs: 2500) diff --git a/packages/core/ios/Tests/MockRUMMonitor.swift b/packages/core/ios/Tests/MockRUMMonitor.swift index f0fa03364..3a882ed47 100644 --- a/packages/core/ios/Tests/MockRUMMonitor.swift +++ b/packages/core/ios/Tests/MockRUMMonitor.swift @@ -38,14 +38,20 @@ internal class MockRUMMonitor: RUMMonitorProtocol { addedAttributes[key] = value } - func removeAttribute(forKey key: DatadogInternal.AttributeKey) {} + func removeAttribute(forKey key: DatadogInternal.AttributeKey) { + addedAttributes.removeValue(forKey: key) + } func addAttributes(_ attributes: [DatadogInternal.AttributeKey : any DatadogInternal.AttributeValue]) { - // Not implemented + for (key, value) in attributes { + addAttribute(forKey: key, value: value) + } } func removeAttributes(forKeys keys: [DatadogInternal.AttributeKey]) { - // Not implemented + for key in keys { + removeAttribute(forKey: key) + } } var debug: Bool diff --git a/packages/core/jest/mock.js b/packages/core/jest/mock.js index 8e154c4cd..c49d13f48 100644 --- a/packages/core/jest/mock.js +++ b/packages/core/jest/mock.js @@ -36,7 +36,16 @@ module.exports = { clearUserInfo: jest .fn() .mockImplementation(() => new Promise(resolve => resolve())), - setAttributes: jest + addAttribute: jest + .fn() + .mockImplementation(() => new Promise(resolve => resolve())), + removeAttribute: jest + .fn() + .mockImplementation(() => new Promise(resolve => resolve())), + addAttributes: jest + .fn() + .mockImplementation(() => new Promise(resolve => resolve())), + removeAttributes: jest .fn() .mockImplementation(() => new Promise(resolve => resolve())), setTrackingConsent: jest diff --git a/packages/core/src/DdSdkReactNative.tsx b/packages/core/src/DdSdkReactNative.tsx index 1838542df..8360a695b 100644 --- a/packages/core/src/DdSdkReactNative.tsx +++ b/packages/core/src/DdSdkReactNative.tsx @@ -175,20 +175,61 @@ export class DdSdkReactNative { ); }; + /** + * Adds a specific attribute to the global context attached with all future Logs, Spans and RUM. + * @param key: Key that identifies the attribute. + * @param value: Value linked to the attribute. + */ + static addAttribute = async ( + key: string, + value: unknown + ): Promise => { + InternalLog.log( + `Adding attribute ${JSON.stringify(value)} for key ${key}`, + SdkVerbosity.DEBUG + ); + await DdSdk.addAttribute(key, { value }); + AttributesSingleton.getInstance().addAttribute(key, value); + }; + + /** + * Removes an attribute from the context attached with all future Logs, Spans and RUM events. + * @param key: They key associated with the attribute to be removed. + */ + static removeAttribute = async (key: string): Promise => { + InternalLog.log( + `Removing attribute for key ${key}`, + SdkVerbosity.DEBUG + ); + await DdSdk.removeAttribute(key); + AttributesSingleton.getInstance().removeAttribute(key); + }; + /** * Adds a set of attributes to the global context attached with all future Logs, Spans and RUM events. - * To remove an attribute, set it to `undefined` in a call to `setAttributes`. * @param attributes: The global context attributes. * @returns a Promise. */ - // eslint-disable-next-line @typescript-eslint/ban-types - static setAttributes = async (attributes: Attributes): Promise => { + static addAttributes = async (attributes: Attributes): Promise => { + InternalLog.log( + `Adding attributes ${JSON.stringify(attributes)}`, + SdkVerbosity.DEBUG + ); + await DdSdk.addAttributes(attributes); + AttributesSingleton.getInstance().addAttributes(attributes); + }; + + /** + * Removes a set of attributes from the context attached with all future Logs, Spans and RUM events. + * @param keys: They keys associated with the attributes to be removed. + */ + static removeAttributes = async (keys: string[]): Promise => { InternalLog.log( - `Setting attributes ${JSON.stringify(attributes)}`, + `Removing attributes for keys ${JSON.stringify(keys)}`, SdkVerbosity.DEBUG ); - await DdSdk.setAttributes(attributes); - AttributesSingleton.getInstance().setAttributes(attributes); + await DdSdk.removeAttributes(keys); + AttributesSingleton.getInstance().removeAttributes(keys); }; /** diff --git a/packages/core/src/__tests__/DdSdkReactNative.test.tsx b/packages/core/src/__tests__/DdSdkReactNative.test.tsx index 57ff5d0ed..5e6f8c447 100644 --- a/packages/core/src/__tests__/DdSdkReactNative.test.tsx +++ b/packages/core/src/__tests__/DdSdkReactNative.test.tsx @@ -62,7 +62,7 @@ beforeEach(async () => { GlobalState.instance.isInitialized = false; DdSdkReactNative['wasAutoInstrumented'] = false; NativeModules.DdSdk.initialize.mockClear(); - NativeModules.DdSdk.setAttributes.mockClear(); + NativeModules.DdSdk.addAttributes.mockClear(); NativeModules.DdSdk.setTrackingConsent.mockClear(); NativeModules.DdSdk.onRUMSessionStarted.mockClear(); @@ -1045,24 +1045,80 @@ describe('DdSdkReactNative', () => { }); }); - describe('setAttributes', () => { - it('calls SDK method when setAttributes', async () => { + describe('addAttribute', () => { + it('calls SDK method when addAttribute', async () => { + // GIVEN + const key = 'foo'; + const value = 'bar'; + + // WHEN + + await DdSdkReactNative.addAttribute(key, value); + + // THEN + expect(DdSdk.addAttribute).toHaveBeenCalledTimes(1); + expect(DdSdk.addAttribute).toHaveBeenCalledWith(key, { value }); + expect(AttributesSingleton.getInstance().getAttribute(key)).toEqual( + value + ); + }); + }); + + describe('removeAttribute', () => { + it('calls SDK method when removeAttribute', async () => { + // GIVEN + const key = 'foo'; + const value = 'bar'; + await DdSdkReactNative.addAttribute(key, value); + + // WHEN + await DdSdkReactNative.removeAttribute(key); + + // THEN + expect(DdSdk.removeAttribute).toHaveBeenCalledTimes(1); + expect(DdSdk.removeAttribute).toHaveBeenCalledWith(key); + expect(AttributesSingleton.getInstance().getAttribute(key)).toEqual( + undefined + ); + }); + }); + + describe('addAttributes', () => { + it('calls SDK method when addAttributes', async () => { // GIVEN const attributes = { foo: 'bar' }; // WHEN - await DdSdkReactNative.setAttributes(attributes); + await DdSdkReactNative.addAttributes(attributes); // THEN - expect(DdSdk.setAttributes).toHaveBeenCalledTimes(1); - expect(DdSdk.setAttributes).toHaveBeenCalledWith(attributes); + expect(DdSdk.addAttributes).toHaveBeenCalledTimes(1); + expect(DdSdk.addAttributes).toHaveBeenCalledWith(attributes); expect(AttributesSingleton.getInstance().getAttributes()).toEqual({ foo: 'bar' }); }); }); + describe('removeAttributes', () => { + it('calls SDK method when removeAttributes', async () => { + // GIVEN + const attributes = { foo: 'bar', baz: 'quux' }; + await DdSdkReactNative.addAttributes(attributes); + + // WHEN + await DdSdkReactNative.removeAttributes(['foo', 'baz']); + + // THEN + expect(DdSdk.removeAttributes).toHaveBeenCalledTimes(1); + expect(DdSdk.removeAttributes).toHaveBeenCalledWith(['foo', 'baz']); + expect(AttributesSingleton.getInstance().getAttributes()).toEqual( + {} + ); + }); + }); + describe('setUserInfo', () => { it('calls SDK method when setUserInfo, and sets the user in UserProvider', async () => { // GIVEN diff --git a/packages/core/src/sdk/AttributesSingleton/AttributesSingleton.ts b/packages/core/src/sdk/AttributesSingleton/AttributesSingleton.ts index a51bb6c99..ac92c2d32 100644 --- a/packages/core/src/sdk/AttributesSingleton/AttributesSingleton.ts +++ b/packages/core/src/sdk/AttributesSingleton/AttributesSingleton.ts @@ -9,13 +9,37 @@ import type { Attributes } from './types'; class AttributesProvider { private attributes: Attributes = {}; - setAttributes = (attributes: Attributes) => { + addAttribute = (key: string, value: unknown) => { + const newAttributes = { ...this.attributes }; + newAttributes[key] = value; + this.attributes = newAttributes; + }; + + removeAttribute = (key: string) => { + const updatedAttributes = { ...this.attributes }; + delete updatedAttributes[key]; + this.attributes = updatedAttributes; + }; + + addAttributes = (attributes: Attributes) => { this.attributes = { ...this.attributes, ...attributes }; }; + removeAttributes = (keys: string[]) => { + const updated = { ...this.attributes }; + for (const k of keys) { + delete updated[k]; + } + this.attributes = updated; + }; + + getAttribute = (key: string): unknown | undefined => { + return this.attributes[key]; + }; + getAttributes = (): Attributes => { return this.attributes; }; diff --git a/packages/core/src/sdk/AttributesSingleton/__tests__/AttributesSingleton.test.ts b/packages/core/src/sdk/AttributesSingleton/__tests__/AttributesSingleton.test.ts index 23fbe5ad7..90d1133b4 100644 --- a/packages/core/src/sdk/AttributesSingleton/__tests__/AttributesSingleton.test.ts +++ b/packages/core/src/sdk/AttributesSingleton/__tests__/AttributesSingleton.test.ts @@ -7,9 +7,12 @@ import { AttributesSingleton } from '../AttributesSingleton'; describe('AttributesSingleton', () => { - it('adds, returns and resets the user info', () => { - // Adding first attributes - AttributesSingleton.getInstance().setAttributes({ + beforeEach(() => { + AttributesSingleton.reset(); + }); + + it('adds, returns and resets the attributes', () => { + AttributesSingleton.getInstance().addAttributes({ appType: 'student', extraInfo: { loggedIn: true @@ -23,11 +26,8 @@ describe('AttributesSingleton', () => { } }); - // Removing and adding new attributes - AttributesSingleton.getInstance().setAttributes({ - appType: undefined, - newAttribute: false - }); + AttributesSingleton.getInstance().removeAttribute('appType'); + AttributesSingleton.getInstance().addAttribute('newAttribute', false); expect(AttributesSingleton.getInstance().getAttributes()).toEqual({ newAttribute: false, @@ -41,4 +41,48 @@ describe('AttributesSingleton', () => { expect(AttributesSingleton.getInstance().getAttributes()).toEqual({}); }); + + it('addAttribute sets a single key and getAttribute returns it', () => { + AttributesSingleton.getInstance().addAttribute('userId', '123'); + expect(AttributesSingleton.getInstance().getAttribute('userId')).toBe( + '123' + ); + expect(AttributesSingleton.getInstance().getAttributes()).toEqual({ + userId: '123' + }); + }); + + it('removeAttribute removes a single key and leaves others intact', () => { + AttributesSingleton.getInstance().addAttributes({ + a: 1, + b: 2 + }); + + AttributesSingleton.getInstance().removeAttribute('a'); + + expect( + AttributesSingleton.getInstance().getAttribute('a') + ).toBeUndefined(); + expect(AttributesSingleton.getInstance().getAttributes()).toEqual({ + b: 2 + }); + }); + + it('removeAttributes removes multiple keys (missing keys are ignored)', () => { + AttributesSingleton.getInstance().addAttributes({ + keyToKeep: 'yes', + keyToRemove1: true, + keyToRemove2: false + }); + + AttributesSingleton.getInstance().removeAttributes([ + 'keyToRemove1', + 'keyToRemove2', + 'keyToIgnore' + ]); + + expect(AttributesSingleton.getInstance().getAttributes()).toEqual({ + keyToKeep: 'yes' + }); + }); }); diff --git a/packages/core/src/specs/NativeDdSdk.ts b/packages/core/src/specs/NativeDdSdk.ts index a2ce1120e..70401fe3c 100644 --- a/packages/core/src/specs/NativeDdSdk.ts +++ b/packages/core/src/specs/NativeDdSdk.ts @@ -26,10 +26,29 @@ export interface Spec extends TurboModule { initialize(configuration: Object): Promise; /** - * Sets the global context (set of attributes) attached with all future Logs, Spans and RUM events. + * Adds a specific attribute to the global context attached with all future Logs, Spans and RUM. + * @param key: Key that identifies the attribute. + * @param value: Value linked to the attribute. + */ + addAttribute(key: string, value: Object): Promise; + + /** + * Removes an attribute from the context attached with all future Logs, Spans and RUM events. + * @param key: They key associated with the attribute to be removed. + */ + removeAttribute(key: string): Promise; + + /** + * Adds the global context (set of attributes) attached with all future Logs, Spans and RUM events. * @param attributes: The global context attributes. */ - setAttributes(attributes: Object): Promise; + addAttributes(attributes: Object): Promise; + + /** + * Removes a set of attributes from the context attached with all future Logs, Spans and RUM events. + * @param keys: They keys associated with the attributes to be removed. + */ + removeAttributes(keys: string[]): Promise; /** * Set the user information. diff --git a/packages/core/src/types.tsx b/packages/core/src/types.tsx index bad19d429..c8d9821cc 100644 --- a/packages/core/src/types.tsx +++ b/packages/core/src/types.tsx @@ -83,11 +83,30 @@ export type DdSdkType = { */ initialize(configuration: DdSdkConfiguration): Promise; + /** + * Sets a specific attribute in the global context attached with all future Logs, Spans and RUM + * @param key: Key that identifies the attribute. + * @param value: Value linked to the attribute. + */ + addAttribute(key: string, value: object): Promise; + + /** + * Removes an attribute from the context attached with all future Logs, Spans and RUM events. + * @param key: They key associated with the attribute to be removed. + */ + removeAttribute(key: string): Promise; + /** * Sets the global context (set of attributes) attached with all future Logs, Spans and RUM events. * @param attributes: The global context attributes. */ - setAttributes(attributes: object): Promise; + addAttributes(attributes: object): Promise; + + /** + * Removes a set of attributes from the context attached with all future Logs, Spans and RUM events. + * @param keys: They keys associated with the attributes to be removed. + */ + removeAttributes(keys: string[]): Promise; /** * Sets the user information. diff --git a/packages/react-native-apollo-client/__mocks__/react-native.ts b/packages/react-native-apollo-client/__mocks__/react-native.ts index 046ced2f6..bbac607d3 100644 --- a/packages/react-native-apollo-client/__mocks__/react-native.ts +++ b/packages/react-native-apollo-client/__mocks__/react-native.ts @@ -18,9 +18,9 @@ actualRN.NativeModules.DdSdk = { initialize: jest.fn().mockImplementation( () => new Promise(resolve => resolve()) ) as jest.MockedFunction, - setAttributes: jest.fn().mockImplementation( + addAttributes: jest.fn().mockImplementation( () => new Promise(resolve => resolve()) - ) as jest.MockedFunction, + ) as jest.MockedFunction, setTrackingConsent: jest.fn().mockImplementation( () => new Promise(resolve => resolve()) ) as jest.MockedFunction, From 5b551df190de2fdfe391350dc921a7b60b62ad38 Mon Sep 17 00:00:00 2001 From: Sergio Barrio Date: Thu, 16 Oct 2025 11:01:21 +0200 Subject: [PATCH 075/410] JS refresh rate normalization --- .../reactnative/DdSdkImplementation.kt | 51 ++++++- .../com/datadog/reactnative/DdSdkTest.kt | 127 ++++++++++++++++++ .../ios/Sources/DdSdkImplementation.swift | 21 ++- packages/core/ios/Tests/DdSdkTests.swift | 108 +++++++++++++++ 4 files changed, 305 insertions(+), 2 deletions(-) diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt index ed545d9e1..574394a36 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt @@ -7,7 +7,10 @@ package com.datadog.reactnative import android.content.Context +import android.hardware.display.DisplayManager +import android.os.Build import android.util.Log +import android.view.Display import com.datadog.android.privacy.TrackingConsent import com.datadog.android.rum.RumPerformanceMetric import com.datadog.android.rum.configuration.VitalsUpdateFrequency @@ -19,6 +22,7 @@ import com.facebook.react.bridge.ReadableMap import java.util.Locale import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicBoolean +import kotlin.math.max /** The entry point to initialize Datadog's features. */ @Suppress("TooManyFunctions") @@ -293,9 +297,10 @@ class DdSdkImplementation( return { if (jsRefreshRateMonitoringEnabled && it > 0.0) { + val normalizedFrameTimeSeconds = normalizeFrameTime(it, appContext) datadog.getRumMonitor() ._getInternal() - ?.updatePerformanceMetric(RumPerformanceMetric.JS_FRAME_TIME, it) + ?.updatePerformanceMetric(RumPerformanceMetric.JS_FRAME_TIME, normalizedFrameTimeSeconds) } if (jsLongTasksMonitoringEnabled && it > @@ -308,6 +313,49 @@ class DdSdkImplementation( } } + /** + * Normalizes frameTime values so when are turned into FPS metrics they are normalized on a range of zero to 60fps. + * @param frameTimeSeconds: the frame time to normalize. In seconds. + * @param context: The current app context + * @param fpsBudget: The maximum fps under which the frame Time will be normalized [0-fpsBudget]. Defaults to 60Hz. + * @param deviceDisplayFps: The maximum fps supported by the device. If not provided it will be set from the value obtained from the app context. + */ + @Suppress("CyclomaticComplexMethod") + fun normalizeFrameTime( + frameTimeSeconds: Double, + context: Context, + fpsBudget: Double? = null, + deviceDisplayFps: Double? = null, + ) : Double { + val frameTimeMs = frameTimeSeconds * 1000.0 + val frameBudgetHz = fpsBudget ?: DEFAULT_REFRESH_HZ + val maxDeviceDisplayHz = deviceDisplayFps ?: getMaxDisplayRefreshRate(context) + ?: 60.0 + + val maxDeviceFrameTimeMs = 1000.0 / maxDeviceDisplayHz + val budgetFrameTimeMs = 1000.0 / frameBudgetHz + + if (listOf( + maxDeviceDisplayHz, frameTimeMs, frameBudgetHz, budgetFrameTimeMs, maxDeviceFrameTimeMs + ).any { !it.isFinite() || it <= 0.0 } + ) return 1.0 / DEFAULT_REFRESH_HZ + + + var normalizedFrameTimeMs = frameTimeMs / (maxDeviceFrameTimeMs / budgetFrameTimeMs) + + normalizedFrameTimeMs = max(normalizedFrameTimeMs, maxDeviceFrameTimeMs) + + return normalizedFrameTimeMs / 1000.0 // in seconds + } + + @Suppress("CyclomaticComplexMethod") + private fun getMaxDisplayRefreshRate(context: Context?): Double { + val dm = context?.getSystemService(Context.DISPLAY_SERVICE) as? DisplayManager ?: return 60.0 + val display: Display = dm.getDisplay(Display.DEFAULT_DISPLAY) ?: return DEFAULT_REFRESH_HZ + + return display.supportedModes.maxOf { it.refreshRate.toDouble() } + } + // endregion internal companion object { internal const val DEFAULT_APP_VERSION = "?" @@ -317,6 +365,7 @@ class DdSdkImplementation( internal const val DD_DROP_ACTION = "_dd.action.drop_action" internal const val MONITOR_JS_ERROR_MESSAGE = "Error monitoring JS refresh rate" internal const val PACKAGE_INFO_NOT_FOUND_ERROR_MESSAGE = "Error getting package info" + internal const val DEFAULT_REFRESH_HZ = 60.0 internal const val NAME = "DdSdk" } } diff --git a/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt b/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt index ae6ded89c..327d8ffc0 100644 --- a/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt +++ b/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt @@ -60,6 +60,7 @@ import java.util.Locale import java.util.stream.Stream import kotlin.time.Duration.Companion.seconds import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.data.Offset import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -3238,6 +3239,132 @@ internal class DdSdkTest { } } + @Test + fun `𝕄 normalize frameTime according to the device's refresh rate`() { + // 10 fps, 60Hz device, 60 fps budget -> 10 fps + var frameTimeSeconds = testedBridgeSdk.normalizeFrameTime( + frameTimeSeconds = 0.1, + context = mockContext, + fpsBudget = 60.0, + deviceDisplayFps = 60.0 + ) + assertThat(frameTimeSeconds).isEqualTo(0.1) + + // 30 fps, 60Hz device, 60 fps budget -> 30 fps + frameTimeSeconds = testedBridgeSdk.normalizeFrameTime( + frameTimeSeconds = 0.03, + context = mockContext, + fpsBudget = 60.0, + deviceDisplayFps = 60.0 + ) + assertThat(frameTimeSeconds).isEqualTo(0.03) + + // 60 fps, 60Hz device, 60 fps budget -> 60 fps + frameTimeSeconds = testedBridgeSdk.normalizeFrameTime( + frameTimeSeconds = 0.016, + context = mockContext, + fpsBudget = 60.0, + deviceDisplayFps = 60.0 + ) + assertThat(frameTimeSeconds).isEqualTo(0.016, Offset.offset(0.005)) + + // 60 fps, 120Hz device, 60 fps budget -> 30 fps + frameTimeSeconds = testedBridgeSdk.normalizeFrameTime( + frameTimeSeconds = 0.016, + context = mockContext, + fpsBudget = 60.0, + deviceDisplayFps = 120.0 + ) + assertThat(frameTimeSeconds).isEqualTo(0.032) + + // 120 fps, 120Hz device, 60 fps budget -> 60 fps + frameTimeSeconds = testedBridgeSdk.normalizeFrameTime( + frameTimeSeconds = 0.0083, + context = mockContext, + fpsBudget = 60.0, + deviceDisplayFps = 120.0 + ) + assertThat(frameTimeSeconds).isEqualTo(0.016, Offset.offset(0.005)) + + // 90 fps, 120Hz device, 60 fps budget -> 45 fps + frameTimeSeconds = testedBridgeSdk.normalizeFrameTime( + frameTimeSeconds = 0.0111, + context = mockContext, + fpsBudget = 60.0, + deviceDisplayFps = 120.0 + ) + assertThat(frameTimeSeconds).isEqualTo(0.0222, Offset.offset(0.001)) + + // 100 fps, 120Hz device, 60 fps budget -> 50 fps + frameTimeSeconds = testedBridgeSdk.normalizeFrameTime( + frameTimeSeconds = 0.01, + context = mockContext, + fpsBudget = 60.0, + deviceDisplayFps = 120.0 + ) + assertThat(frameTimeSeconds).isEqualTo(0.02, Offset.offset(0.001)) + + // 120 fps, 120Hz device, 120 fps budget -> 120 fps + frameTimeSeconds = testedBridgeSdk.normalizeFrameTime( + frameTimeSeconds = 0.0083, + context = mockContext, + fpsBudget = 120.0, + deviceDisplayFps = 120.0 + ) + assertThat(frameTimeSeconds).isEqualTo(0.0083, Offset.offset(0.001)) + + // 80 fps, 160Hz device, 60 fps budget -> 30 fps + frameTimeSeconds = testedBridgeSdk.normalizeFrameTime( + frameTimeSeconds = 0.0125, + context = mockContext, + fpsBudget = 60.0, + deviceDisplayFps = 160.0 + ) + assertThat(frameTimeSeconds).isEqualTo(0.033, Offset.offset(0.001)) + + // 160 fps, 160Hz device, 60 fps budget -> 60 fps + frameTimeSeconds = testedBridgeSdk.normalizeFrameTime( + frameTimeSeconds = 0.00625, + context = mockContext, + fpsBudget = 60.0, + deviceDisplayFps = 160.0 + ) + assertThat(frameTimeSeconds).isEqualTo(0.016, Offset.offset(0.001)) + + // Edge cases + frameTimeSeconds = testedBridgeSdk.normalizeFrameTime( + frameTimeSeconds = 0.0, + context = mockContext, + fpsBudget = 0.0, + deviceDisplayFps = 0.0 + ) + assertThat(frameTimeSeconds).isEqualTo(0.016, Offset.offset(0.001)) + + frameTimeSeconds = testedBridgeSdk.normalizeFrameTime( + frameTimeSeconds = 0.016, + context = mockContext, + fpsBudget = 0.0, + deviceDisplayFps = 0.0 + ) + assertThat(frameTimeSeconds).isEqualTo(0.016, Offset.offset(0.001)) + + frameTimeSeconds = testedBridgeSdk.normalizeFrameTime( + frameTimeSeconds = 0.016, + context = mockContext, + fpsBudget = 60.0, + deviceDisplayFps = 0.0 + ) + assertThat(frameTimeSeconds).isEqualTo(0.016, Offset.offset(0.001)) + + frameTimeSeconds = testedBridgeSdk.normalizeFrameTime( + frameTimeSeconds = 0.016, + context = mockContext, + fpsBudget = 0.0, + deviceDisplayFps = 60.0 + ) + assertThat(frameTimeSeconds).isEqualTo(0.016, Offset.offset(0.001)) + } + // endregion // region Internal diff --git a/packages/core/ios/Sources/DdSdkImplementation.swift b/packages/core/ios/Sources/DdSdkImplementation.swift index 8aae5e10d..437b5ee39 100644 --- a/packages/core/ios/Sources/DdSdkImplementation.swift +++ b/packages/core/ios/Sources/DdSdkImplementation.swift @@ -244,7 +244,8 @@ public class DdSdkImplementation: NSObject { // Leave JS thread ASAP to give as much time to JS engine work. sharedQueue.async { if (shouldRecordFrameTime) { - rumMonitorInternal.updatePerformanceMetric(at: now, metric: .jsFrameTimeSeconds, value: frameTime, attributes: [:]) + let normalizedFrameTimeSeconds = DdSdkImplementation.normalizeFrameTimeForDeviceRefreshRate(frameTime) + rumMonitorInternal.updatePerformanceMetric(at: now, metric: .jsFrameTimeSeconds, value: normalizedFrameTimeSeconds, attributes: [:]) } if (shouldRecordLongTask) { rumMonitorInternal.addLongTask(at: now, duration: frameTime, attributes: ["long_task.target": "javascript"]) @@ -254,5 +255,23 @@ public class DdSdkImplementation: NSObject { return frameTimeCallback } + + // Normalizes frameTime values so when they are turned into FPS metrics they are normalized on a range between 0 and fpsBudget. If fpsBudget is not provided it will default to 60hz. + public static func normalizeFrameTimeForDeviceRefreshRate(_ frameTime: Double, fpsBudget: Double? = nil, deviceDisplayFps: Double? = nil) -> Double { + let DEFAULT_REFRESH_HZ = 60.0 + let frameTimeMs: Double = frameTime * 1000.0 + let frameBudgetHz: Double = fpsBudget ?? DEFAULT_REFRESH_HZ + let maxDeviceDisplayHz = deviceDisplayFps ?? Double(UIScreen.main.maximumFramesPerSecond) + let maxDeviceFrameTimeMs = 1000.0 / maxDeviceDisplayHz + let budgetFrameTimeMs = 1000.0 / frameBudgetHz + + guard maxDeviceDisplayHz > 0, frameTimeMs.isFinite, frameTimeMs > 0, frameBudgetHz > 0, budgetFrameTimeMs.isFinite, budgetFrameTimeMs > 0, maxDeviceFrameTimeMs.isFinite, maxDeviceFrameTimeMs > 0 else { + return 1.0 / DEFAULT_REFRESH_HZ + } + + var normalizedFrameTimeMs = frameTimeMs / (maxDeviceFrameTimeMs / budgetFrameTimeMs) + normalizedFrameTimeMs = max(normalizedFrameTimeMs, maxDeviceFrameTimeMs) + return normalizedFrameTimeMs / 1000.0 // in seconds + } } diff --git a/packages/core/ios/Tests/DdSdkTests.swift b/packages/core/ios/Tests/DdSdkTests.swift index adbb57da9..4a5d13f2e 100644 --- a/packages/core/ios/Tests/DdSdkTests.swift +++ b/packages/core/ios/Tests/DdSdkTests.swift @@ -1181,6 +1181,114 @@ class DdSdkTests: XCTestCase { XCTAssertEqual(rumMonitorMock.receivedLongTasks.first?.value, 0.25) XCTAssertEqual(rumMonitorMock.lastReceivedPerformanceMetrics[.jsFrameTimeSeconds], 0.25) } + + func testFrameTimeNormalizationFromCallback() { + let mockRefreshRateMonitor = MockJSRefreshRateMonitor() + let rumMonitorMock = MockRUMMonitor() + + DdSdkImplementation( + mainDispatchQueue: DispatchQueueMock(), + jsDispatchQueue: DispatchQueueMock(), + jsRefreshRateMonitor: mockRefreshRateMonitor, + RUMMonitorProvider: { rumMonitorMock }, + RUMMonitorInternalProvider: { rumMonitorMock._internalMock } + ).initialize( + configuration: .mockAny( + longTaskThresholdMs: 200, + vitalsUpdateFrequency: "average" + ), + resolve: mockResolve, + reject: mockReject + ) + + XCTAssertTrue(mockRefreshRateMonitor.isStarted) + + // 10 fps + mockRefreshRateMonitor.executeFrameCallback(frameTime: 0.1) + sharedQueue.sync {} + XCTAssertEqual(rumMonitorMock.lastReceivedPerformanceMetrics[.jsFrameTimeSeconds], 0.1) + + // 30 fps + mockRefreshRateMonitor.executeFrameCallback(frameTime: 0.03) + sharedQueue.sync {} + XCTAssertEqual(rumMonitorMock.lastReceivedPerformanceMetrics[.jsFrameTimeSeconds], 0.03) + + // 45 fps + mockRefreshRateMonitor.executeFrameCallback(frameTime: 0.02) + sharedQueue.sync {} + XCTAssertEqual(rumMonitorMock.lastReceivedPerformanceMetrics[.jsFrameTimeSeconds], 0.02) + + // 60 fps + mockRefreshRateMonitor.executeFrameCallback(frameTime: 0.016) + sharedQueue.sync {} + XCTAssertEqual(rumMonitorMock.lastReceivedPerformanceMetrics[.jsFrameTimeSeconds]!, 0.016, accuracy: 0.001) + + // 90 fps + mockRefreshRateMonitor.executeFrameCallback(frameTime: 0.011) + sharedQueue.sync {} + XCTAssertEqual(rumMonitorMock.lastReceivedPerformanceMetrics[.jsFrameTimeSeconds]!, 0.016, accuracy: 0.001) + + // 120 fps + mockRefreshRateMonitor.executeFrameCallback(frameTime: 0.008) + sharedQueue.sync {} + XCTAssertEqual(rumMonitorMock.lastReceivedPerformanceMetrics[.jsFrameTimeSeconds]!, 0.016, accuracy: 0.001) + } + + func testFrameTimeNormalizationUtilityFunction() { + + // 10 fps, 60fps capable device, 60 fps budget -> Normalized to 10fps + var frameTimeSeconds = DdSdkImplementation.normalizeFrameTimeForDeviceRefreshRate(0.1, fpsBudget: 60.0, deviceDisplayFps: 60.0) + XCTAssertEqual(frameTimeSeconds, 0.1, accuracy: 0.01) + + // 30 fps, 60fps capable device, 60 fps budget -> Normalized to 30fps + frameTimeSeconds = DdSdkImplementation.normalizeFrameTimeForDeviceRefreshRate(0.03, fpsBudget: 60.0, deviceDisplayFps: 60.0) + XCTAssertEqual(frameTimeSeconds, 0.03, accuracy: 0.01) + + // 60 fps, 60fps capable device, 60 fps budget-> Normalized to 60fps + frameTimeSeconds = DdSdkImplementation.normalizeFrameTimeForDeviceRefreshRate(0.016, fpsBudget: 60.0, deviceDisplayFps: 60.0) + XCTAssertEqual(frameTimeSeconds, 0.016, accuracy: 0.01) + + // 60 fps, 120fps capable device, 60 fps budget -> Normalized to 30fps + frameTimeSeconds = DdSdkImplementation.normalizeFrameTimeForDeviceRefreshRate(0.016, fpsBudget: 60.0, deviceDisplayFps: 120.0) + XCTAssertEqual(frameTimeSeconds, 0.03, accuracy: 0.01) + + // 120 fps, 120fps capable device, 60 fps budget -> Normalized to 60fps + frameTimeSeconds = DdSdkImplementation.normalizeFrameTimeForDeviceRefreshRate(0.0083, fpsBudget: 60.0, deviceDisplayFps: 120.0) + XCTAssertEqual(frameTimeSeconds, 0.016, accuracy: 0.001) + + // 90 fps, 120fps capable device, 60 fps budget -> Normalized to 45fps + frameTimeSeconds = DdSdkImplementation.normalizeFrameTimeForDeviceRefreshRate(0.0111, fpsBudget: 60.0, deviceDisplayFps: 120.0) + XCTAssertEqual(frameTimeSeconds, 0.0222, accuracy: 0.001) + + // 100 fps, 120fps capable device, 60 fps budget -> Normalized to 50fps + frameTimeSeconds = DdSdkImplementation.normalizeFrameTimeForDeviceRefreshRate(0.01, fpsBudget: 60.0, deviceDisplayFps: 120.0) + XCTAssertEqual(frameTimeSeconds, 0.02, accuracy: 0.001) + + // 120 fps, 120fps capable device, 120 fps budget -> Normalized to 120fps + frameTimeSeconds = DdSdkImplementation.normalizeFrameTimeForDeviceRefreshRate(0.0083, fpsBudget: 120.0, deviceDisplayFps: 120.0) + XCTAssertEqual(frameTimeSeconds, 0.0083, accuracy: 0.001) + + // 80 fps, 160fps capable device, 60 fps budget -> Normalized to 30fps + frameTimeSeconds = DdSdkImplementation.normalizeFrameTimeForDeviceRefreshRate(0.0125, fpsBudget: 60.0, deviceDisplayFps: 160.0) + XCTAssertEqual(frameTimeSeconds, 0.033, accuracy: 0.001) + + // 160 fps, 160fps capable device, 60 fps budget -> Normalized to 60fps + frameTimeSeconds = DdSdkImplementation.normalizeFrameTimeForDeviceRefreshRate(0.00625, fpsBudget: 60.0, deviceDisplayFps: 160.0) + XCTAssertEqual(frameTimeSeconds, 0.016, accuracy: 0.001) + + // Edge cases + frameTimeSeconds = DdSdkImplementation.normalizeFrameTimeForDeviceRefreshRate(0, fpsBudget: 0, deviceDisplayFps: 0) + XCTAssertEqual(frameTimeSeconds, 0.016, accuracy: 0.001) + + frameTimeSeconds = DdSdkImplementation.normalizeFrameTimeForDeviceRefreshRate(0.016, fpsBudget: 0, deviceDisplayFps: 0) + XCTAssertEqual(frameTimeSeconds, 0.016, accuracy: 0.001) + + frameTimeSeconds = DdSdkImplementation.normalizeFrameTimeForDeviceRefreshRate(0.016, fpsBudget: 60.0, deviceDisplayFps: 0) + XCTAssertEqual(frameTimeSeconds, 0.016, accuracy: 0.001) + + frameTimeSeconds = DdSdkImplementation.normalizeFrameTimeForDeviceRefreshRate(0.016, fpsBudget: 0, deviceDisplayFps: 60.0) + XCTAssertEqual(frameTimeSeconds, 0.016, accuracy: 0.001) + } func testSDKInitializationWithCustomEndpoints() throws { let mockRefreshRateMonitor = MockJSRefreshRateMonitor() From 2918733e3d680fa9036ba62218ee400bb47aee98 Mon Sep 17 00:00:00 2001 From: Sergio Barrio Date: Fri, 10 Oct 2025 14:52:21 +0200 Subject: [PATCH 076/410] Expose view Attributes API --- packages/core/__mocks__/react-native.ts | 12 ++ .../reactnative/DdRumImplementation.kt | 44 ++++++ .../reactnative/DdSdkImplementation.kt | 2 - .../kotlin/com/datadog/reactnative/DdRum.kt | 38 ++++++ .../kotlin/com/datadog/reactnative/DdRum.kt | 38 ++++++ .../com/datadog/reactnative/DdRumTest.kt | 58 ++++++++ .../com/datadog/tools/unit/MockRumMonitor.kt | 17 +-- packages/core/ios/Sources/DdRum.mm | 47 ++++++- .../ios/Sources/DdRumImplementation.swift | 28 ++++ .../ios/Sources/DdSdkImplementation.swift | 129 ++++++++++++------ packages/core/ios/Tests/DdRumTests.swift | 59 ++++++++ packages/core/ios/Tests/MockRUMMonitor.swift | 38 +++--- packages/core/jest/mock.js | 12 ++ packages/core/src/rum/DdRum.ts | 45 ++++++ packages/core/src/rum/__tests__/DdRum.test.ts | 92 +++++++++++++ packages/core/src/rum/types.ts | 26 ++++ packages/core/src/specs/NativeDdRum.ts | 25 ++++ 17 files changed, 638 insertions(+), 72 deletions(-) diff --git a/packages/core/__mocks__/react-native.ts b/packages/core/__mocks__/react-native.ts index 24e3f80c7..73308f711 100644 --- a/packages/core/__mocks__/react-native.ts +++ b/packages/core/__mocks__/react-native.ts @@ -132,6 +132,18 @@ actualRN.NativeModules.DdRum = { addTiming: jest.fn().mockImplementation( () => new Promise(resolve => resolve()) ) as jest.MockedFunction, + addViewAttribute: jest.fn().mockImplementation( + () => new Promise(resolve => resolve()) + ) as jest.MockedFunction, + removeViewAttribute: jest.fn().mockImplementation( + () => new Promise(resolve => resolve()) + ) as jest.MockedFunction, + addViewAttributes: jest.fn().mockImplementation( + () => new Promise(resolve => resolve()) + ) as jest.MockedFunction, + removeViewAttributes: jest.fn().mockImplementation( + () => new Promise(resolve => resolve()) + ) as jest.MockedFunction, addViewLoadingTime: jest.fn().mockImplementation( () => new Promise(resolve => resolve()) ) as jest.MockedFunction, diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdRumImplementation.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdRumImplementation.kt index 013872c08..4e3cd416f 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdRumImplementation.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdRumImplementation.kt @@ -13,6 +13,7 @@ import com.datadog.android.rum.RumErrorSource import com.datadog.android.rum.RumResourceKind import com.datadog.android.rum.RumResourceMethod import com.facebook.react.bridge.Promise +import com.facebook.react.bridge.ReadableArray import com.facebook.react.bridge.ReadableMap import java.util.Locale @@ -248,6 +249,49 @@ class DdRumImplementation(private val datadog: DatadogWrapper = DatadogSDKWrappe promise.resolve(null) } + /** + * Adds a custom attribute to the active RUM View. It will be propagated to all future RUM events associated with the active View. + * @param key: key for this view attribute. + * @param value: value for this attribute. + */ + fun addViewAttribute(key: String, value: ReadableMap, promise: Promise) { + val attributeValue = value.toMap()["value"] + val attributes = mutableMapOf() + attributes[key] = attributeValue + datadog.getRumMonitor().addViewAttributes(attributes) + promise.resolve(null) + } + + /** + * Removes an attribute from the active RUM View. + * @param key: key for the attribute to be removed from the view. + */ + fun removeViewAttribute(key: String, promise: Promise) { + val keysToDelete: Collection = listOf(key) + datadog.getRumMonitor().removeViewAttributes(keysToDelete) + promise.resolve(null) + } + + /** + * Adds multiple attributes to the active RUM View. They will be propagated to all future RUM events associated with the active View. + * @param attributes: key/value object containing all attributes to be added to the view. + */ + fun addViewAttributes(attributes: ReadableMap, promise: Promise) { + datadog.getRumMonitor().addViewAttributes(attributes.toMap()) + promise.resolve(null) + } + + /** + * Removes multiple attributes from the active RUM View. + * @param keys: keys for the attributes to be removed from the view. + */ + fun removeViewAttributes(keys: ReadableArray, promise: Promise) { + val keysToDelete = (0 until keys.size()) + .mapNotNull { keys.getString(it) } + datadog.getRumMonitor().removeViewAttributes(keysToDelete) + promise.resolve(null) + } + /** * Adds the loading time of the view to the active view. * It is calculated as the difference between the current time and the start time of the view. diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt index 574394a36..6741e971e 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt @@ -143,8 +143,6 @@ class DdSdkImplementation( if (id != null) { datadog.setUserInfo(id, name, email, extraInfo) - } else { - // TO DO - Log warning? } promise.resolve(null) diff --git a/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdRum.kt b/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdRum.kt index ce8104685..6cb2b385b 100644 --- a/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdRum.kt +++ b/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdRum.kt @@ -9,6 +9,7 @@ package com.datadog.reactnative import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.ReactMethod +import com.facebook.react.bridge.ReadableArray import com.facebook.react.bridge.ReadableMap /** @@ -201,6 +202,43 @@ class DdRum( implementation.addTiming(name, promise) } + /** + * Adds a custom attribute to the active RUM View. It will be propagated to all future RUM events associated with the active View. + * @param key: key for this view attribute. + * @param value: value for this attribute. + */ + @ReactMethod + override fun addViewAttribute(key: String, value: ReadableMap, promise: Promise) { + implementation.addViewAttribute(key, value, promise) + } + + /** + * Removes an attribute from the active RUM View. + * @param key: key for the attribute to be removed from the view. + */ + @ReactMethod + override fun removeViewAttribute(key: String, promise: Promise) { + implementation.removeViewAttribute(key, promise) + } + + /** + * Adds multiple attributes to the active RUM View. They will be propagated to all future RUM events associated with the active View. + * @param attributes: key/value object containing all attributes to be added to the view. + */ + @ReactMethod + override fun addViewAttributes(attributes: ReadableMap, promise: Promise) { + implementation.addViewAttributes(attributes, promise) + } + + /** + * Removes multiple attributes from the active RUM View. + * @param keys: keys for the attributes to be removed from the view. + */ + @ReactMethod + override fun removeViewAttributes(keys: ReadableArray, promise: Promise) { + implementation.removeViewAttributes(keys, promise) + } + /** * Adds the loading time of the view to the active view. * It is calculated as the difference between the current time and the start time of the view. diff --git a/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdRum.kt b/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdRum.kt index 79742e854..a6c4965ea 100644 --- a/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdRum.kt +++ b/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdRum.kt @@ -10,6 +10,7 @@ import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.ReactContextBaseJavaModule import com.facebook.react.bridge.ReactMethod +import com.facebook.react.bridge.ReadableArray import com.facebook.react.bridge.ReadableMap /** @@ -192,6 +193,43 @@ class DdRum( implementation.addTiming(name, promise) } + /** + * Adds a custom attribute to the active RUM View. It will be propagated to all future RUM events associated with the active View. + * @param key: key for this view attribute. + * @param value: value for this attribute. + */ + @ReactMethod + fun addViewAttribute(key: String, value: ReadableMap, promise: Promise) { + implementation.addViewAttribute(key, value, promise) + } + + /** + * Removes an attribute from the active RUM View. + * @param key: key for the attribute to be removed from the view. + */ + @ReactMethod + fun removeViewAttribute(key: String, promise: Promise) { + implementation.removeViewAttribute(key, promise) + } + + /** + * Adds multiple attributes to the active RUM View. They will be propagated to all future RUM events associated with the active View. + * @param attributes: key/value object containing all attributes to be added to the view. + */ + @ReactMethod + fun addViewAttributes(attributes: ReadableMap, promise: Promise) { + implementation.addViewAttributes(attributes, promise) + } + + /** + * Removes multiple attributes from the active RUM View. + * @param keys: keys for the attributes to be removed from the view. + */ + @ReactMethod + fun removeViewAttributes(keys: ReadableArray, promise: Promise) { + implementation.removeViewAttributes(keys, promise) + } + /** * Adds the loading time of the view to the active view. * It is calculated as the difference between the current time and the start time of the view. diff --git a/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdRumTest.kt b/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdRumTest.kt index be1c57b3a..9619794df 100644 --- a/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdRumTest.kt +++ b/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdRumTest.kt @@ -13,13 +13,16 @@ import com.datadog.android.rum.RumMonitor import com.datadog.android.rum.RumResourceKind import com.datadog.android.rum.RumResourceMethod import com.datadog.tools.unit.forge.BaseConfigurator +import com.datadog.tools.unit.toReadableArray import com.datadog.tools.unit.toReadableMap import com.facebook.react.bridge.Promise import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.annotation.AdvancedForgery import fr.xgouchet.elmyr.annotation.BoolForgery import fr.xgouchet.elmyr.annotation.DoubleForgery import fr.xgouchet.elmyr.annotation.Forgery import fr.xgouchet.elmyr.annotation.IntForgery +import fr.xgouchet.elmyr.annotation.MapForgery import fr.xgouchet.elmyr.annotation.StringForgery import fr.xgouchet.elmyr.annotation.StringForgeryType import fr.xgouchet.elmyr.junit5.ForgeConfiguration @@ -456,6 +459,61 @@ internal class DdRumTest { verify(mockRumMonitor).addTiming(timing) } + @Test + fun `M call addViewAttribute W addViewAttribute()`( + @StringForgery key: String, + @StringForgery value: String + ) { + var attributeMap = mutableMapOf() + attributeMap.put("value", value) + + var attributes = mutableMapOf() + attributes.put(key, value) + + // When + testedDdRum.addViewAttribute(key, attributeMap.toReadableMap(), mockPromise) + + // Then + verify(mockRumMonitor).addViewAttributes(attributes) + } + + @Test + fun `M call removeViewAttribute W removeViewAttribute()`(@StringForgery key: String) { + // When + testedDdRum.removeViewAttribute(key, mockPromise) + + // Then + verify(mockRumMonitor).removeViewAttributes(listOf(key)) + } + + @Test + fun `M call addViewAttributes W addViewAttributes()`( + @MapForgery( + key = AdvancedForgery(string = [StringForgery(StringForgeryType.NUMERICAL)]), + value = AdvancedForgery(string = [StringForgery(StringForgeryType.ASCII)]) + ) customAttributes: Map + ) { + // When + testedDdRum.addViewAttributes(customAttributes.toReadableMap(), mockPromise) + + // Then + verify(mockRumMonitor).addViewAttributes(customAttributes) + } + + @Test + fun `𝕄 call removeViewAttributes 𝕎 removeViewAttributes`( + @MapForgery( + key = AdvancedForgery(string = [StringForgery(StringForgeryType.NUMERICAL)]), + value = AdvancedForgery(string = [StringForgery(StringForgeryType.ASCII)]) + ) customAttributes: Map + ) { + // When + testedDdRum.removeViewAttributes(customAttributes.keys.toReadableArray(), mockPromise) + + // Then + verify(mockRumMonitor).removeViewAttributes(customAttributes.keys.toList()) + } + @Test fun `M call addViewLoadingTime w addViewLoadingTime()`(@BoolForgery overwrite: Boolean) { // When diff --git a/packages/core/android/src/test/kotlin/com/datadog/tools/unit/MockRumMonitor.kt b/packages/core/android/src/test/kotlin/com/datadog/tools/unit/MockRumMonitor.kt index 702cc2533..13f73d94a 100644 --- a/packages/core/android/src/test/kotlin/com/datadog/tools/unit/MockRumMonitor.kt +++ b/packages/core/android/src/test/kotlin/com/datadog/tools/unit/MockRumMonitor.kt @@ -30,7 +30,13 @@ class MockRumMonitor : RumMonitor { override fun addAttribute(key: String, value: Any?) {} - override fun addViewAttributes(attributes: Map) {} + override fun removeAttribute(key: String) {} + + override fun clearAttributes() {} + + override fun getAttributes(): Map { + return mapOf() + } override fun addError( message: String, @@ -55,15 +61,10 @@ class MockRumMonitor : RumMonitor { @ExperimentalRumApi override fun addViewLoadingTime(overwrite: Boolean) {} - override fun clearAttributes() {} - - override fun getAttributes(): Map { - return mapOf() - } - override fun getCurrentSessionId(callback: (String?) -> Unit) {} - override fun removeAttribute(key: String) {} + override fun addViewAttributes(attributes: Map) {} + override fun removeViewAttributes(attributes: Collection) {} override fun startAction( diff --git a/packages/core/ios/Sources/DdRum.mm b/packages/core/ios/Sources/DdRum.mm index 5d831942a..f5c324ce8 100644 --- a/packages/core/ios/Sources/DdRum.mm +++ b/packages/core/ios/Sources/DdRum.mm @@ -107,6 +107,35 @@ @implementation DdRum [self addTiming:name resolve:resolve reject:reject]; } +RCT_EXPORT_METHOD(addViewAttribute:(NSString*) key + withValue:(NSDictionary*) value + withResolver:(RCTPromiseResolveBlock)resolve + withRejecter:(RCTPromiseRejectBlock)reject) +{ + [self addViewAttribute:key value:value resolve:resolve reject:reject]; +} + +RCT_EXPORT_METHOD(removeViewAttribute:(NSString*) key + withResolver:(RCTPromiseResolveBlock)resolve + withRejecter:(RCTPromiseRejectBlock)reject) +{ + [self removeViewAttribute:key resolve:resolve reject:reject]; +} + +RCT_EXPORT_METHOD(addViewAttributes:(NSDictionary*) attributes + withResolver:(RCTPromiseResolveBlock)resolve + withRejecter:(RCTPromiseRejectBlock)reject) +{ + [self addViewAttributes:attributes resolve:resolve reject:reject]; +} + +RCT_EXPORT_METHOD(removeViewAttributes:(NSArray *)keys + withResolver:(RCTPromiseResolveBlock)resolve + withRejecter:(RCTPromiseRejectBlock)reject) +{ + [self removeViewAttributes:keys resolve:resolve reject:reject]; +} + RCT_REMAP_METHOD(addViewLoadingTime, withOverwrite:(BOOL)overwrite withResolver:(RCTPromiseResolveBlock)resolve withRejecter:(RCTPromiseRejectBlock)reject) @@ -138,7 +167,7 @@ @implementation DdRum // Thanks to this guard, we won't compile this code when we build for the old architecture. #ifdef RCT_NEW_ARCH_ENABLED - (std::shared_ptr)getTurboModule: - (const facebook::react::ObjCTurboModule::InitParams &)params +(const facebook::react::ObjCTurboModule::InitParams &)params { return std::make_shared(params); } @@ -180,6 +209,22 @@ - (void)addTiming:(NSString *)name resolve:(RCTPromiseResolveBlock)resolve rejec [self.ddRumImplementation addTimingWithName:name resolve:resolve reject:reject]; } +- (void)addViewAttribute:(NSString *)key value:(NSDictionary *)value resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + [self.ddRumImplementation addViewAttributeWithKey:key value:value resolve:resolve reject:reject]; +} + +- (void)removeViewAttribute:(NSString *)key resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + [self.ddRumImplementation removeViewAttributeWithKey:key resolve:resolve reject:reject]; +} + +- (void)addViewAttributes:(NSDictionary *)attributes resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + [self.ddRumImplementation addViewAttributesWithAttributes:attributes resolve:resolve reject:reject]; +} + +- (void)removeViewAttributes:(NSArray *)keys resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + [self.ddRumImplementation removeViewAttributesWithKeys:keys resolve:resolve reject:reject]; +} + - (void)addViewLoadingTime:(BOOL)overwrite resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {\ [self.ddRumImplementation addViewLoadingTimeWithOverwrite:overwrite resolve:resolve reject:reject]; } diff --git a/packages/core/ios/Sources/DdRumImplementation.swift b/packages/core/ios/Sources/DdRumImplementation.swift index 9f8da4c7f..6fac21f82 100644 --- a/packages/core/ios/Sources/DdRumImplementation.swift +++ b/packages/core/ios/Sources/DdRumImplementation.swift @@ -181,6 +181,34 @@ public class DdRumImplementation: NSObject { resolve(nil) } + @objc + public func addViewAttribute(key: AttributeKey, value: NSDictionary, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { + if let attributeValue = value.object(forKey: "value") { + let castedAttribute = castValueToSwift(attributeValue) + nativeRUM.addViewAttribute(forKey: key, value: castedAttribute) + } + resolve(nil) + } + + @objc + public func removeViewAttribute(key: AttributeKey, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { + nativeRUM.removeViewAttribute(forKey: key) + resolve(nil) + } + + @objc + public func addViewAttributes(attributes: NSDictionary, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { + let castedAttributes = castAttributesToSwift(attributes) + nativeRUM.addViewAttributes(castedAttributes) + resolve(nil) + } + + @objc + public func removeViewAttributes(keys: [AttributeKey], resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { + nativeRUM.removeViewAttributes(forKeys: keys) + resolve(nil) + } + @objc public func addViewLoadingTime(overwrite: Bool, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { nativeRUM.addViewLoadingTime(overwrite: overwrite) diff --git a/packages/core/ios/Sources/DdSdkImplementation.swift b/packages/core/ios/Sources/DdSdkImplementation.swift index 437b5ee39..9c3fe980f 100644 --- a/packages/core/ios/Sources/DdSdkImplementation.swift +++ b/packages/core/ios/Sources/DdSdkImplementation.swift @@ -4,13 +4,14 @@ * Copyright 2016-Present Datadog, Inc. */ -import Foundation import DatadogCore -import DatadogRUM -import DatadogLogs -import DatadogTrace import DatadogCrashReporting import DatadogInternal +import DatadogLogs +import DatadogRUM +import DatadogTrace +import DatadogWebViewTracking +import Foundation import React #if os(iOS) @@ -18,7 +19,8 @@ import DatadogWebViewTracking #endif func getDefaultAppVersion() -> String { - let bundleShortVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String + let bundleShortVersion = + Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String let bundleVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String return bundleShortVersion ?? bundleVersion ?? "0.0.0" } @@ -35,7 +37,7 @@ public class DdSdkImplementation: NSObject { var webviewMessageEmitter: InternalExtension.AbstractMessageEmitter? #endif - private let jsLongTaskThresholdInSeconds: TimeInterval = 0.1; + private let jsLongTaskThresholdInSeconds: TimeInterval = 0.1 @objc public convenience init(bridge: RCTBridge) { @@ -47,7 +49,7 @@ public class DdSdkImplementation: NSObject { RUMMonitorInternalProvider: { RUMMonitor.shared()._internal } ) } - + init( mainDispatchQueue: DispatchQueueType, jsDispatchQueue: DispatchQueueType, @@ -62,10 +64,13 @@ public class DdSdkImplementation: NSObject { self.RUMMonitorInternalProvider = RUMMonitorInternalProvider super.init() } - + // Using @escaping RCTPromiseResolveBlock type will result in an issue when compiling the Swift header file. @objc - public func initialize(configuration: NSDictionary, resolve:@escaping ((Any?) -> Void), reject:RCTPromiseRejectBlock) -> Void { + public func initialize( + configuration: NSDictionary, resolve: @escaping ((Any?) -> Void), + reject: RCTPromiseRejectBlock + ) { let sdkConfiguration = configuration.asDdSdkConfiguration() let nativeInitialization = DdSdkNativeInitialization() @@ -117,7 +122,9 @@ public class DdSdkImplementation: NSObject { } @objc - public func setUserInfo(userInfo: NSDictionary, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { + public func setUserInfo( + userInfo: NSDictionary, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock + ) { let castedUserInfo = castAttributesToSwift(userInfo) let id = castedUserInfo["id"] as? String let name = castedUserInfo["name"] as? String @@ -125,21 +132,22 @@ public class DdSdkImplementation: NSObject { var extraInfo: [AttributeKey: AttributeValue] = [:] if let extraInfoEncodable = castedUserInfo["extraInfo"] as? AnyEncodable, - let extraInfoDict = extraInfoEncodable.value as? [String: Any] { + let extraInfoDict = extraInfoEncodable.value as? [String: Any] + { extraInfo = castAttributesToSwift(extraInfoDict) } if let validId = id { Datadog.setUserInfo(id: validId, name: name, email: email, extraInfo: extraInfo) - } else { - // TO DO - log warning message? } resolve(nil) } - + @objc - public func addUserExtraInfo(extraInfo: NSDictionary, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { + public func addUserExtraInfo( + extraInfo: NSDictionary, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock + ) { let castedExtraInfo = castAttributesToSwift(extraInfo) Datadog.addUserExtraInfo(castedExtraInfo) @@ -147,35 +155,37 @@ public class DdSdkImplementation: NSObject { } @objc - public func clearUserInfo(resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { + public func clearUserInfo(resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) { Datadog.clearUserInfo() resolve(nil) } @objc - public func setTrackingConsent(trackingConsent: NSString, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { + public func setTrackingConsent( + trackingConsent: NSString, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock + ) { Datadog.set(trackingConsent: (trackingConsent as NSString?).asTrackingConsent()) resolve(nil) } - - + @objc - public func sendTelemetryLog(message: NSString, attributes: NSDictionary, config: NSDictionary, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { + public func sendTelemetryLog( + message: NSString, attributes: NSDictionary, config: NSDictionary, + resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock + ) { let castedAttributes = castAttributesToSwift(attributes) let castedConfig = castAttributesToSwift(config) - DdTelemetry.sendTelemetryLog(message: message as String, attributes: castedAttributes, config: castedConfig) + DdTelemetry.sendTelemetryLog( + message: message as String, attributes: castedAttributes, config: castedConfig) resolve(nil) } @objc - public func telemetryDebug(message: NSString, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { - DdTelemetry.telemetryDebug(id: "datadog_react_native:\(message)", message: message as String) - resolve(nil) - } - - @objc - public func telemetryError(message: NSString, stack: NSString, kind: NSString, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { - DdTelemetry.telemetryError(id: "datadog_react_native:\(String(describing: kind)):\(message)", message: message as String, kind: kind as String, stack: stack as String) + public func telemetryDebug( + message: NSString, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock + ) { + DdTelemetry.telemetryDebug( + id: "datadog_react_native:\(message)", message: message as String) resolve(nil) } <<<<<<< HEAD @@ -185,27 +195,50 @@ public class DdSdkImplementation: NSObject { >>>>>>> 0443e0ff (iOS: Always use SDK default core instance) @objc - public func consumeWebviewEvent(message: NSString, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { - do{ + public func telemetryError( + message: NSString, stack: NSString, kind: NSString, resolve: RCTPromiseResolveBlock, + reject: RCTPromiseRejectBlock + ) { + DdTelemetry.telemetryError( + id: "datadog_react_native:\(String(describing: kind)):\(message)", + message: message as String, kind: kind as String, stack: stack as String) + resolve(nil) + } + + @objc + public func consumeWebviewEvent( + message: NSString, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock + ) { + do { try DatadogSDKWrapper.shared.sendWebviewMessage(body: message) } catch { - DdTelemetry.telemetryError(id: "datadog_react_native:\(error.localizedDescription)", message: "The message being sent was:\(message)" as String, kind: "WebViewEventBridgeError" as String, stack: String(describing: error) as String) + DdTelemetry.telemetryError( + id: "datadog_react_native:\(error.localizedDescription)", + message: "The message being sent was:\(message)" as String, + kind: "WebViewEventBridgeError" as String, + stack: String(describing: error) as String) } resolve(nil) } +<<<<<<< HEAD #endif +======= + +>>>>>>> 1f781a51 (Expose view Attributes API) @objc - public func clearAllData(resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { + public func clearAllData(resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) { Datadog.clearAllData() resolve(nil) } - func overrideReactNativeTelemetry(rnConfiguration: DdSdkConfiguration) -> Void { + func overrideReactNativeTelemetry(rnConfiguration: DdSdkConfiguration) { DdTelemetry.overrideTelemetryConfiguration( - initializationType: rnConfiguration.configurationForTelemetry?.initializationType as? String, - reactNativeVersion: rnConfiguration.configurationForTelemetry?.reactNativeVersion as? String, + initializationType: rnConfiguration.configurationForTelemetry?.initializationType + as? String, + reactNativeVersion: rnConfiguration.configurationForTelemetry?.reactNativeVersion + as? String, reactVersion: rnConfiguration.configurationForTelemetry?.reactVersion as? String, trackCrossPlatformLongTasks: rnConfiguration.longTaskThresholdMs != 0, trackErrors: rnConfiguration.configurationForTelemetry?.trackErrors, @@ -220,24 +253,28 @@ public class DdSdkImplementation: NSObject { func startJSRefreshRateMonitoring(sdkConfiguration: DdSdkConfiguration) { if let frameTimeCallback = buildFrameTimeCallback(sdkConfiguration: sdkConfiguration) { // Falling back to mainDispatchQueue if bridge is nil is only useful for tests - self.jsRefreshRateMonitor.startMonitoring(jsQueue: jsDispatchQueue, frameTimeCallback: frameTimeCallback) + self.jsRefreshRateMonitor.startMonitoring( + jsQueue: jsDispatchQueue, frameTimeCallback: frameTimeCallback) } } - func buildFrameTimeCallback(sdkConfiguration: DdSdkConfiguration)-> ((Double) -> ())? { + func buildFrameTimeCallback(sdkConfiguration: DdSdkConfiguration) -> ((Double) -> Void)? { let jsRefreshRateMonitoringEnabled = sdkConfiguration.vitalsUpdateFrequency != nil let jsLongTaskMonitoringEnabled = sdkConfiguration.longTaskThresholdMs != 0 - - if (!jsRefreshRateMonitoringEnabled && !jsLongTaskMonitoringEnabled) { + + if !jsRefreshRateMonitoringEnabled && !jsLongTaskMonitoringEnabled { return nil } func frameTimeCallback(frameTime: Double) { // These checks happen before dispatching because they are quick and less overhead than the dispatch itself. let shouldRecordFrameTime = jsRefreshRateMonitoringEnabled && frameTime > 0 - let shouldRecordLongTask = jsLongTaskMonitoringEnabled && frameTime > sdkConfiguration.longTaskThresholdMs / 1_000 + let shouldRecordLongTask = + jsLongTaskMonitoringEnabled + && frameTime > sdkConfiguration.longTaskThresholdMs / 1_000 guard shouldRecordFrameTime || shouldRecordLongTask, - let rumMonitorInternal = RUMMonitorInternalProvider() else { return } + let rumMonitorInternal = RUMMonitorInternalProvider() + else { return } // Record current timestamp, it may change slightly before event is created on background thread. let now = Date() @@ -247,12 +284,14 @@ public class DdSdkImplementation: NSObject { let normalizedFrameTimeSeconds = DdSdkImplementation.normalizeFrameTimeForDeviceRefreshRate(frameTime) rumMonitorInternal.updatePerformanceMetric(at: now, metric: .jsFrameTimeSeconds, value: normalizedFrameTimeSeconds, attributes: [:]) } - if (shouldRecordLongTask) { - rumMonitorInternal.addLongTask(at: now, duration: frameTime, attributes: ["long_task.target": "javascript"]) + if shouldRecordLongTask { + rumMonitorInternal.addLongTask( + at: now, duration: frameTime, attributes: ["long_task.target": "javascript"] + ) } } } - + return frameTimeCallback } diff --git a/packages/core/ios/Tests/DdRumTests.swift b/packages/core/ios/Tests/DdRumTests.swift index 1102b7b6b..5f6adc016 100644 --- a/packages/core/ios/Tests/DdRumTests.swift +++ b/packages/core/ios/Tests/DdRumTests.swift @@ -253,6 +253,65 @@ internal class DdRumTests: XCTestCase { XCTAssertEqual(mockNativeRUM.receivedAttributes.count, 0) } + func testAddViewAttribute() throws { + let viewAttributeKey = "attributeKey" + let viewAttributes = NSDictionary( + dictionary: [ + "value": 123, + ] + ) + + rum.addViewAttribute(key: viewAttributeKey, value: viewAttributes, resolve: mockResolve, reject: mockReject) + + XCTAssertEqual(mockNativeRUM.calledMethods.count, 1) + XCTAssertEqual(mockNativeRUM.calledMethods.last, .addViewAttribute(key: viewAttributeKey)) + XCTAssertEqual(mockNativeRUM.receivedAttributes.count, 1) + let lastAttributes = try XCTUnwrap(mockNativeRUM.receivedAttributes.last) + XCTAssertEqual(lastAttributes.count, 1) + XCTAssertEqual(lastAttributes["attributeKey"] as? Int64, 123) + } + + func testRemoveViewAttribute() throws { + let viewAttributeKey = "attributeKey" + + rum.removeViewAttribute(key: viewAttributeKey, resolve: mockResolve, reject: mockReject) + + XCTAssertEqual(mockNativeRUM.calledMethods.count, 1) + XCTAssertEqual(mockNativeRUM.calledMethods.last, .removeViewAttribute(key: viewAttributeKey)) + } + + func testAddViewAttributes() throws { + let viewAttributes = NSDictionary( + dictionary: [ + "attribute-1": 123, + "attribute-2": "abc", + "attribute-3": true, + ] + ) + + rum.addViewAttributes(attributes: viewAttributes, resolve: mockResolve, reject: mockReject) + + + XCTAssertEqual(mockNativeRUM.calledMethods.count, 1) + XCTAssertEqual(mockNativeRUM.calledMethods.last, .addViewAttributes()) + XCTAssertEqual(mockNativeRUM.receivedAttributes.count, 1) + let lastAttributes = try XCTUnwrap(mockNativeRUM.receivedAttributes.last) + XCTAssertEqual(lastAttributes.count, 3) + XCTAssertEqual(lastAttributes["attribute-1"] as? Int64, 123) + XCTAssertEqual(lastAttributes["attribute-2"] as? String, "abc") + XCTAssertEqual(lastAttributes["attribute-3"] as? Bool, true) + } + + + func testRemoveViewAttributes() throws { + let viewAttributeKeys = ["attributeKey1", "attributeKey2", "attributeKey3"] + + rum.removeViewAttributes(keys: viewAttributeKeys, resolve: mockResolve, reject: mockReject) + + XCTAssertEqual(mockNativeRUM.calledMethods.count, 1) + XCTAssertEqual(mockNativeRUM.calledMethods.last, .removeViewAttributes(keys: viewAttributeKeys)) + } + func testAddViewLoadingTime() throws { rum.addViewLoadingTime(overwrite: true, resolve: mockResolve, reject: mockReject) diff --git a/packages/core/ios/Tests/MockRUMMonitor.swift b/packages/core/ios/Tests/MockRUMMonitor.swift index 3a882ed47..4a1d0cd92 100644 --- a/packages/core/ios/Tests/MockRUMMonitor.swift +++ b/packages/core/ios/Tests/MockRUMMonitor.swift @@ -10,22 +10,6 @@ @testable import DatadogSDKReactNative internal class MockRUMMonitor: RUMMonitorProtocol { - func addViewAttribute(forKey key: DatadogInternal.AttributeKey, value: any DatadogInternal.AttributeValue) { - // not implemented - } - - func addViewAttributes(_ attributes: [DatadogInternal.AttributeKey : any DatadogInternal.AttributeValue]) { - // not implemented - } - - func removeViewAttribute(forKey key: DatadogInternal.AttributeKey) { - // not implemented - } - - func removeViewAttributes(forKeys keys: [DatadogInternal.AttributeKey]) { - // not implemented - } - func currentSessionID(completion: @escaping (String?) -> Void) { // not implemented } @@ -71,6 +55,10 @@ internal class MockRUMMonitor: RUMMonitorProtocol { case stopUserAction(type: RUMActionType, name: String?) case addUserAction(type: RUMActionType, name: String) case addTiming(name: String) + case addViewAttribute(key: String) + case removeViewAttribute(key: String) + case addViewAttributes(_: Int? = nil) // We need an attribute for the case to be Equatable + case removeViewAttributes(keys: [String]) case addViewLoadingTime(overwrite: Bool) case stopSession(_: Int? = nil) // We need an attribute for the case to be Equatable case addResourceMetrics(resourceKey: String, @@ -131,6 +119,24 @@ internal class MockRUMMonitor: RUMMonitorProtocol { func addTiming(name: String) { calledMethods.append(.addTiming(name: name)) } + func addViewAttribute(forKey key: DatadogInternal.AttributeKey, value: any DatadogInternal.AttributeValue) { + calledMethods.append(.addViewAttribute(key: key)) + receivedAttributes.append([key :value]) + } + + func removeViewAttribute(forKey key: DatadogInternal.AttributeKey) { + calledMethods.append(.removeViewAttribute(key: key)) + } + + func addViewAttributes(_ attributes: [DatadogInternal.AttributeKey : any DatadogInternal.AttributeValue]) { + calledMethods.append(.addViewAttributes()) + receivedAttributes.append(attributes) + } + + func removeViewAttributes(forKeys keys: [DatadogInternal.AttributeKey]) { + calledMethods.append(.removeViewAttributes(keys: keys)) + } + func addViewLoadingTime(overwrite: Bool) { calledMethods.append(.addViewLoadingTime(overwrite: overwrite)) } diff --git a/packages/core/jest/mock.js b/packages/core/jest/mock.js index c49d13f48..0dc9ec138 100644 --- a/packages/core/jest/mock.js +++ b/packages/core/jest/mock.js @@ -119,6 +119,18 @@ module.exports = { addTiming: jest .fn() .mockImplementation(() => new Promise(resolve => resolve())), + addViewAttribute: jest + .fn() + .mockImplementation(() => new Promise(resolve => resolve())), + removeViewAttribute: jest + .fn() + .mockImplementation(() => new Promise(resolve => resolve())), + addViewAttributes: jest + .fn() + .mockImplementation(() => new Promise(resolve => resolve())), + removeViewAttributes: jest + .fn() + .mockImplementation(() => new Promise(resolve => resolve())), addViewLoadingTime: jest .fn() .mockImplementation(() => new Promise(resolve => resolve())), diff --git a/packages/core/src/rum/DdRum.ts b/packages/core/src/rum/DdRum.ts index b32ab410b..bedc6f33a 100644 --- a/packages/core/src/rum/DdRum.ts +++ b/packages/core/src/rum/DdRum.ts @@ -9,6 +9,7 @@ import { DdAttributes } from '../DdAttributes'; import { InternalLog } from '../InternalLog'; import { SdkVerbosity } from '../SdkVerbosity'; import type { DdNativeRumType } from '../nativeModulesTypes'; +import type { Attributes } from '../sdk/AttributesSingleton/types'; import { bufferVoidNativeCall } from '../sdk/DatadogProvider/Buffer/bufferNativeCall'; import { DdSdk } from '../sdk/DdSdk'; import { GlobalState } from '../sdk/GlobalState/GlobalState'; @@ -285,6 +286,50 @@ class DdRumWrapper implements DdRumType { return bufferVoidNativeCall(() => this.nativeRum.addTiming(name)); }; + addViewAttribute = (key: string, value: unknown): Promise => { + InternalLog.log( + `Adding view attribute “${key}" with value “${JSON.stringify( + value + )}” to RUM View`, + SdkVerbosity.DEBUG + ); + return bufferVoidNativeCall(() => + this.nativeRum.addViewAttribute(key, { value }) + ); + }; + + removeViewAttribute = (key: string): Promise => { + InternalLog.log( + `Removing view attribute “${key}" from RUM View`, + SdkVerbosity.DEBUG + ); + return bufferVoidNativeCall(() => + this.nativeRum.removeViewAttribute(key) + ); + }; + + addViewAttributes = (attributes: Attributes): Promise => { + InternalLog.log( + `Adding view attributes "${JSON.stringify( + attributes + )}” to RUM View`, + SdkVerbosity.DEBUG + ); + return bufferVoidNativeCall(() => + this.nativeRum.addViewAttributes(attributes) + ); + }; + + removeViewAttributes = (keys: string[]): Promise => { + InternalLog.log( + `Removing view attributes “${keys}" from RUM View`, + SdkVerbosity.DEBUG + ); + return bufferVoidNativeCall(() => + this.nativeRum.removeViewAttributes(keys) + ); + }; + addViewLoadingTime = (overwrite: boolean): Promise => { InternalLog.log( overwrite diff --git a/packages/core/src/rum/__tests__/DdRum.test.ts b/packages/core/src/rum/__tests__/DdRum.test.ts index 7e5fc24de..41b873fe9 100644 --- a/packages/core/src/rum/__tests__/DdRum.test.ts +++ b/packages/core/src/rum/__tests__/DdRum.test.ts @@ -1091,6 +1091,98 @@ describe('DdRum', () => { }); }); + describe('DdRum.addTiming', () => { + it('calls the native SDK when setting a timing', async () => { + // GIVEN + const timingName = 'testTiming'; + + // WHEN + await DdRum.addTiming(timingName); + + // THEN + expect(NativeModules.DdRum.addTiming).toHaveBeenCalledTimes(1); + expect(NativeModules.DdRum.addTiming).toHaveBeenCalledWith( + timingName + ); + }); + }); + + describe('DdRum.addViewAttribute', () => { + it('calls the native SDK when setting a view attribute', async () => { + // GIVEN + const key = 'testAttribute'; + const value = { test: 'attribute' }; + + // WHEN + + await DdRum.addViewAttribute(key, value); + + // THEN + expect( + NativeModules.DdRum.addViewAttribute + ).toHaveBeenCalledTimes(1); + expect( + NativeModules.DdRum.addViewAttribute + ).toHaveBeenCalledWith(key, { value }); + }); + }); + + describe('DdRum.removViewAttribute', () => { + it('calls the native SDK when removing a view attribute', async () => { + // GIVEN + const key = 'testAttribute'; + + // WHEN + await DdRum.removeViewAttribute(key); + + // THEN + expect( + NativeModules.DdRum.removeViewAttribute + ).toHaveBeenCalledTimes(1); + expect( + NativeModules.DdRum.removeViewAttribute + ).toHaveBeenCalledWith(key); + }); + }); + + describe('DdRum.addViewAttributes', () => { + it('calls the native SDK when setting view attributes', async () => { + // GIVEN + const attributes = { + test: 'attribute' + }; + + // WHEN + await DdRum.addViewAttributes(attributes); + + // THEN + expect( + NativeModules.DdRum.addViewAttributes + ).toHaveBeenCalledTimes(1); + expect( + NativeModules.DdRum.addViewAttributes + ).toHaveBeenCalledWith(attributes); + }); + }); + + describe('DdRum.removViewAttributes', () => { + it('calls the native SDK when removing view attributes', async () => { + // GIVEN + const keysToDelete = ['test1', 'test2']; + + // WHEN + await DdRum.removeViewAttributes(keysToDelete); + + // THEN + expect( + NativeModules.DdRum.removeViewAttributes + ).toHaveBeenCalledTimes(1); + expect( + NativeModules.DdRum.removeViewAttributes + ).toHaveBeenCalledWith(keysToDelete); + }); + }); + describe('DdRum.addAction', () => { test('uses given context when context is valid', async () => { const context = { diff --git a/packages/core/src/rum/types.ts b/packages/core/src/rum/types.ts index fc8d07c02..3def7f0e6 100644 --- a/packages/core/src/rum/types.ts +++ b/packages/core/src/rum/types.ts @@ -4,6 +4,7 @@ * Copyright 2016-Present Datadog, Inc. */ +import type { Attributes } from '../sdk/AttributesSingleton/types'; import type { ErrorSource } from '../types'; import type { DatadogTracingContext } from './instrumentation/resourceTracking/distributedTracing/DatadogTracingContext'; @@ -148,6 +149,31 @@ export type DdRumType = { */ addTiming(name: string): Promise; + /** + * Adds a custom attribute to the active RUM View. It will be propagated to all future RUM events associated with the active View. + * @param key: key for this view attribute. + * @param value: value for this attribute. + */ + addViewAttribute(key: string, value: unknown): Promise; + + /** + * Removes an attribute from the active RUM View. + * @param key: key for the attribute to be removed from the view. + */ + removeViewAttribute(key: string): Promise; + + /** + * Adds multiple attributes to the active RUM View. They will be propagated to all future RUM events associated with the active View. + * @param attributes: key/value object containing all attributes to be added to the view. + */ + addViewAttributes(attributes: Attributes): Promise; + + /** + * Removes multiple attributes from the active RUM View. + * @param keys: keys for the attributes to be removed from the view. + */ + removeViewAttributes(keys: string[]): Promise; + /** * Adds the loading time of the view to the active view. * It is calculated as the difference between the current time and the start time of the view. diff --git a/packages/core/src/specs/NativeDdRum.ts b/packages/core/src/specs/NativeDdRum.ts index f6f7b3daa..e31f5b925 100644 --- a/packages/core/src/specs/NativeDdRum.ts +++ b/packages/core/src/specs/NativeDdRum.ts @@ -136,6 +136,31 @@ export interface Spec extends TurboModule { */ addTiming(name: string): Promise; + /** + * Adds a custom attribute to the active RUM View. It will be propagated to all future RUM events associated with the active View. + * @param key: key for this view attribute. + * @param value: value for this attribute. + */ + addViewAttribute(key: string, value: Object): Promise; + + /** + * Removes an attribute from the active RUM View. + * @param key: key for the attribute to be removed from the view. + */ + removeViewAttribute(key: string): Promise; + + /** + * Adds multiple attributes to the active RUM View. They will be propagated to all future RUM events associated with the active View. + * @param attributes: key/value object containing all attributes to be added to the view. + */ + addViewAttributes(attributes: Object): Promise; + + /** + * Removes multiple attributes from the active RUM View. + * @param keys: keys for the attributes to be removed from the view. + */ + removeViewAttributes(keys: string[]): Promise; + /** * Adds the loading time of the view to the active view. * It is calculated as the difference between the current time and the start time of the view. From 96c5f67b6c5c6d4330f691e5d466adbabdd3bf76 Mon Sep 17 00:00:00 2001 From: Sergio Barrio Date: Fri, 22 Aug 2025 15:43:01 +0200 Subject: [PATCH 077/410] Fix FileBasedConfiguration related issues --- example/datadog-configuration.json | 20 ++ example/src/App.tsx | 20 +- .../codepush/src/__tests__/index.test.tsx | 4 +- .../FileBasedConfiguration.ts | 54 ++--- .../__tests__/FileBasedConfiguration.test.ts | 192 ++++++++++-------- .../__fixtures__/malformed-configuration.json | 1 - 6 files changed, 157 insertions(+), 134 deletions(-) create mode 100644 example/datadog-configuration.json diff --git a/example/datadog-configuration.json b/example/datadog-configuration.json new file mode 100644 index 000000000..684e60304 --- /dev/null +++ b/example/datadog-configuration.json @@ -0,0 +1,20 @@ +{ + "$schema": "./node_modules/@datadog/mobile-react-native/datadog-configuration.schema.json", + "configuration": { + "applicationId": "APP_ID", + "batchSize": "SMALL", + "clientToken": "CLIENT_TOKEN", + "env": "ENVIRONMENT", + "longTaskThresholdMs": 1000, + "nativeCrashReportEnabled": true, + "sessionSamplingRate": 100, + "site": "US1", + "telemetrySampleRate": 20, + "trackBackgroundEvents": false, + "trackErrors": true, + "trackInteractions": true, + "trackResources": true, + "trackingConsent": "GRANTED", + "verbosity": "DEBUG" + } +} diff --git a/example/src/App.tsx b/example/src/App.tsx index cefcafffe..c29982a97 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -7,7 +7,7 @@ import AboutScreen from './screens/AboutScreen'; import style from './screens/styles'; import { navigationRef } from './NavigationRoot'; import { DdRumReactNavigationTracking, ViewNamePredicate } from '@datadog/mobile-react-navigation'; -import {DatadogProvider} from '@datadog/mobile-react-native' +import {DatadogProvider, FileBasedConfiguration} from '@datadog/mobile-react-native' import { Route } from "@react-navigation/native"; import { NestedNavigator } from './screens/NestedNavigator/NestedNavigator'; import { getDatadogConfig, onDatadogInitialization } from './ddUtils'; @@ -19,9 +19,25 @@ const viewPredicate: ViewNamePredicate = function customViewNamePredicate(route: return "Custom RN " + trackedName; } +// === Datadog Provider Configuration schemes === + +// 1.- Direct configuration +const configuration = getDatadogConfig(TrackingConsent.GRANTED) + +// 2.- File based configuration from .json +// const configuration = new FileBasedConfiguration(require("../datadog-configuration.json")); + +// 3.- File based configuration from .json and custom mapper setup +// const configuration = new FileBasedConfiguration( { +// configuration: require("../datadog-configuration.json").configuration, +// errorEventMapper: (event) => event, +// resourceEventMapper: (event) => event, +// actionEventMapper: (event) => event}); + + export default function App() { return ( - + { DdRumReactNavigationTracking.startTrackingViews(navigationRef.current, viewPredicate) }}> diff --git a/packages/codepush/src/__tests__/index.test.tsx b/packages/codepush/src/__tests__/index.test.tsx index 5c94c16ef..63e1af7d6 100644 --- a/packages/codepush/src/__tests__/index.test.tsx +++ b/packages/codepush/src/__tests__/index.test.tsx @@ -279,7 +279,7 @@ describe('AppCenter Codepush integration', () => { }; const configuration = new FileBasedConfiguration({ - configuration: { configuration: autoInstrumentationConfig } + configuration: autoInstrumentationConfig }); render(); @@ -346,7 +346,7 @@ describe('AppCenter Codepush integration', () => { }; const configuration = new FileBasedConfiguration({ - configuration: { configuration: autoInstrumentationConfig } + configuration: autoInstrumentationConfig }); render(); diff --git a/packages/core/src/sdk/FileBasedConfiguration/FileBasedConfiguration.ts b/packages/core/src/sdk/FileBasedConfiguration/FileBasedConfiguration.ts index ddd1943aa..3fc69f1f3 100644 --- a/packages/core/src/sdk/FileBasedConfiguration/FileBasedConfiguration.ts +++ b/packages/core/src/sdk/FileBasedConfiguration/FileBasedConfiguration.ts @@ -56,44 +56,7 @@ export class FileBasedConfiguration extends DatadogProviderConfiguration { const resolveJSONConfiguration = ( userSpecifiedConfiguration: unknown ): Record => { - if ( - userSpecifiedConfiguration === undefined || - userSpecifiedConfiguration === null - ) { - try { - // This corresponds to a file located at the root of a RN project. - // /!\ We have to write the require this way as dynamic requires are not supported by Hermes. - // eslint-disable-next-line global-require, @typescript-eslint/no-var-requires - const jsonContent = require('../../../../../../datadog-configuration.json'); - - if ( - typeof jsonContent !== 'object' || - !jsonContent['configuration'] - ) { - console.error(`Failed to parse the Datadog configuration file located at the root of the project. -Your configuration must validate the node_modules/@datadog/mobile-react-native/datadog-configuration.schema.json JSON schema. -You can use VSCode to check your configuration by adding the following line to your JSON file: -{ - "$schema": "./node_modules/@datadog/mobile-react-native/datadog-configuration.schema.json", -}`); - - return {}; - } - - return jsonContent.configuration as Record; - } catch (error) { - console.error(`Failed to read Datadog configuration file at the root of the project. -If you don't have a datadog-configuration.json file at the same level as your node_modules directory,\ -please use the following syntax:\n -new FileBasedConfiguration({configuration: require('./file/to/configuration-file.json')}) -`); - return {}; - } - } - if ( - typeof userSpecifiedConfiguration !== 'object' || - !(userSpecifiedConfiguration as any)['configuration'] - ) { + if (typeof userSpecifiedConfiguration !== 'object') { console.error(`Failed to parse the Datadog configuration file you provided. Your configuration must validate the node_modules/@datadog/mobile-react-native/datadog-configuration.schema.json JSON schema. You can use VSCode to check your configuration by adding the following line to your JSON file: @@ -104,10 +67,7 @@ You can use VSCode to check your configuration by adding the following line to y return {}; } - return (userSpecifiedConfiguration as any)['configuration'] as Record< - string, - any - >; + return (userSpecifiedConfiguration as any) as Record; }; export const getJSONConfiguration = ( @@ -130,6 +90,16 @@ export const getJSONConfiguration = ( } => { const configuration = resolveJSONConfiguration(userSpecifiedConfiguration); + if ( + configuration.clientToken === undefined || + configuration.env === undefined || + configuration.applicationId === undefined + ) { + console.warn( + 'DATADOG: Warning: Malformed json configuration file - clientToken, applicationId and env are mandatory properties.' + ); + } + return { clientToken: configuration.clientToken, env: configuration.env, diff --git a/packages/core/src/sdk/FileBasedConfiguration/__tests__/FileBasedConfiguration.test.ts b/packages/core/src/sdk/FileBasedConfiguration/__tests__/FileBasedConfiguration.test.ts index 716243e86..6d3ee2e44 100644 --- a/packages/core/src/sdk/FileBasedConfiguration/__tests__/FileBasedConfiguration.test.ts +++ b/packages/core/src/sdk/FileBasedConfiguration/__tests__/FileBasedConfiguration.test.ts @@ -16,35 +16,99 @@ import malformedConfiguration from './__fixtures__/malformed-configuration.json' describe('FileBasedConfiguration', () => { describe('with user-specified configuration', () => { + it('resolves configuration fields', () => { + const configuration = new FileBasedConfiguration( + configurationAllFields + ); + + expect(configuration).toMatchInlineSnapshot(` + FileBasedConfiguration { + "actionEventMapper": null, + "actionNameAttribute": "action-name-attr", + "additionalConfiguration": {}, + "applicationId": "fake-app-id", + "batchProcessingLevel": "MEDIUM", + "batchSize": "MEDIUM", + "bundleLogsWithRum": true, + "bundleLogsWithTraces": true, + "clientToken": "fake-client-token", + "customEndpoints": {}, + "env": "fake-env", + "errorEventMapper": null, + "firstPartyHosts": [ + { + "match": "example.com", + "propagatorTypes": [ + "b3multi", + "tracecontext", + ], + }, + ], + "initializationMode": "SYNC", + "logEventMapper": null, + "longTaskThresholdMs": 44, + "nativeCrashReportEnabled": false, + "nativeInteractionTracking": false, + "nativeLongTaskThresholdMs": 200, + "nativeViewTracking": false, + "proxyConfig": undefined, + "resourceEventMapper": null, + "resourceTracingSamplingRate": 33, + "serviceName": undefined, + "sessionSamplingRate": 100, + "site": "US5", + "telemetrySampleRate": 20, + "trackBackgroundEvents": false, + "trackErrors": true, + "trackFrustrations": true, + "trackInteractions": true, + "trackResources": true, + "trackWatchdogTerminations": false, + "trackingConsent": "not_granted", + "uploadFrequency": "AVERAGE", + "useAccessibilityLabel": false, + "verbosity": "warn", + "vitalsUpdateFrequency": "AVERAGE", + } + `); + }); + + it('prints a warning message when the configuration file cannot be parsed correctly', () => { + const warnSpy = jest.spyOn(console, 'warn'); + getJSONConfiguration(malformedConfiguration); + + expect(warnSpy).toHaveBeenCalledWith( + 'DATADOG: Warning: Malformed json configuration file - clientToken, applicationId and env are mandatory properties.' + ); + }); + it('resolves all properties from a given file path', () => { const config = new FileBasedConfiguration({ configuration: { - configuration: { - applicationId: 'fake-app-id', - env: 'fake-env', - clientToken: 'fake-client-token', - trackInteractions: true, - trackResources: true, - trackErrors: true, - trackingConsent: 'NOT_GRANTED', - longTaskThresholdMs: 44, - site: 'US5', - verbosity: 'WARN', - actionNameAttribute: 'action-name-attr', - useAccessibilityLabel: false, - resourceTracingSamplingRate: 33, - firstPartyHosts: [ - { - match: 'example.com', - propagatorTypes: [ - 'B3MULTI', - 'TRACECONTEXT', - 'B3', - 'DATADOG' - ] - } - ] - } + applicationId: 'fake-app-id', + env: 'fake-env', + clientToken: 'fake-client-token', + trackInteractions: true, + trackResources: true, + trackErrors: true, + trackingConsent: 'NOT_GRANTED', + longTaskThresholdMs: 44, + site: 'US5', + verbosity: 'WARN', + actionNameAttribute: 'action-name-attr', + useAccessibilityLabel: false, + resourceTracingSamplingRate: 33, + firstPartyHosts: [ + { + match: 'example.com', + propagatorTypes: [ + 'B3MULTI', + 'TRACECONTEXT', + 'B3', + 'DATADOG' + ] + } + ] } }); expect(config).toMatchInlineSnapshot(` @@ -103,11 +167,9 @@ describe('FileBasedConfiguration', () => { it('applies default values to configuration from a given file path', () => { const config = new FileBasedConfiguration({ configuration: { - configuration: { - applicationId: 'fake-app-id', - env: 'fake-env', - clientToken: 'fake-client-token' - } + applicationId: 'fake-app-id', + env: 'fake-env', + clientToken: 'fake-client-token' } }); expect(config).toMatchInlineSnapshot(` @@ -159,11 +221,9 @@ describe('FileBasedConfiguration', () => { const resourceEventMapper = () => null; const config = new FileBasedConfiguration({ configuration: { - configuration: { - applicationId: 'fake-app-id', - env: 'fake-env', - clientToken: 'fake-client-token' - } + applicationId: 'fake-app-id', + env: 'fake-env', + clientToken: 'fake-client-token' }, actionEventMapper, errorEventMapper, @@ -188,62 +248,20 @@ describe('FileBasedConfiguration', () => { it('prints a warning message when the first party hosts contain unknown propagator types', () => { const config = new FileBasedConfiguration({ configuration: { - configuration: { - applicationId: 'fake-app-id', - env: 'fake-env', - clientToken: 'fake-client-token', - firstPartyHosts: [ - { - match: 'example.com', - propagatorTypes: ['UNKNOWN'] - } - ] - } + applicationId: 'fake-app-id', + env: 'fake-env', + clientToken: 'fake-client-token', + firstPartyHosts: [ + { + match: 'example.com', + propagatorTypes: ['UNKNOWN'] + } + ] } }); expect(config.firstPartyHosts).toHaveLength(0); }); }); - describe('with resolved file configuration', () => { - it('resolves configuration fields', () => { - const configuration = getJSONConfiguration(configurationAllFields); - - expect(configuration).toMatchInlineSnapshot(` - { - "actionNameAttribute": "action-name-attr", - "applicationId": "fake-app-id", - "clientToken": "fake-client-token", - "env": "fake-env", - "firstPartyHosts": [ - { - "match": "example.com", - "propagatorTypes": [ - "b3multi", - "tracecontext", - ], - }, - ], - "longTaskThresholdMs": 44, - "resourceTracingSamplingRate": 33, - "site": "US5", - "trackErrors": true, - "trackInteractions": true, - "trackResources": true, - "trackingConsent": "not_granted", - "useAccessibilityLabel": false, - "verbosity": "warn", - } - `); - }); - it('prints a warning message when the configuration file is not found', () => { - expect(() => getJSONConfiguration(undefined)).not.toThrow(); - }); - it('prints a warning message when the configuration file cannot be parsed correctly', () => { - expect(() => - getJSONConfiguration(malformedConfiguration) - ).not.toThrow(); - }); - }); describe('formatPropagatorType', () => { it('formats all propagatorTypes correctly', () => { diff --git a/packages/core/src/sdk/FileBasedConfiguration/__tests__/__fixtures__/malformed-configuration.json b/packages/core/src/sdk/FileBasedConfiguration/__tests__/__fixtures__/malformed-configuration.json index 28423084d..0e1b26639 100644 --- a/packages/core/src/sdk/FileBasedConfiguration/__tests__/__fixtures__/malformed-configuration.json +++ b/packages/core/src/sdk/FileBasedConfiguration/__tests__/__fixtures__/malformed-configuration.json @@ -1,5 +1,4 @@ { "clientToken": "clientToken", - "env": "env", "applicationId": "applicationId" } From 71638bf01e812974e9e621294a453cb4de18f990 Mon Sep 17 00:00:00 2001 From: Sergio Barrio Date: Mon, 27 Oct 2025 14:45:54 +0100 Subject: [PATCH 078/410] Bump Android Native SDK to 3.2.0 and regenerate app podfiles --- benchmarks/android/app/build.gradle | 2 +- benchmarks/ios/Podfile.lock | 10 +++++----- example-new-architecture/ios/Podfile.lock | 6 +++--- example/ios/Podfile.lock | 14 +++++++------- packages/core/android/build.gradle | 10 +++++----- .../android/build.gradle | 4 ++-- packages/react-native-webview/android/build.gradle | 2 +- 7 files changed, 24 insertions(+), 24 deletions(-) diff --git a/benchmarks/android/app/build.gradle b/benchmarks/android/app/build.gradle index 04c240bd0..6d9f28764 100644 --- a/benchmarks/android/app/build.gradle +++ b/benchmarks/android/app/build.gradle @@ -129,5 +129,5 @@ dependencies { // Benchmark tools from dd-sdk-android are used for vitals recording // Remember to bump thid alongside the main dd-sdk-android dependencies - implementation("com.datadoghq:dd-sdk-android-benchmark-internal:3.1.0") + implementation("com.datadoghq:dd-sdk-android-benchmark-internal:3.2.0") } diff --git a/benchmarks/ios/Podfile.lock b/benchmarks/ios/Podfile.lock index 1be186d5f..403441ec6 100644 --- a/benchmarks/ios/Podfile.lock +++ b/benchmarks/ios/Podfile.lock @@ -10,7 +10,7 @@ PODS: - DatadogInternal (= 3.1.0) - DatadogRUM (3.1.0): - DatadogInternal (= 3.1.0) - - DatadogSDKReactNative (2.12.1): + - DatadogSDKReactNative (2.13.0): - DatadogCore (= 3.1.0) - DatadogCrashReporting (= 3.1.0) - DatadogLogs (= 3.1.0) @@ -60,7 +60,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - DatadogSDKReactNativeWebView (2.12.1): + - DatadogSDKReactNativeWebView (2.13.0): - DatadogInternal (= 3.1.0) - DatadogSDKReactNative - DatadogWebViewTracking (= 3.1.0) @@ -2075,9 +2075,9 @@ SPEC CHECKSUMS: DatadogInternal: 7837b2ce3d525d429682532eeda697b181299fdc DatadogLogs: 250894b5a99da5b924a019049c0d0326823cdbd6 DatadogRUM: 0d2a60e1abb8aacfb8827ef84f6d5deb4d5026c8 - DatadogSDKReactNative: 8e0f39de38621d4d7ed961a74d8a216fd3a38321 - DatadogSDKReactNativeSessionReplay: f9288c8e981dcc65d1f727b01421ee9a7601e75f - DatadogSDKReactNativeWebView: 993527f6c5d38e0fcc4804a6a60c334dd199dc5b + DatadogSDKReactNative: 620018df2896abcfad6b338c633cc8eccd5de406 + DatadogSDKReactNativeSessionReplay: b2ef22431dd0816adea8d65df13180cf40533f9d + DatadogSDKReactNativeWebView: 299629cf348a5e8f1dabb8289920a00eee625d6a DatadogSessionReplay: 6bc71888e2b41dd0de3325f06f0c0b3cee0e6df4 DatadogTrace: f59e933074cd285ad7e9f5af991f8fe04b095991 DatadogWebViewTracking: 9bc92b4147aeed47eb1911451f651094aa6dd6c1 diff --git a/example-new-architecture/ios/Podfile.lock b/example-new-architecture/ios/Podfile.lock index 0378f5d72..051f25e11 100644 --- a/example-new-architecture/ios/Podfile.lock +++ b/example-new-architecture/ios/Podfile.lock @@ -10,7 +10,7 @@ PODS: - DatadogInternal (= 3.1.0) - DatadogRUM (3.1.0): - DatadogInternal (= 3.1.0) - - DatadogSDKReactNative (2.12.1): + - DatadogSDKReactNative (2.13.0): - DatadogCore (= 3.1.0) - DatadogCrashReporting (= 3.1.0) - DatadogLogs (= 3.1.0) @@ -37,7 +37,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - DatadogSDKReactNative/Tests (2.12.1): + - DatadogSDKReactNative/Tests (2.13.0): - DatadogCore (= 3.1.0) - DatadogCrashReporting (= 3.1.0) - DatadogLogs (= 3.1.0) @@ -1855,7 +1855,7 @@ SPEC CHECKSUMS: DatadogInternal: 7837b2ce3d525d429682532eeda697b181299fdc DatadogLogs: 250894b5a99da5b924a019049c0d0326823cdbd6 DatadogRUM: 0d2a60e1abb8aacfb8827ef84f6d5deb4d5026c8 - DatadogSDKReactNative: 069ea9876220b2d09b0f4b180ce571b1b6ecbb35 + DatadogSDKReactNative: 2f11191b56e18680f633bfb125ab1832b327d9b4 DatadogTrace: f59e933074cd285ad7e9f5af991f8fe04b095991 DatadogWebViewTracking: 9bc92b4147aeed47eb1911451f651094aa6dd6c1 DoubleConversion: f16ae600a246532c4020132d54af21d0ddb2a385 diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 94d93b3c2..d0ba99782 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -10,7 +10,7 @@ PODS: - DatadogInternal (= 3.1.0) - DatadogRUM (3.1.0): - DatadogInternal (= 3.1.0) - - DatadogSDKReactNative (2.12.1): + - DatadogSDKReactNative (2.13.0): - DatadogCore (= 3.1.0) - DatadogCrashReporting (= 3.1.0) - DatadogLogs (= 3.1.0) @@ -18,7 +18,7 @@ PODS: - DatadogTrace (= 3.1.0) - DatadogWebViewTracking (= 3.1.0) - React-Core - - DatadogSDKReactNative/Tests (2.12.1): + - DatadogSDKReactNative/Tests (2.13.0): - DatadogCore (= 3.1.0) - DatadogCrashReporting (= 3.1.0) - DatadogLogs (= 3.1.0) @@ -73,12 +73,12 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - DatadogSDKReactNativeWebView (2.12.1): + - DatadogSDKReactNativeWebView (2.13.0): - DatadogInternal (= 3.1.0) - DatadogSDKReactNative - DatadogWebViewTracking (= 3.1.0) - React-Core - - DatadogSDKReactNativeWebView/Tests (2.12.1): + - DatadogSDKReactNativeWebView/Tests (2.13.0): - DatadogInternal (= 3.1.0) - DatadogSDKReactNative - DatadogWebViewTracking (= 3.1.0) @@ -1993,9 +1993,9 @@ SPEC CHECKSUMS: DatadogInternal: 7837b2ce3d525d429682532eeda697b181299fdc DatadogLogs: 250894b5a99da5b924a019049c0d0326823cdbd6 DatadogRUM: 0d2a60e1abb8aacfb8827ef84f6d5deb4d5026c8 - DatadogSDKReactNative: af351a4e1ce08124c290c52de94b0062a166cc67 - DatadogSDKReactNativeSessionReplay: dcbd55d9d0f2b86026996a8b7ec9654922d5dfe1 - DatadogSDKReactNativeWebView: 096ac87eb753b6a217b93441983264b9837c3b7e + DatadogSDKReactNative: 822ff8092666172584d4d5e56f79c3799887d408 + DatadogSDKReactNativeSessionReplay: afc4e2b1db34ba8af3a442b0691359faaf5e586e + DatadogSDKReactNativeWebView: 00affefdaca0cf2375e669fa03925d8fa75263d0 DatadogSessionReplay: 6bc71888e2b41dd0de3325f06f0c0b3cee0e6df4 DatadogTrace: f59e933074cd285ad7e9f5af991f8fe04b095991 DatadogWebViewTracking: 9bc92b4147aeed47eb1911451f651094aa6dd6c1 diff --git a/packages/core/android/build.gradle b/packages/core/android/build.gradle index 59bb75cc3..080185d41 100644 --- a/packages/core/android/build.gradle +++ b/packages/core/android/build.gradle @@ -201,16 +201,16 @@ dependencies { // This breaks builds if the React Native target is below 0.76.0. as it relies on Gradle 8.5.0. // To avoid this, we enforce 1.0.0-beta01 on RN < 0.76.0 if (reactNativeMinorVersion < 76) { - implementation("com.datadoghq:dd-sdk-android-rum:3.1.0") { + implementation("com.datadoghq:dd-sdk-android-rum:3.2.0") { exclude group: "androidx.metrics", module: "metrics-performance" } implementation "androidx.metrics:metrics-performance:1.0.0-beta01" } else { - implementation "com.datadoghq:dd-sdk-android-rum:3.1.0" + implementation "com.datadoghq:dd-sdk-android-rum:3.2.0" } - implementation "com.datadoghq:dd-sdk-android-logs:3.1.0" - implementation "com.datadoghq:dd-sdk-android-trace:3.1.0" - implementation "com.datadoghq:dd-sdk-android-webview:3.1.0" + implementation "com.datadoghq:dd-sdk-android-logs:3.2.0" + implementation "com.datadoghq:dd-sdk-android-trace:3.2.0" + implementation "com.datadoghq:dd-sdk-android-webview:3.2.0" implementation "com.google.code.gson:gson:2.10.0" testImplementation "org.junit.platform:junit-platform-launcher:1.6.2" testImplementation "org.junit.jupiter:junit-jupiter-api:5.6.2" diff --git a/packages/react-native-session-replay/android/build.gradle b/packages/react-native-session-replay/android/build.gradle index a0d77f2ff..bea305684 100644 --- a/packages/react-native-session-replay/android/build.gradle +++ b/packages/react-native-session-replay/android/build.gradle @@ -216,8 +216,8 @@ dependencies { api "com.facebook.react:react-android:$reactNativeVersion" } implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" - implementation "com.datadoghq:dd-sdk-android-session-replay:3.1.0" - implementation "com.datadoghq:dd-sdk-android-internal:3.1.0" + implementation "com.datadoghq:dd-sdk-android-session-replay:3.2.0" + implementation "com.datadoghq:dd-sdk-android-internal:3.2.0" implementation project(path: ':datadog_mobile-react-native') testImplementation "org.junit.platform:junit-platform-launcher:1.6.2" diff --git a/packages/react-native-webview/android/build.gradle b/packages/react-native-webview/android/build.gradle index 87ca1b7e7..dbb8d0593 100644 --- a/packages/react-native-webview/android/build.gradle +++ b/packages/react-native-webview/android/build.gradle @@ -190,7 +190,7 @@ dependencies { implementation "com.facebook.react:react-android:$reactNativeVersion" } - implementation "com.datadoghq:dd-sdk-android-webview:3.1.0" + implementation "com.datadoghq:dd-sdk-android-webview:3.2.0" implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation project(path: ':datadog_mobile-react-native') From c736fd97a61e334e90ad859a99cfcfc1a893cc5c Mon Sep 17 00:00:00 2001 From: Marco Saia Date: Tue, 9 Sep 2025 12:08:35 +0200 Subject: [PATCH 079/410] iOS: Always use SDK default core instance --- .../core/ios/Sources/DatadogSDKWrapper.swift | 113 +----------------- .../ios/Sources/DdSdkImplementation.swift | 37 ++---- .../Sources/DdSdkNativeInitialization.swift | 3 + 3 files changed, 17 insertions(+), 136 deletions(-) diff --git a/packages/core/ios/Sources/DatadogSDKWrapper.swift b/packages/core/ios/Sources/DatadogSDKWrapper.swift index 3c56688b0..894f0a09f 100644 --- a/packages/core/ios/Sources/DatadogSDKWrapper.swift +++ b/packages/core/ios/Sources/DatadogSDKWrapper.swift @@ -13,15 +13,12 @@ import DatadogCrashReporting import DatadogInternal import Foundation -<<<<<<< HEAD + #if os(iOS) import DatadogWebViewTracking #endif -public typealias OnCoreInitializedListener = (DatadogCoreProtocol) -> Void -======= -public typealias OnSdkInitializedListener = () -> Void ->>>>>>> 0443e0ff (iOS: Always use SDK default core instance) +public typealias OnSdkInitializedListener = (DatadogCoreProtocol) -> Void /// Wrapper around the Datadog SDK. Use DatadogSDKWrapper.shared to access the instance. public class DatadogSDKWrapper { @@ -45,118 +42,16 @@ public class DatadogSDKWrapper { loggerConfiguration: DatadogLogs.Logger.Configuration, trackingConsent: TrackingConsent ) -> Void { - Datadog.initialize(with: coreConfiguration, trackingConsent: trackingConsent) + let core = Datadog.initialize(with: coreConfiguration, trackingConsent: trackingConsent) for listener in onSdkInitializedListeners { - listener() + listener(core) } self.loggerConfiguration = loggerConfiguration } -<<<<<<< HEAD - internal func isInitialized() -> Bool { - return Datadog.isInitialized() - } - - internal func clearAllData() -> Void { - if let core = coreInstance { - Datadog.clearAllData(in: core) - } else { - Datadog.clearAllData() - } - } - - // Features - internal func enableRUM(with configuration: RUM.Configuration) { - if let core = coreInstance { - RUM.enable(with: configuration, in: core) - } else { - consolePrint("Core instance was not found when initializing RUM.", .critical) - } - } - - internal func enableLogs(with configuration: Logs.Configuration) { - if let core = coreInstance { - Logs.enable(with: configuration, in: core) - } else { - consolePrint("Core instance was not found when initializing Logs.", .critical) - } - } - - internal func enableTrace(with configuration: Trace.Configuration) { - if let core = coreInstance { - Trace.enable(with: configuration, in: core) - } else { - consolePrint("Core instance was not found when initializing Trace.", .critical) - } - } - - internal func enableCrashReporting() { - if let core = coreInstance { - CrashReporting.enable(in: core) - } else { - consolePrint("Core instance was not found when initializing CrashReporting.", .critical) - } - } - - internal func createLogger() -> LoggerProtocol { - let core = coreInstance ?? { - consolePrint("Core instance was not found when creating Logger.", .critical) - return CoreRegistry.default - }() - - return DatadogLogs.Logger.create(with: loggerConfiguration, in: core) - } - - // Telemetry - internal func sendTelemetryLog(message: String, attributes: [String: any Encodable], config: [String: any Encodable]) { - if let core = coreInstance { - let id = (config["onlyOnce"] as? Bool) == true ? message : UUID().uuidString - core.telemetry.debug(id: id, message: message, attributes: attributes) - } else { - consolePrint("Core instance was not found when calling sendTelemetryLog.", .warn) - } - } - - internal func telemetryDebug(id: String, message: String) { - return Datadog._internal.telemetry.debug(id: id, message: message) - } - - internal func telemetryError(id: String, message: String, kind: String?, stack: String?) { - return Datadog._internal.telemetry.error(id: id, message: message, kind: kind, stack: stack) - } - - internal func overrideTelemetryConfiguration( - initializationType: String? = nil, - reactNativeVersion: String? = nil, - reactVersion: String? = nil, - trackCrossPlatformLongTasks: Bool? = nil, - trackErrors: Bool? = nil, - trackInteractions: Bool? = nil, - trackLongTask: Bool? = nil, - trackNativeErrors: Bool? = nil, - trackNativeLongTasks: Bool? = nil, - trackNetworkRequests: Bool? = nil - ) { - coreInstance?.telemetry.configuration( - initializationType: initializationType, - reactNativeVersion: reactNativeVersion, - reactVersion: reactVersion, - trackCrossPlatformLongTasks: trackCrossPlatformLongTasks, - trackErrors: trackErrors, - trackLongTask: trackLongTask, - trackNativeErrors: trackNativeErrors, - trackNativeLongTasks: trackNativeLongTasks, - trackNetworkRequests: trackNetworkRequests, - trackUserInteractions: trackInteractions - ) - } - - #if os(iOS) -======= ->>>>>>> 0443e0ff (iOS: Always use SDK default core instance) // Webview private var webviewMessageEmitter: InternalExtension.AbstractMessageEmitter? diff --git a/packages/core/ios/Sources/DdSdkImplementation.swift b/packages/core/ios/Sources/DdSdkImplementation.swift index 9c3fe980f..997068ddf 100644 --- a/packages/core/ios/Sources/DdSdkImplementation.swift +++ b/packages/core/ios/Sources/DdSdkImplementation.swift @@ -175,33 +175,21 @@ public class DdSdkImplementation: NSObject { ) { let castedAttributes = castAttributesToSwift(attributes) let castedConfig = castAttributesToSwift(config) - DdTelemetry.sendTelemetryLog( - message: message as String, attributes: castedAttributes, config: castedConfig) + DdTelemetry.sendTelemetryLog(message: message as String, attributes: castedAttributes, config: castedConfig) resolve(nil) } @objc - public func telemetryDebug( - message: NSString, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock - ) { - DdTelemetry.telemetryDebug( - id: "datadog_react_native:\(message)", message: message as String) + + public func telemetryDebug(message: NSString, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { + DdTelemetry.telemetryDebug(id: "datadog_react_native:\(message)", message: message as String) resolve(nil) } -<<<<<<< HEAD #if os(iOS) -======= - ->>>>>>> 0443e0ff (iOS: Always use SDK default core instance) @objc - public func telemetryError( - message: NSString, stack: NSString, kind: NSString, resolve: RCTPromiseResolveBlock, - reject: RCTPromiseRejectBlock - ) { - DdTelemetry.telemetryError( - id: "datadog_react_native:\(String(describing: kind)):\(message)", - message: message as String, kind: kind as String, stack: stack as String) + public func telemetryError(message: NSString, stack: NSString, kind: NSString, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { + DdTelemetry.telemetryError(id: "datadog_react_native:\(String(describing: kind)):\(message)", message: message as String, kind: kind as String, stack: stack as String) resolve(nil) } @@ -221,24 +209,19 @@ public class DdSdkImplementation: NSObject { resolve(nil) } -<<<<<<< HEAD #endif -======= ->>>>>>> 1f781a51 (Expose view Attributes API) @objc - public func clearAllData(resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) { + public func clearAllData(resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { Datadog.clearAllData() resolve(nil) } - func overrideReactNativeTelemetry(rnConfiguration: DdSdkConfiguration) { + func overrideReactNativeTelemetry(rnConfiguration: DdSdkConfiguration) -> Void { DdTelemetry.overrideTelemetryConfiguration( - initializationType: rnConfiguration.configurationForTelemetry?.initializationType - as? String, - reactNativeVersion: rnConfiguration.configurationForTelemetry?.reactNativeVersion - as? String, + initializationType: rnConfiguration.configurationForTelemetry?.initializationType as? String, + reactNativeVersion: rnConfiguration.configurationForTelemetry?.reactNativeVersion as? String, reactVersion: rnConfiguration.configurationForTelemetry?.reactVersion as? String, trackCrossPlatformLongTasks: rnConfiguration.longTaskThresholdMs != 0, trackErrors: rnConfiguration.configurationForTelemetry?.trackErrors, diff --git a/packages/core/ios/Sources/DdSdkNativeInitialization.swift b/packages/core/ios/Sources/DdSdkNativeInitialization.swift index 4ea229871..b55f90561 100644 --- a/packages/core/ios/Sources/DdSdkNativeInitialization.swift +++ b/packages/core/ios/Sources/DdSdkNativeInitialization.swift @@ -94,12 +94,15 @@ public class DdSdkNativeInitialization: NSObject { CrashReporting.enable() } <<<<<<< HEAD +<<<<<<< HEAD #if os(iOS) DatadogSDKWrapper.shared.enableWebviewTracking() #endif ======= >>>>>>> 0443e0ff (iOS: Always use SDK default core instance) +======= +>>>>>>> 93aa6125 (iOS: Always use SDK default core instance) } func buildSDKConfiguration(configuration: DdSdkConfiguration, defaultAppVersion: String = getDefaultAppVersion()) -> Datadog.Configuration { From 846118cd8c6271fe976eee53d1b52108718f8c4a Mon Sep 17 00:00:00 2001 From: Sergio Barrio Date: Mon, 8 Sep 2025 15:27:17 +0200 Subject: [PATCH 080/410] Bump native SDK dependencies to 3.0.0 --- benchmarks/android/app/build.gradle | 2 +- benchmarks/ios/Podfile.lock | 178 ++++++++-------- example-new-architecture/ios/Podfile.lock | 156 +++++++------- example/ios/Podfile.lock | 196 +++++++++--------- packages/core/DatadogSDKReactNative.podspec | 12 +- packages/core/android/build.gradle | 10 +- ...DatadogSDKReactNativeSessionReplay.podspec | 2 +- .../android/build.gradle | 4 +- .../DatadogSDKReactNativeWebView.podspec | 4 +- .../react-native-webview/android/build.gradle | 2 +- 10 files changed, 283 insertions(+), 283 deletions(-) diff --git a/benchmarks/android/app/build.gradle b/benchmarks/android/app/build.gradle index 6d9f28764..7dc4b3707 100644 --- a/benchmarks/android/app/build.gradle +++ b/benchmarks/android/app/build.gradle @@ -129,5 +129,5 @@ dependencies { // Benchmark tools from dd-sdk-android are used for vitals recording // Remember to bump thid alongside the main dd-sdk-android dependencies - implementation("com.datadoghq:dd-sdk-android-benchmark-internal:3.2.0") + implementation("com.datadoghq:dd-sdk-android-benchmark-internal:3.0.0") } diff --git a/benchmarks/ios/Podfile.lock b/benchmarks/ios/Podfile.lock index 403441ec6..16194127d 100644 --- a/benchmarks/ios/Podfile.lock +++ b/benchmarks/ios/Podfile.lock @@ -1,22 +1,22 @@ PODS: - boost (1.84.0) - - DatadogCore (3.1.0): - - DatadogInternal (= 3.1.0) - - DatadogCrashReporting (3.1.0): - - DatadogInternal (= 3.1.0) + - DatadogCore (3.0.0): + - DatadogInternal (= 3.0.0) + - DatadogCrashReporting (3.0.0): + - DatadogInternal (= 3.0.0) - PLCrashReporter (~> 1.12.0) - - DatadogInternal (3.1.0) - - DatadogLogs (3.1.0): - - DatadogInternal (= 3.1.0) - - DatadogRUM (3.1.0): - - DatadogInternal (= 3.1.0) - - DatadogSDKReactNative (2.13.0): - - DatadogCore (= 3.1.0) - - DatadogCrashReporting (= 3.1.0) - - DatadogLogs (= 3.1.0) - - DatadogRUM (= 3.1.0) - - DatadogTrace (= 3.1.0) - - DatadogWebViewTracking (= 3.1.0) + - DatadogInternal (3.0.0) + - DatadogLogs (3.0.0): + - DatadogInternal (= 3.0.0) + - DatadogRUM (3.0.0): + - DatadogInternal (= 3.0.0) + - DatadogSDKReactNative (2.12.1): + - DatadogCore (= 3.0.0) + - DatadogCrashReporting (= 3.0.0) + - DatadogLogs (= 3.0.0) + - DatadogRUM (= 3.0.0) + - DatadogTrace (= 3.0.0) + - DatadogWebViewTracking (= 3.0.0) - DoubleConversion - glog - hermes-engine @@ -39,7 +39,7 @@ PODS: - Yoga - DatadogSDKReactNativeSessionReplay (2.13.2): - DatadogSDKReactNative - - DatadogSessionReplay (= 3.1.0) + - DatadogSessionReplay (= 3.0.0) - DoubleConversion - glog - hermes-engine @@ -60,10 +60,10 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - DatadogSDKReactNativeWebView (2.13.0): - - DatadogInternal (= 3.1.0) + - DatadogSDKReactNativeWebView (2.12.1): + - DatadogInternal (= 3.0.0) - DatadogSDKReactNative - - DatadogWebViewTracking (= 3.1.0) + - DatadogWebViewTracking (= 3.0.0) - DoubleConversion - glog - hermes-engine @@ -84,13 +84,13 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - DatadogSessionReplay (3.1.0): - - DatadogInternal (= 3.1.0) - - DatadogTrace (3.1.0): - - DatadogInternal (= 3.1.0) + - DatadogSessionReplay (3.0.0): + - DatadogInternal (= 3.0.0) + - DatadogTrace (3.0.0): + - DatadogInternal (= 3.0.0) - OpenTelemetrySwiftApi (= 1.13.1) - - DatadogWebViewTracking (3.1.0): - - DatadogInternal (= 3.1.0) + - DatadogWebViewTracking (3.0.0): + - DatadogInternal (= 3.0.0) - DoubleConversion (1.1.6) - fast_float (6.1.4) - FBLazyVector (0.78.2) @@ -2070,17 +2070,17 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: boost: 7e761d76ca2ce687f7cc98e698152abd03a18f90 - DatadogCore: d2f51c7fb4308cf3c25e55e2e7242e5d558ee71d - DatadogCrashReporting: f636f1d1c534572c0b0abcdc59df244c884d825d - DatadogInternal: 7837b2ce3d525d429682532eeda697b181299fdc - DatadogLogs: 250894b5a99da5b924a019049c0d0326823cdbd6 - DatadogRUM: 0d2a60e1abb8aacfb8827ef84f6d5deb4d5026c8 - DatadogSDKReactNative: 620018df2896abcfad6b338c633cc8eccd5de406 - DatadogSDKReactNativeSessionReplay: b2ef22431dd0816adea8d65df13180cf40533f9d - DatadogSDKReactNativeWebView: 299629cf348a5e8f1dabb8289920a00eee625d6a - DatadogSessionReplay: 6bc71888e2b41dd0de3325f06f0c0b3cee0e6df4 - DatadogTrace: f59e933074cd285ad7e9f5af991f8fe04b095991 - DatadogWebViewTracking: 9bc92b4147aeed47eb1911451f651094aa6dd6c1 + DatadogCore: 0c39ba4ef3dee02071f235f2fb56af7e29f4ceb0 + DatadogCrashReporting: 604e65e524cb2fc22cda39fd9c9ea5fd3bddf24e + DatadogInternal: 442673a7fb5329299b489b91ed705aa2085380f7 + DatadogLogs: 0146a0e3140ba62fd42729593c0910d692aea5fd + DatadogRUM: f97fbd0290ecec5bc952fb03a6a1ae068649243f + DatadogSDKReactNative: 241bf982c16ceff03d94a58e6d005e55c47b99c6 + DatadogSDKReactNativeSessionReplay: bba36092686e3183e97c1a0c7f4ca8142582ae28 + DatadogSDKReactNativeWebView: 6da060df20e235abac533e582d9fc2b3a5070840 + DatadogSessionReplay: 0357b911c6ab42c77c93b29761ecc20d9282e971 + DatadogTrace: d714e7c456e612b7f171483d08086a59aa1c4213 + DatadogWebViewTracking: fb9591c09fca82b143f3843a7164a7e782175c53 DoubleConversion: cb417026b2400c8f53ae97020b2be961b59470cb fast_float: 06eeec4fe712a76acc9376682e4808b05ce978b6 FBLazyVector: e32d34492c519a2194ec9d7f5e7a79d11b73f91c @@ -2089,70 +2089,70 @@ SPEC CHECKSUMS: hermes-engine: 2771b98fb813fdc6f92edd7c9c0035ecabf9fee7 OpenTelemetrySwiftApi: aaee576ed961e0c348af78df58b61300e95bd104 PLCrashReporter: db59ef96fa3d25f3650040d02ec2798cffee75f2 - RCT-Folly: 36fe2295e44b10d831836cc0d1daec5f8abcf809 + RCT-Folly: e78785aa9ba2ed998ea4151e314036f6c49e6d82 RCTDeprecation: be794de7dc6ed8f9f7fbf525f86e7651b8b68746 RCTRequired: a83787b092ec554c2eb6019ff3f5b8d125472b3b RCTTypeSafety: 48ad3c858926b1c46f46a81a58822b476e178e2c React: 3b5754191f1b65f1dbc52fbea7959c3d2d9e39c9 React-callinvoker: 6beeaf4c7db11b6cc953fac45f2c76e3fb125013 - React-Core: 88e817c42de035378cc71e009193b9a044d3f595 - React-CoreModules: dcf764d71efb4f75d38fcae8d4513b6729f49360 - React-cxxreact: 8cdcc937c5fbc406fe843a381102fd69440ca78a + React-Core: 8a10ac9de53373a3ecb5dfcbcf56df1d3dad0861 + React-CoreModules: af6999b35c7c01b0e12b59d27f3e054e13da43b1 + React-cxxreact: 833f00155ce8c2fda17f6d286f8eaeff2ececc69 React-debug: 440175830c448e7e53e61ebb8d8468c3256b645e - React-defaultsnativemodule: 4824bcd7b96ee2d75c28b1ca21f58976867f5535 - React-domnativemodule: a421118b475618961cf282e8ea85347cc9bb453c - React-Fabric: 6ac7de06009eb96b609a770b17abba6e460b5f45 - React-FabricComponents: e3bc2680a5a9a4917ff0c8d7f390688c30ef753c - React-FabricImage: 8bad558dec7478077974caa96acc79692d6b71f5 + React-defaultsnativemodule: a970effe18fe50bdbbb7115c3297f873b666d0d4 + React-domnativemodule: 45f886342a724e61531b18fba1859bb6782e5d62 + React-Fabric: 69f1881f2177a8512304a64157943548ab6df0cf + React-FabricComponents: f54111c8e2439fc273ab07483e3a7054ca1e75af + React-FabricImage: 9ad2619dfe8c386d79e8aaa87da6e8f018ab9592 React-featureflags: b9cf9b35baca1c7f20c06a104ffc325a02752faa - React-featureflagsnativemodule: dc93d81da9f41f7132e24455ec8b4b60802fd5b0 - React-graphics: aaa5a38bea15d7b895b210d95d554af45a07002a - React-hermes: 08ad9fb832d1b9faef391be17309aa6a69fad23b - React-idlecallbacksnativemodule: aacea33ef6c511a9781f9286cc7cdf93f39bba14 - React-ImageManager: c596c3b658c9c14607f9183ed0f635c8dd77987c - React-jserrorhandler: 987609b2f16b7d79d63fcd621bf0110dd7400b35 - React-jsi: afa286d7e0c102c2478dc420d4f8935e13c973fc - React-jsiexecutor: 08f5b512b4db9e2f147416d60a0a797576b9cfef - React-jsinspector: 5a94bcae66e3637711c4d96a00038ab9ec935bf5 - React-jsinspectortracing: a12589a0adbb2703cbc4380dabe9a58800810923 - React-jsitracing: 0b1a403d7757cec66b7dd8b308d04db85eef75f3 - React-logger: 304814ae37503c8eb54359851cc55bd4f936b39c - React-Mapbuffer: b588d1ca18d2ce626f868f04ab12d8b1f004f12c - React-microtasksnativemodule: 11831d070aa47755bb5739069eb04ec621fec548 - react-native-config: 3367df9c1f25bb96197007ec531c7087ed4554c3 - react-native-safe-area-context: 9b169299f9dc95f1d7fe1dd266fde53bd899cd0c - react-native-slider: 27263d134d55db948a4706f1e47d0ec88fb354dd - react-native-webview: be9957759cb73cb64f2ed5359e32a85f1f5bdff8 - React-NativeModulesApple: 79a4404ac301b40bec3b367879c5e9a9ce81683c - React-perflogger: 0ea25c109dba33d47dec36b2634bf7ea67c1a555 - React-performancetimeline: f74480de6efbcd8541c34317c0baedb433f27296 + React-featureflagsnativemodule: 7f1bc76d1d2c5bede5e753b8d188dbde7c59b12f + React-graphics: 069e0d0b31ed1e80feb023ad4f7e97f00e84f7b9 + React-hermes: 63df5ac5a944889c8758a6213b39ed825863adb7 + React-idlecallbacksnativemodule: 4c700bd7c0012adf904929075a79418b828b5ffc + React-ImageManager: 5d1ba8a7bae44ebba43fc93da64937c713d42941 + React-jserrorhandler: 0defd58f8bb797cdd0a820f733bf42d8bee708ce + React-jsi: 99d6207ec802ad73473a0dad3c9ad48cd98463f6 + React-jsiexecutor: 8c8097b4ba7e7f480582d6e6238b01be5dcc01c0 + React-jsinspector: ea148ec45bc7ff830e443383ea715f9780c15934 + React-jsinspectortracing: 46bb2841982f01e7b63eaab98140fa1de5b2a1db + React-jsitracing: c1063fc2233960d1c8322291e74bca51d25c10d7 + React-logger: 763728cf4eebc9c5dc9bfc3649e22295784f69f3 + React-Mapbuffer: 63278529b5cf531a7eaf8fc71244fabb062ca90c + React-microtasksnativemodule: 6a39463c32ce831c4c2aa8469273114d894b6be9 + react-native-config: 644074ab88db883fcfaa584f03520ec29589d7df + react-native-safe-area-context: afcc2e2b3e78ae8ef90d81e658aacee34ebc27ea + react-native-slider: 310d3f89edd6ca8344a974bfe83a29a3fbb60e5a + react-native-webview: 80ef603d1df42e24fdde765686fbb9b8a6ecd554 + React-NativeModulesApple: fd0545efbb7f936f78edd15a6564a72d2c34bb32 + React-perflogger: 5f8fa36a8e168fb355efe72099efe77213bc2ac6 + React-performancetimeline: 8c0ecfa1ae459cc5678a65f95ac3bf85644d6feb React-RCTActionSheet: 2ef95837e89b9b154f13cd8401f9054fc3076aff - React-RCTAnimation: 33d960d7f58a81779eea6dea47ad0364c67e1517 - React-RCTAppDelegate: 85c13403fd6f6b6cc630428d52bd8bd76a670dc9 - React-RCTBlob: 74c986a02d951931d2f6ed0e07ed5a7eb385bfc0 - React-RCTFabric: 384a8fea4f22fc0f21299d771971862883ba630a - React-RCTFBReactNativeSpec: eb1c3ec5149f76133593a516ff9d5efe32ebcecd - React-RCTImage: 2c58b5ddeb3c65e52f942bbe13ff9c59bd649b09 - React-RCTLinking: b6b14f8a3e62c02fc627ac4f3fb0c7bd941f907c - React-RCTNetwork: 1d050f2466c1541b339587d46f78d5eee218d626 - React-RCTSettings: 8148f6be0ccc0cfe6e313417ebf8a479caaa2146 - React-RCTText: 64114531ad1359e4e02a4a8af60df606dbbabc25 - React-RCTVibration: f4859417a7dd859b6bf18b1aba897e52beb72ef6 + React-RCTAnimation: 46abefd5acfda7e6629f9e153646deecc70babd2 + React-RCTAppDelegate: 7e58e0299e304cceee3f7019fa77bc6990f66b22 + React-RCTBlob: f68c63a801ef1d27e83c4011e3b083cc86a200d7 + React-RCTFabric: c59f41d0c4edbaac8baa232731ca09925ae4dda7 + React-RCTFBReactNativeSpec: 3240b9b8d792aa4be0fb85c9898fc183125ba8de + React-RCTImage: 34e0bba1507e55f1c614bd759eb91d9be48c8c5b + React-RCTLinking: a0b6c9f4871c18b0b81ea952f43e752718bd5f1d + React-RCTNetwork: bdafd661ac2b20d23b779e45bf7ac3e4c8bd1b60 + React-RCTSettings: 98aa5163796f43789314787b584a84eba47787a9 + React-RCTText: 424a274fc9015b29de89cf3cbcdf4dd85dd69f83 + React-RCTVibration: 92d9875a955b0adb34b4b773528fdbbbc5addd6c React-rendererconsistency: 5ac4164ec18cfdd76ed5f864dbfdc56a5a948bc9 - React-rendererdebug: 3dc1d97bbee0c0c13191e501a96ed9325bbd920e + React-rendererdebug: 710dbd7990e355852c786aa6bc7753f6028f357a React-rncore: 0bace3b991d8843bb5b57c5f2301ec6e9c94718b - React-RuntimeApple: 1e1e0a0c6086bc8c3b07e8f1a2f6ca99b50419a0 - React-RuntimeCore: d39322c59bef2a4b343fda663d20649f29f57fcc + React-RuntimeApple: 701ec44a8b5d863ee9b6a2b2447b6a26bb6805a1 + React-RuntimeCore: a82767065b9a936b05e209dc6987bc1ea9eb5d2d React-runtimeexecutor: 876dfc1d8daa819dfd039c40f78f277c5a3e66a6 - React-RuntimeHermes: 44f5f2baf039f249b31ea4f3e224484fd1731e0e - React-runtimescheduler: 3b3c5b50743bb8743ca49b9e5a70c2c385f156e1 + React-RuntimeHermes: e7a051fd91cab8849df56ac917022ef6064ad621 + React-runtimescheduler: c544141f2124ee3d5f3d5bf0d69f4029a61a68b0 React-timing: 1ee3572c398f5579c9df5bf76aacddf5683ff74e - React-utils: 0cfb7c7fb37d4e5f31cc18ffc7426be0ae6bf907 - ReactAppDependencyProvider: b48473fe434569ff8f6cb6ed4421217ebcbda878 - ReactCodegen: 653a0d8532d8c7dab50c391392044d98e20c9f79 - ReactCommon: 547db015202a80a5b3e7e041586ea54c4a087180 - RNCPicker: ffbd7b9fc7c1341929e61dbef6219f7860f57418 - RNScreens: 0f01bbed9bd8045a8d58e4b46993c28c7f498f3c + React-utils: 18703928768cb37e70cf2efff09def12d74a399e + ReactAppDependencyProvider: 4893bde33952f997a323eb1a1ee87a72764018ff + ReactCodegen: da30aff1cea9b5993dcbc33bf1ef47a463c55194 + ReactCommon: 865ebe76504a95e115b6229dd00a31e56d2d4bfe + RNCPicker: cfb51a08c6e10357d9a65832e791825b0747b483 + RNScreens: 790123c4a28783d80a342ce42e8c7381bed62db1 SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 Yoga: e14bad835e12b6c7e2260fc320bd00e0f4b45add diff --git a/example-new-architecture/ios/Podfile.lock b/example-new-architecture/ios/Podfile.lock index 051f25e11..281513566 100644 --- a/example-new-architecture/ios/Podfile.lock +++ b/example-new-architecture/ios/Podfile.lock @@ -1,22 +1,22 @@ PODS: - boost (1.84.0) - - DatadogCore (3.1.0): - - DatadogInternal (= 3.1.0) - - DatadogCrashReporting (3.1.0): - - DatadogInternal (= 3.1.0) + - DatadogCore (3.0.0): + - DatadogInternal (= 3.0.0) + - DatadogCrashReporting (3.0.0): + - DatadogInternal (= 3.0.0) - PLCrashReporter (~> 1.12.0) - - DatadogInternal (3.1.0) - - DatadogLogs (3.1.0): - - DatadogInternal (= 3.1.0) - - DatadogRUM (3.1.0): - - DatadogInternal (= 3.1.0) - - DatadogSDKReactNative (2.13.0): - - DatadogCore (= 3.1.0) - - DatadogCrashReporting (= 3.1.0) - - DatadogLogs (= 3.1.0) - - DatadogRUM (= 3.1.0) - - DatadogTrace (= 3.1.0) - - DatadogWebViewTracking (= 3.1.0) + - DatadogInternal (3.0.0) + - DatadogLogs (3.0.0): + - DatadogInternal (= 3.0.0) + - DatadogRUM (3.0.0): + - DatadogInternal (= 3.0.0) + - DatadogSDKReactNative (2.12.1): + - DatadogCore (= 3.0.0) + - DatadogCrashReporting (= 3.0.0) + - DatadogLogs (= 3.0.0) + - DatadogRUM (= 3.0.0) + - DatadogTrace (= 3.0.0) + - DatadogWebViewTracking (= 3.0.0) - DoubleConversion - glog - hermes-engine @@ -37,13 +37,13 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - DatadogSDKReactNative/Tests (2.13.0): - - DatadogCore (= 3.1.0) - - DatadogCrashReporting (= 3.1.0) - - DatadogLogs (= 3.1.0) - - DatadogRUM (= 3.1.0) - - DatadogTrace (= 3.1.0) - - DatadogWebViewTracking (= 3.1.0) + - DatadogSDKReactNative/Tests (2.12.1): + - DatadogCore (= 3.0.0) + - DatadogCrashReporting (= 3.0.0) + - DatadogLogs (= 3.0.0) + - DatadogRUM (= 3.0.0) + - DatadogTrace (= 3.0.0) + - DatadogWebViewTracking (= 3.0.0) - DoubleConversion - glog - hermes-engine @@ -64,11 +64,11 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - DatadogTrace (3.1.0): - - DatadogInternal (= 3.1.0) + - DatadogTrace (3.0.0): + - DatadogInternal (= 3.0.0) - OpenTelemetrySwiftApi (= 1.13.1) - - DatadogWebViewTracking (3.1.0): - - DatadogInternal (= 3.1.0) + - DatadogWebViewTracking (3.0.0): + - DatadogInternal (= 3.0.0) - DoubleConversion (1.1.6) - fast_float (6.1.4) - FBLazyVector (0.76.9) @@ -1850,14 +1850,14 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: boost: 1dca942403ed9342f98334bf4c3621f011aa7946 - DatadogCore: d2f51c7fb4308cf3c25e55e2e7242e5d558ee71d - DatadogCrashReporting: f636f1d1c534572c0b0abcdc59df244c884d825d - DatadogInternal: 7837b2ce3d525d429682532eeda697b181299fdc - DatadogLogs: 250894b5a99da5b924a019049c0d0326823cdbd6 - DatadogRUM: 0d2a60e1abb8aacfb8827ef84f6d5deb4d5026c8 - DatadogSDKReactNative: 2f11191b56e18680f633bfb125ab1832b327d9b4 - DatadogTrace: f59e933074cd285ad7e9f5af991f8fe04b095991 - DatadogWebViewTracking: 9bc92b4147aeed47eb1911451f651094aa6dd6c1 + DatadogCore: 0c39ba4ef3dee02071f235f2fb56af7e29f4ceb0 + DatadogCrashReporting: 604e65e524cb2fc22cda39fd9c9ea5fd3bddf24e + DatadogInternal: 442673a7fb5329299b489b91ed705aa2085380f7 + DatadogLogs: 0146a0e3140ba62fd42729593c0910d692aea5fd + DatadogRUM: f97fbd0290ecec5bc952fb03a6a1ae068649243f + DatadogSDKReactNative: 0a80aa75958d595a99be54d2838db53eda7a08af + DatadogTrace: d714e7c456e612b7f171483d08086a59aa1c4213 + DatadogWebViewTracking: fb9591c09fca82b143f3843a7164a7e782175c53 DoubleConversion: f16ae600a246532c4020132d54af21d0ddb2a385 fast_float: 06eeec4fe712a76acc9376682e4808b05ce978b6 FBLazyVector: 7605ea4810e0e10ae4815292433c09bf4324ba45 @@ -1866,62 +1866,62 @@ SPEC CHECKSUMS: hermes-engine: 9e868dc7be781364296d6ee2f56d0c1a9ef0bb11 OpenTelemetrySwiftApi: aaee576ed961e0c348af78df58b61300e95bd104 PLCrashReporter: db59ef96fa3d25f3650040d02ec2798cffee75f2 - RCT-Folly: 7b4f73a92ad9571b9dbdb05bb30fad927fa971e1 + RCT-Folly: ea9d9256ba7f9322ef911169a9f696e5857b9e17 RCTDeprecation: ebe712bb05077934b16c6bf25228bdec34b64f83 RCTRequired: ca91e5dd26b64f577b528044c962baf171c6b716 RCTTypeSafety: e7678bd60850ca5a41df9b8dc7154638cb66871f React: 4641770499c39f45d4e7cde1eba30e081f9d8a3d React-callinvoker: 4bef67b5c7f3f68db5929ab6a4d44b8a002998ea - React-Core: 0a06707a0b34982efc4a556aff5dae4b22863455 - React-CoreModules: 907334e94314189c2e5eed4877f3efe7b26d85b0 - React-cxxreact: 3a1d5e8f4faa5e09be26614e9c8bbcae8d11b73d + React-Core: a68cea3e762814e60ecc3fa521c7f14c36c99245 + React-CoreModules: d81b1eaf8066add66299bab9d23c9f00c9484c7c + React-cxxreact: 984f8b1feeca37181d4e95301fcd6f5f6501c6ab React-debug: 817160c07dc8d24d020fbd1eac7b3558ffc08964 - React-defaultsnativemodule: 814830ccbc3fb08d67d0190e63b179ee4098c67b - React-domnativemodule: 270acf94bd0960b026bc3bfb327e703665d27fb4 - React-Fabric: 64586dc191fc1c170372a638b8e722e4f1d0a09b - React-FabricComponents: b0ebd032387468ea700574c581b139f57a7497fb - React-FabricImage: 81f0e0794caf25ad1224fa406d288fbc1986607f + React-defaultsnativemodule: 18a684542f82ce1897552a1c4b847be414c9566e + React-domnativemodule: 90bdd4ec3ab38c47cfc3461c1e9283a8507d613f + React-Fabric: f6dade7007533daeb785ba5925039d83f343be4b + React-FabricComponents: b0655cc3e1b5ae12a4a1119aa7d8308f0ad33520 + React-FabricImage: 9b157c4c01ac2bf433f834f0e1e5fe234113a576 React-featureflags: f2792b067a351d86fdc7bec23db3b9a2f2c8d26c - React-featureflagsnativemodule: 0d7091ae344d6160c0557048e127897654a5c00f - React-graphics: cbebe910e4a15b65b0bff94a4d3ed278894d6386 - React-hermes: ec18c10f5a69d49fb9b5e17ae95494e9ea13d4d3 - React-idlecallbacksnativemodule: 6b84add48971da9c40403bd1860d4896462590f2 - React-ImageManager: f2a4c01c2ccb2193e60a20c135da74c7ca4d36f2 - React-jserrorhandler: 61d205b5a7cbc57fed3371dd7eed48c97f49fc64 - React-jsi: 95f7676103137861b79b0f319467627bcfa629ee - React-jsiexecutor: 41e0fe87cda9ea3970ffb872ef10f1ff8dbd1932 - React-jsinspector: 15578208796723e5c6f39069b6e8bf36863ef6e2 - React-jsitracing: 3758cdb155ea7711f0e77952572ea62d90c69f0b - React-logger: dbca7bdfd4aa5ef69431362bde6b36d49403cb20 - React-Mapbuffer: 6efad4a606c1fae7e4a93385ee096681ef0300dc - React-microtasksnativemodule: a645237a841d733861c70b69908ab4a1707b52ad + React-featureflagsnativemodule: 742a8325b3c821d2a1ca13a6d2a0fc72d04555e0 + React-graphics: 68969e4e49d73f89da7abef4116c9b5f466aa121 + React-hermes: ac0bcba26a5d288ebc99b500e1097da2d0297ddf + React-idlecallbacksnativemodule: d61d9c9816131bf70d3d80cd04889fc625ee523f + React-ImageManager: e906eec93a9eb6102a06576b89d48d80a4683020 + React-jserrorhandler: ac5dde01104ff444e043cad8f574ca02756e20d6 + React-jsi: 496fa2b9d63b726aeb07d0ac800064617d71211d + React-jsiexecutor: dd22ab48371b80f37a0a30d0e8915b6d0f43a893 + React-jsinspector: 4629ac376f5765e684d19064f2093e55c97fd086 + React-jsitracing: 7a1c9cd484248870cf660733cd3b8114d54c035f + React-logger: c4052eb941cca9a097ef01b59543a656dc088559 + React-Mapbuffer: 33546a3ebefbccb8770c33a1f8a5554fa96a54de + React-microtasksnativemodule: d80ff86c8902872d397d9622f1a97aadcc12cead React-nativeconfig: 8efdb1ef1e9158c77098a93085438f7e7b463678 - React-NativeModulesApple: 958d4f6c5c2ace4c0f427cf7ef82e28ae6538a22 - React-perflogger: 9b4f13c0afe56bc7b4a0e93ec74b1150421ee22d - React-performancetimeline: 359db1cb889aa0282fafc5838331b0987c4915a9 + React-NativeModulesApple: cebca2e5320a3d66e123cade23bd90a167ffce5e + React-perflogger: 72e653eb3aba9122f9e57cf012d22d2486f33358 + React-performancetimeline: cd6a9374a72001165995d2ab632f672df04076dc React-RCTActionSheet: aacf2375084dea6e7c221f4a727e579f732ff342 - React-RCTAnimation: d8c82deebebe3aaf7a843affac1b57cb2dc073d4 - React-RCTAppDelegate: 1774aa421a29a41a704ecaf789811ef73c4634b6 - React-RCTBlob: 70a58c11a6a3500d1a12f2e51ca4f6c99babcff8 - React-RCTFabric: 731cda82aed592aacce2d32ead69d78cde5d9274 - React-RCTImage: 5e9d655ba6a790c31e3176016f9b47fd0978fbf0 - React-RCTLinking: 2a48338252805091f7521eaf92687206401bdf2a - React-RCTNetwork: 0c1282b377257f6b1c81934f72d8a1d0c010e4c3 - React-RCTSettings: f757b679a74e5962be64ea08d7865a7debd67b40 - React-RCTText: e7d20c490b407d3b4a2daa48db4bcd8ec1032af2 - React-RCTVibration: 8228e37144ca3122a91f1de16ba8e0707159cfec + React-RCTAnimation: 395ab53fd064dff81507c15efb781c8684d9a585 + React-RCTAppDelegate: 345a6f1b82abc578437df0ce7e9c48740eca827c + React-RCTBlob: 13311e554c1a367de063c10ee7c5e6573b2dd1d6 + React-RCTFabric: 007b1a98201cc49b5bc6e1417d7fe3f6fc6e2b78 + React-RCTImage: 1b1f914bcc12187c49ba5d949dac38c2eb9f5cc8 + React-RCTLinking: 4ac7c42beb65e36fba0376f3498f3cd8dd0be7fa + React-RCTNetwork: 938902773add4381e84426a7aa17a2414f5f94f7 + React-RCTSettings: e848f1ba17a7a18479cf5a31d28145f567da8223 + React-RCTText: 7e98fafdde7d29e888b80f0b35544e0cb07913cf + React-RCTVibration: cd7d80affd97dc7afa62f9acd491419558b64b78 React-rendererconsistency: b4917053ecbaa91469c67a4319701c9dc0d40be6 - React-rendererdebug: 81becbc8852b38d9b1b68672aa504556481330d5 + React-rendererdebug: aa181c36dd6cf5b35511d1ed875d6638fd38f0ec React-rncore: 120d21715c9b4ba8f798bffe986cb769b988dd74 - React-RuntimeApple: 52ed0e9e84a7c2607a901149fb13599a3c057655 - React-RuntimeCore: ca6189d2e53d86db826e2673fe8af6571b8be157 + React-RuntimeApple: d033becbbd1eba6f9f6e3af6f1893030ce203edd + React-RuntimeCore: 38af280bb678e66ba000a3c3d42920b2a138eebb React-runtimeexecutor: 877596f82f5632d073e121cba2d2084b76a76899 - React-RuntimeHermes: 3b752dc5d8a1661c9d1687391d6d96acfa385549 - React-runtimescheduler: 8321bb09175ace2a4f0b3e3834637eb85bf42ebe + React-RuntimeHermes: 37aad735ff21ca6de2d8450a96de1afe9f86c385 + React-runtimescheduler: 8ec34cc885281a34696ea16c4fd86892d631f38d React-timing: 331cbf9f2668c67faddfd2e46bb7f41cbd9320b9 - React-utils: 54df9ada708578c8ad40d92895d6fed03e0e8a9e - ReactCodegen: 21a52ccddc6479448fc91903a437dd23ddc7366c - ReactCommon: bfd3600989d79bc3acbe7704161b171a1480b9fd + React-utils: ed818f19ab445000d6b5c4efa9d462449326cc9f + ReactCodegen: f853a20cc9125c5521c8766b4b49375fec20648b + ReactCommon: 300d8d9c5cb1a6cd79a67cf5d8f91e4d477195f9 SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 Yoga: feb4910aba9742cfedc059e2b2902e22ffe9954a diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index d0ba99782..fac339d80 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -1,34 +1,34 @@ PODS: - boost (1.84.0) - - DatadogCore (3.1.0): - - DatadogInternal (= 3.1.0) - - DatadogCrashReporting (3.1.0): - - DatadogInternal (= 3.1.0) + - DatadogCore (3.0.0): + - DatadogInternal (= 3.0.0) + - DatadogCrashReporting (3.0.0): + - DatadogInternal (= 3.0.0) - PLCrashReporter (~> 1.12.0) - - DatadogInternal (3.1.0) - - DatadogLogs (3.1.0): - - DatadogInternal (= 3.1.0) - - DatadogRUM (3.1.0): - - DatadogInternal (= 3.1.0) - - DatadogSDKReactNative (2.13.0): - - DatadogCore (= 3.1.0) - - DatadogCrashReporting (= 3.1.0) - - DatadogLogs (= 3.1.0) - - DatadogRUM (= 3.1.0) - - DatadogTrace (= 3.1.0) - - DatadogWebViewTracking (= 3.1.0) + - DatadogInternal (3.0.0) + - DatadogLogs (3.0.0): + - DatadogInternal (= 3.0.0) + - DatadogRUM (3.0.0): + - DatadogInternal (= 3.0.0) + - DatadogSDKReactNative (2.12.1): + - DatadogCore (= 3.0.0) + - DatadogCrashReporting (= 3.0.0) + - DatadogLogs (= 3.0.0) + - DatadogRUM (= 3.0.0) + - DatadogTrace (= 3.0.0) + - DatadogWebViewTracking (= 3.0.0) - React-Core - - DatadogSDKReactNative/Tests (2.13.0): - - DatadogCore (= 3.1.0) - - DatadogCrashReporting (= 3.1.0) - - DatadogLogs (= 3.1.0) - - DatadogRUM (= 3.1.0) - - DatadogTrace (= 3.1.0) - - DatadogWebViewTracking (= 3.1.0) + - DatadogSDKReactNative/Tests (2.12.1): + - DatadogCore (= 3.0.0) + - DatadogCrashReporting (= 3.0.0) + - DatadogLogs (= 3.0.0) + - DatadogRUM (= 3.0.0) + - DatadogTrace (= 3.0.0) + - DatadogWebViewTracking (= 3.0.0) - React-Core - DatadogSDKReactNativeSessionReplay (2.13.2): - DatadogSDKReactNative - - DatadogSessionReplay (= 3.1.0) + - DatadogSessionReplay (= 3.0.0) - DoubleConversion - glog - hermes-engine @@ -51,7 +51,7 @@ PODS: - Yoga - DatadogSDKReactNativeSessionReplay/Tests (2.13.2): - DatadogSDKReactNative - - DatadogSessionReplay (= 3.1.0) + - DatadogSessionReplay (= 3.0.0) - DoubleConversion - glog - hermes-engine @@ -73,25 +73,25 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - DatadogSDKReactNativeWebView (2.13.0): - - DatadogInternal (= 3.1.0) + - DatadogSDKReactNativeWebView (2.12.1): + - DatadogInternal (= 3.0.0) - DatadogSDKReactNative - - DatadogWebViewTracking (= 3.1.0) + - DatadogWebViewTracking (= 3.0.0) - React-Core - - DatadogSDKReactNativeWebView/Tests (2.13.0): - - DatadogInternal (= 3.1.0) + - DatadogSDKReactNativeWebView/Tests (2.12.1): + - DatadogInternal (= 3.0.0) - DatadogSDKReactNative - - DatadogWebViewTracking (= 3.1.0) + - DatadogWebViewTracking (= 3.0.0) - React-Core - react-native-webview - React-RCTText - - DatadogSessionReplay (3.1.0): - - DatadogInternal (= 3.1.0) - - DatadogTrace (3.1.0): - - DatadogInternal (= 3.1.0) + - DatadogSessionReplay (3.0.0): + - DatadogInternal (= 3.0.0) + - DatadogTrace (3.0.0): + - DatadogInternal (= 3.0.0) - OpenTelemetrySwiftApi (= 1.13.1) - - DatadogWebViewTracking (3.1.0): - - DatadogInternal (= 3.1.0) + - DatadogWebViewTracking (3.0.0): + - DatadogInternal (= 3.0.0) - DoubleConversion (1.1.6) - fast_float (6.1.4) - FBLazyVector (0.76.9) @@ -1988,17 +1988,17 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: boost: 1dca942403ed9342f98334bf4c3621f011aa7946 - DatadogCore: d2f51c7fb4308cf3c25e55e2e7242e5d558ee71d - DatadogCrashReporting: f636f1d1c534572c0b0abcdc59df244c884d825d - DatadogInternal: 7837b2ce3d525d429682532eeda697b181299fdc - DatadogLogs: 250894b5a99da5b924a019049c0d0326823cdbd6 - DatadogRUM: 0d2a60e1abb8aacfb8827ef84f6d5deb4d5026c8 - DatadogSDKReactNative: 822ff8092666172584d4d5e56f79c3799887d408 - DatadogSDKReactNativeSessionReplay: afc4e2b1db34ba8af3a442b0691359faaf5e586e - DatadogSDKReactNativeWebView: 00affefdaca0cf2375e669fa03925d8fa75263d0 - DatadogSessionReplay: 6bc71888e2b41dd0de3325f06f0c0b3cee0e6df4 - DatadogTrace: f59e933074cd285ad7e9f5af991f8fe04b095991 - DatadogWebViewTracking: 9bc92b4147aeed47eb1911451f651094aa6dd6c1 + DatadogCore: 0c39ba4ef3dee02071f235f2fb56af7e29f4ceb0 + DatadogCrashReporting: 604e65e524cb2fc22cda39fd9c9ea5fd3bddf24e + DatadogInternal: 442673a7fb5329299b489b91ed705aa2085380f7 + DatadogLogs: 0146a0e3140ba62fd42729593c0910d692aea5fd + DatadogRUM: f97fbd0290ecec5bc952fb03a6a1ae068649243f + DatadogSDKReactNative: e430b3f4d7adb0fac07b61e2fb65ceacdaf82b3e + DatadogSDKReactNativeSessionReplay: 4b2a3d166a79581f18522795b40141c34cf3685d + DatadogSDKReactNativeWebView: 35dc2b9736e1aaa82b366bf6b8a8a959a9b088c5 + DatadogSessionReplay: 0357b911c6ab42c77c93b29761ecc20d9282e971 + DatadogTrace: d714e7c456e612b7f171483d08086a59aa1c4213 + DatadogWebViewTracking: fb9591c09fca82b143f3843a7164a7e782175c53 DoubleConversion: f16ae600a246532c4020132d54af21d0ddb2a385 fast_float: 06eeec4fe712a76acc9376682e4808b05ce978b6 FBLazyVector: 7605ea4810e0e10ae4815292433c09bf4324ba45 @@ -2008,69 +2008,69 @@ SPEC CHECKSUMS: HMSegmentedControl: 34c1f54d822d8308e7b24f5d901ec674dfa31352 OpenTelemetrySwiftApi: aaee576ed961e0c348af78df58b61300e95bd104 PLCrashReporter: db59ef96fa3d25f3650040d02ec2798cffee75f2 - RCT-Folly: 7b4f73a92ad9571b9dbdb05bb30fad927fa971e1 + RCT-Folly: ea9d9256ba7f9322ef911169a9f696e5857b9e17 RCTDeprecation: ebe712bb05077934b16c6bf25228bdec34b64f83 RCTRequired: ca91e5dd26b64f577b528044c962baf171c6b716 RCTTypeSafety: e7678bd60850ca5a41df9b8dc7154638cb66871f React: 4641770499c39f45d4e7cde1eba30e081f9d8a3d React-callinvoker: 4bef67b5c7f3f68db5929ab6a4d44b8a002998ea - React-Core: 0a06707a0b34982efc4a556aff5dae4b22863455 - React-CoreModules: 907334e94314189c2e5eed4877f3efe7b26d85b0 - React-cxxreact: 3a1d5e8f4faa5e09be26614e9c8bbcae8d11b73d + React-Core: a68cea3e762814e60ecc3fa521c7f14c36c99245 + React-CoreModules: d81b1eaf8066add66299bab9d23c9f00c9484c7c + React-cxxreact: 984f8b1feeca37181d4e95301fcd6f5f6501c6ab React-debug: 817160c07dc8d24d020fbd1eac7b3558ffc08964 - React-defaultsnativemodule: a965cb39fb0a79276ab611793d39f52e59a9a851 - React-domnativemodule: d647f94e503c62c44f54291334b1aa22a30fa08b - React-Fabric: 64586dc191fc1c170372a638b8e722e4f1d0a09b - React-FabricComponents: b0ebd032387468ea700574c581b139f57a7497fb - React-FabricImage: 81f0e0794caf25ad1224fa406d288fbc1986607f + React-defaultsnativemodule: 21f216e8db975897eb32b5f13247f5bbfaa97f41 + React-domnativemodule: 19270ad4b8d33312838d257f24731a0026809d49 + React-Fabric: f6dade7007533daeb785ba5925039d83f343be4b + React-FabricComponents: b0655cc3e1b5ae12a4a1119aa7d8308f0ad33520 + React-FabricImage: 9b157c4c01ac2bf433f834f0e1e5fe234113a576 React-featureflags: f2792b067a351d86fdc7bec23db3b9a2f2c8d26c - React-featureflagsnativemodule: 95a02d895475de8ace78fedd76143866838bb720 - React-graphics: cbebe910e4a15b65b0bff94a4d3ed278894d6386 - React-hermes: ec18c10f5a69d49fb9b5e17ae95494e9ea13d4d3 - React-idlecallbacksnativemodule: 0c1ae840cc5587197cd926a3cb76828ad059d116 - React-ImageManager: f2a4c01c2ccb2193e60a20c135da74c7ca4d36f2 - React-jserrorhandler: 61d205b5a7cbc57fed3371dd7eed48c97f49fc64 - React-jsi: 95f7676103137861b79b0f319467627bcfa629ee - React-jsiexecutor: 41e0fe87cda9ea3970ffb872ef10f1ff8dbd1932 - React-jsinspector: 15578208796723e5c6f39069b6e8bf36863ef6e2 - React-jsitracing: 3758cdb155ea7711f0e77952572ea62d90c69f0b - React-logger: dbca7bdfd4aa5ef69431362bde6b36d49403cb20 - React-Mapbuffer: 6efad4a606c1fae7e4a93385ee096681ef0300dc - React-microtasksnativemodule: 8732b71aa66045da4bb341ddee1bb539f71e5f38 - react-native-crash-tester: 3ffaa64141427ca362079cb53559fe9a532487ae - react-native-safe-area-context: 04803a01f39f31cc6605a5531280b477b48f8a88 - react-native-webview: 1e12de2fad74c17b4f8b1b53ebd1e3baa0148d71 + React-featureflagsnativemodule: 3a8731d8fd9f755be57e00d9fa8a7f92aa77e87d + React-graphics: 68969e4e49d73f89da7abef4116c9b5f466aa121 + React-hermes: ac0bcba26a5d288ebc99b500e1097da2d0297ddf + React-idlecallbacksnativemodule: 9a2c5b5c174c0c476f039bedc1b9497a8272133e + React-ImageManager: e906eec93a9eb6102a06576b89d48d80a4683020 + React-jserrorhandler: ac5dde01104ff444e043cad8f574ca02756e20d6 + React-jsi: 496fa2b9d63b726aeb07d0ac800064617d71211d + React-jsiexecutor: dd22ab48371b80f37a0a30d0e8915b6d0f43a893 + React-jsinspector: 4629ac376f5765e684d19064f2093e55c97fd086 + React-jsitracing: 7a1c9cd484248870cf660733cd3b8114d54c035f + React-logger: c4052eb941cca9a097ef01b59543a656dc088559 + React-Mapbuffer: 33546a3ebefbccb8770c33a1f8a5554fa96a54de + React-microtasksnativemodule: 5c3d795318c22ab8df55100e50b151384a4a60b3 + react-native-crash-tester: 48bde9d6f5256c61ef2e0c52dfc74256b26e55eb + react-native-safe-area-context: e134b241010ebe2aacdcea013565963d13826faa + react-native-webview: 2ea635bc43fd8a4b89de61133e8cc0607084e9f8 React-nativeconfig: 8efdb1ef1e9158c77098a93085438f7e7b463678 - React-NativeModulesApple: 958d4f6c5c2ace4c0f427cf7ef82e28ae6538a22 - React-perflogger: 9b4f13c0afe56bc7b4a0e93ec74b1150421ee22d - React-performancetimeline: 359db1cb889aa0282fafc5838331b0987c4915a9 + React-NativeModulesApple: cebca2e5320a3d66e123cade23bd90a167ffce5e + React-perflogger: 72e653eb3aba9122f9e57cf012d22d2486f33358 + React-performancetimeline: cd6a9374a72001165995d2ab632f672df04076dc React-RCTActionSheet: aacf2375084dea6e7c221f4a727e579f732ff342 - React-RCTAnimation: d8c82deebebe3aaf7a843affac1b57cb2dc073d4 - React-RCTAppDelegate: 6c0377d9c4058773ea7073bb34bb9ebd6ddf5a84 - React-RCTBlob: 70a58c11a6a3500d1a12f2e51ca4f6c99babcff8 - React-RCTFabric: 7eb6dd2c8fda98cb860a572e3f4e4eb60d62c89e - React-RCTImage: 5e9d655ba6a790c31e3176016f9b47fd0978fbf0 - React-RCTLinking: 2a48338252805091f7521eaf92687206401bdf2a - React-RCTNetwork: 0c1282b377257f6b1c81934f72d8a1d0c010e4c3 - React-RCTSettings: f757b679a74e5962be64ea08d7865a7debd67b40 - React-RCTText: e7d20c490b407d3b4a2daa48db4bcd8ec1032af2 - React-RCTVibration: 8228e37144ca3122a91f1de16ba8e0707159cfec + React-RCTAnimation: 395ab53fd064dff81507c15efb781c8684d9a585 + React-RCTAppDelegate: 1e5b43833e3e36e9fa34eec20be98174bc0e14a2 + React-RCTBlob: 13311e554c1a367de063c10ee7c5e6573b2dd1d6 + React-RCTFabric: bd906861a4e971e21d8df496c2d8f3ca6956f840 + React-RCTImage: 1b1f914bcc12187c49ba5d949dac38c2eb9f5cc8 + React-RCTLinking: 4ac7c42beb65e36fba0376f3498f3cd8dd0be7fa + React-RCTNetwork: 938902773add4381e84426a7aa17a2414f5f94f7 + React-RCTSettings: e848f1ba17a7a18479cf5a31d28145f567da8223 + React-RCTText: 7e98fafdde7d29e888b80f0b35544e0cb07913cf + React-RCTVibration: cd7d80affd97dc7afa62f9acd491419558b64b78 React-rendererconsistency: b4917053ecbaa91469c67a4319701c9dc0d40be6 - React-rendererdebug: 81becbc8852b38d9b1b68672aa504556481330d5 + React-rendererdebug: aa181c36dd6cf5b35511d1ed875d6638fd38f0ec React-rncore: 120d21715c9b4ba8f798bffe986cb769b988dd74 - React-RuntimeApple: 52ed0e9e84a7c2607a901149fb13599a3c057655 - React-RuntimeCore: ca6189d2e53d86db826e2673fe8af6571b8be157 + React-RuntimeApple: d033becbbd1eba6f9f6e3af6f1893030ce203edd + React-RuntimeCore: 38af280bb678e66ba000a3c3d42920b2a138eebb React-runtimeexecutor: 877596f82f5632d073e121cba2d2084b76a76899 - React-RuntimeHermes: 3b752dc5d8a1661c9d1687391d6d96acfa385549 - React-runtimescheduler: 8321bb09175ace2a4f0b3e3834637eb85bf42ebe + React-RuntimeHermes: 37aad735ff21ca6de2d8450a96de1afe9f86c385 + React-runtimescheduler: 8ec34cc885281a34696ea16c4fd86892d631f38d React-timing: 331cbf9f2668c67faddfd2e46bb7f41cbd9320b9 - React-utils: 54df9ada708578c8ad40d92895d6fed03e0e8a9e - ReactCodegen: 21a52ccddc6479448fc91903a437dd23ddc7366c - ReactCommon: bfd3600989d79bc3acbe7704161b171a1480b9fd - ReactNativeNavigation: 50c1eef68b821e7265eff3a391d27ed18fdce459 - RNCAsyncStorage: 23e56519cc41d3bade3c8d4479f7760cb1c11996 - RNGestureHandler: 950dfa674dbf481460ca389c65b9036ac4ab8ada - RNScreens: 606ab1cf68162f7ba0d049a31f2a84089a6fffb4 + React-utils: ed818f19ab445000d6b5c4efa9d462449326cc9f + ReactCodegen: f853a20cc9125c5521c8766b4b49375fec20648b + ReactCommon: 300d8d9c5cb1a6cd79a67cf5d8f91e4d477195f9 + ReactNativeNavigation: 445f86273eb245d15b14023ee4ef9d6e4f891ad6 + RNCAsyncStorage: b44e8a4e798c3e1f56bffccd0f591f674fb9198f + RNGestureHandler: cb711d56ee3b03a5adea1d38324d4459ab55653f + RNScreens: f75b26fd4777848c216e27b0a09e1bf9c9f4760a SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 Yoga: feb4910aba9742cfedc059e2b2902e22ffe9954a diff --git a/packages/core/DatadogSDKReactNative.podspec b/packages/core/DatadogSDKReactNative.podspec index c0d235304..a2f41ddd1 100644 --- a/packages/core/DatadogSDKReactNative.podspec +++ b/packages/core/DatadogSDKReactNative.podspec @@ -19,14 +19,14 @@ Pod::Spec.new do |s| s.dependency "React-Core" # /!\ Remember to keep the versions in sync with DatadogSDKReactNativeSessionReplay.podspec - s.dependency 'DatadogCore', '3.1.0' - s.dependency 'DatadogLogs', '3.1.0' - s.dependency 'DatadogTrace', '3.1.0' - s.dependency 'DatadogRUM', '3.1.0' - s.dependency 'DatadogCrashReporting', '3.1.0' + s.dependency 'DatadogCore', '3.0.0' + s.dependency 'DatadogLogs', '3.0.0' + s.dependency 'DatadogTrace', '3.0.0' + s.dependency 'DatadogRUM', '3.0.0' + s.dependency 'DatadogCrashReporting', '3.0.0' # DatadogWebViewTracking is not available for tvOS - s.ios.dependency 'DatadogWebViewTracking', '3.1.0' + s.ios.dependency 'DatadogWebViewTracking', '3.0.0' s.test_spec 'Tests' do |test_spec| test_spec.source_files = 'ios/Tests/**/*.{swift,json}' diff --git a/packages/core/android/build.gradle b/packages/core/android/build.gradle index 080185d41..a8d09d4d1 100644 --- a/packages/core/android/build.gradle +++ b/packages/core/android/build.gradle @@ -201,16 +201,16 @@ dependencies { // This breaks builds if the React Native target is below 0.76.0. as it relies on Gradle 8.5.0. // To avoid this, we enforce 1.0.0-beta01 on RN < 0.76.0 if (reactNativeMinorVersion < 76) { - implementation("com.datadoghq:dd-sdk-android-rum:3.2.0") { + implementation("com.datadoghq:dd-sdk-android-rum:3.0.0") { exclude group: "androidx.metrics", module: "metrics-performance" } implementation "androidx.metrics:metrics-performance:1.0.0-beta01" } else { - implementation "com.datadoghq:dd-sdk-android-rum:3.2.0" + implementation "com.datadoghq:dd-sdk-android-rum:3.0.0" } - implementation "com.datadoghq:dd-sdk-android-logs:3.2.0" - implementation "com.datadoghq:dd-sdk-android-trace:3.2.0" - implementation "com.datadoghq:dd-sdk-android-webview:3.2.0" + implementation "com.datadoghq:dd-sdk-android-logs:3.0.0" + implementation "com.datadoghq:dd-sdk-android-trace:3.0.0" + implementation "com.datadoghq:dd-sdk-android-webview:3.0.0" implementation "com.google.code.gson:gson:2.10.0" testImplementation "org.junit.platform:junit-platform-launcher:1.6.2" testImplementation "org.junit.jupiter:junit-jupiter-api:5.6.2" diff --git a/packages/react-native-session-replay/DatadogSDKReactNativeSessionReplay.podspec b/packages/react-native-session-replay/DatadogSDKReactNativeSessionReplay.podspec index 6a5d0b78f..3a2d5ff48 100644 --- a/packages/react-native-session-replay/DatadogSDKReactNativeSessionReplay.podspec +++ b/packages/react-native-session-replay/DatadogSDKReactNativeSessionReplay.podspec @@ -23,7 +23,7 @@ Pod::Spec.new do |s| s.dependency "React-Core" # /!\ Remember to keep the version in sync with DatadogSDKReactNative.podspec - s.dependency 'DatadogSessionReplay', '3.1.0' + s.dependency 'DatadogSessionReplay', '3.0.0' s.dependency 'DatadogSDKReactNative' s.test_spec 'Tests' do |test_spec| diff --git a/packages/react-native-session-replay/android/build.gradle b/packages/react-native-session-replay/android/build.gradle index bea305684..6d6fe20b1 100644 --- a/packages/react-native-session-replay/android/build.gradle +++ b/packages/react-native-session-replay/android/build.gradle @@ -216,8 +216,8 @@ dependencies { api "com.facebook.react:react-android:$reactNativeVersion" } implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" - implementation "com.datadoghq:dd-sdk-android-session-replay:3.2.0" - implementation "com.datadoghq:dd-sdk-android-internal:3.2.0" + implementation "com.datadoghq:dd-sdk-android-session-replay:3.0.0" + implementation "com.datadoghq:dd-sdk-android-internal:3.0.0" implementation project(path: ':datadog_mobile-react-native') testImplementation "org.junit.platform:junit-platform-launcher:1.6.2" diff --git a/packages/react-native-webview/DatadogSDKReactNativeWebView.podspec b/packages/react-native-webview/DatadogSDKReactNativeWebView.podspec index 080a853d8..26e160bbc 100644 --- a/packages/react-native-webview/DatadogSDKReactNativeWebView.podspec +++ b/packages/react-native-webview/DatadogSDKReactNativeWebView.podspec @@ -23,8 +23,8 @@ Pod::Spec.new do |s| end # /!\ Remember to keep the version in sync with DatadogSDKReactNative.podspec - s.dependency 'DatadogWebViewTracking', '3.1.0' - s.dependency 'DatadogInternal', '3.1.0' + s.dependency 'DatadogWebViewTracking', '3.0.0' + s.dependency 'DatadogInternal', '3.0.0' s.dependency 'DatadogSDKReactNative' s.test_spec 'Tests' do |test_spec| diff --git a/packages/react-native-webview/android/build.gradle b/packages/react-native-webview/android/build.gradle index dbb8d0593..098eae019 100644 --- a/packages/react-native-webview/android/build.gradle +++ b/packages/react-native-webview/android/build.gradle @@ -190,7 +190,7 @@ dependencies { implementation "com.facebook.react:react-android:$reactNativeVersion" } - implementation "com.datadoghq:dd-sdk-android-webview:3.2.0" + implementation "com.datadoghq:dd-sdk-android-webview:3.0.0" implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation project(path: ':datadog_mobile-react-native') From 50271fb4c11d3bd084d3dab0df8eedec70bc5a8b Mon Sep 17 00:00:00 2001 From: Sergio Barrio Date: Fri, 12 Sep 2025 14:43:25 +0200 Subject: [PATCH 081/410] Remove setUser --- packages/core/ios/Tests/MockRUMMonitor.swift | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/packages/core/ios/Tests/MockRUMMonitor.swift b/packages/core/ios/Tests/MockRUMMonitor.swift index 4a1d0cd92..c692f6e7c 100644 --- a/packages/core/ios/Tests/MockRUMMonitor.swift +++ b/packages/core/ios/Tests/MockRUMMonitor.swift @@ -10,6 +10,22 @@ @testable import DatadogSDKReactNative internal class MockRUMMonitor: RUMMonitorProtocol { + func addViewAttribute(forKey key: DatadogInternal.AttributeKey, value: any DatadogInternal.AttributeValue) { + // not implemented + } + + func addViewAttributes(_ attributes: [DatadogInternal.AttributeKey : any DatadogInternal.AttributeValue]) { + // not implemented + } + + func removeViewAttribute(forKey key: DatadogInternal.AttributeKey) { + // not implemented + } + + func removeViewAttributes(forKeys keys: [DatadogInternal.AttributeKey]) { + // not implemented + } + func currentSessionID(completion: @escaping (String?) -> Void) { // not implemented } From 6a7b4267c1cc62507d38e6506b65c6dd77332413 Mon Sep 17 00:00:00 2001 From: Sergio Barrio Date: Fri, 19 Sep 2025 11:45:00 +0200 Subject: [PATCH 082/410] Bump Native SDKs to 3.1.0 --- benchmarks/android/app/build.gradle | 2 +- benchmarks/ios/Podfile.lock | 70 +++++++-------- example-new-architecture/ios/Podfile.lock | 66 +++++++------- example/ios/Podfile.lock | 88 +++++++++---------- packages/core/DatadogSDKReactNative.podspec | 12 +-- packages/core/android/build.gradle | 10 +-- ...DatadogSDKReactNativeSessionReplay.podspec | 2 +- .../android/build.gradle | 4 +- .../DatadogSDKReactNativeWebView.podspec | 4 +- .../react-native-webview/android/build.gradle | 2 +- 10 files changed, 130 insertions(+), 130 deletions(-) diff --git a/benchmarks/android/app/build.gradle b/benchmarks/android/app/build.gradle index 7dc4b3707..04c240bd0 100644 --- a/benchmarks/android/app/build.gradle +++ b/benchmarks/android/app/build.gradle @@ -129,5 +129,5 @@ dependencies { // Benchmark tools from dd-sdk-android are used for vitals recording // Remember to bump thid alongside the main dd-sdk-android dependencies - implementation("com.datadoghq:dd-sdk-android-benchmark-internal:3.0.0") + implementation("com.datadoghq:dd-sdk-android-benchmark-internal:3.1.0") } diff --git a/benchmarks/ios/Podfile.lock b/benchmarks/ios/Podfile.lock index 16194127d..5c91024d5 100644 --- a/benchmarks/ios/Podfile.lock +++ b/benchmarks/ios/Podfile.lock @@ -1,22 +1,22 @@ PODS: - boost (1.84.0) - - DatadogCore (3.0.0): - - DatadogInternal (= 3.0.0) - - DatadogCrashReporting (3.0.0): - - DatadogInternal (= 3.0.0) + - DatadogCore (3.1.0): + - DatadogInternal (= 3.1.0) + - DatadogCrashReporting (3.1.0): + - DatadogInternal (= 3.1.0) - PLCrashReporter (~> 1.12.0) - - DatadogInternal (3.0.0) - - DatadogLogs (3.0.0): - - DatadogInternal (= 3.0.0) - - DatadogRUM (3.0.0): - - DatadogInternal (= 3.0.0) + - DatadogInternal (3.1.0) + - DatadogLogs (3.1.0): + - DatadogInternal (= 3.1.0) + - DatadogRUM (3.1.0): + - DatadogInternal (= 3.1.0) - DatadogSDKReactNative (2.12.1): - - DatadogCore (= 3.0.0) - - DatadogCrashReporting (= 3.0.0) - - DatadogLogs (= 3.0.0) - - DatadogRUM (= 3.0.0) - - DatadogTrace (= 3.0.0) - - DatadogWebViewTracking (= 3.0.0) + - DatadogCore (= 3.1.0) + - DatadogCrashReporting (= 3.1.0) + - DatadogLogs (= 3.1.0) + - DatadogRUM (= 3.1.0) + - DatadogTrace (= 3.1.0) + - DatadogWebViewTracking (= 3.1.0) - DoubleConversion - glog - hermes-engine @@ -39,7 +39,7 @@ PODS: - Yoga - DatadogSDKReactNativeSessionReplay (2.13.2): - DatadogSDKReactNative - - DatadogSessionReplay (= 3.0.0) + - DatadogSessionReplay (= 3.1.0) - DoubleConversion - glog - hermes-engine @@ -61,9 +61,9 @@ PODS: - ReactCommon/turbomodule/core - Yoga - DatadogSDKReactNativeWebView (2.12.1): - - DatadogInternal (= 3.0.0) + - DatadogInternal (= 3.1.0) - DatadogSDKReactNative - - DatadogWebViewTracking (= 3.0.0) + - DatadogWebViewTracking (= 3.1.0) - DoubleConversion - glog - hermes-engine @@ -84,13 +84,13 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - DatadogSessionReplay (3.0.0): - - DatadogInternal (= 3.0.0) - - DatadogTrace (3.0.0): - - DatadogInternal (= 3.0.0) + - DatadogSessionReplay (3.1.0): + - DatadogInternal (= 3.1.0) + - DatadogTrace (3.1.0): + - DatadogInternal (= 3.1.0) - OpenTelemetrySwiftApi (= 1.13.1) - - DatadogWebViewTracking (3.0.0): - - DatadogInternal (= 3.0.0) + - DatadogWebViewTracking (3.1.0): + - DatadogInternal (= 3.1.0) - DoubleConversion (1.1.6) - fast_float (6.1.4) - FBLazyVector (0.78.2) @@ -2070,17 +2070,17 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: boost: 7e761d76ca2ce687f7cc98e698152abd03a18f90 - DatadogCore: 0c39ba4ef3dee02071f235f2fb56af7e29f4ceb0 - DatadogCrashReporting: 604e65e524cb2fc22cda39fd9c9ea5fd3bddf24e - DatadogInternal: 442673a7fb5329299b489b91ed705aa2085380f7 - DatadogLogs: 0146a0e3140ba62fd42729593c0910d692aea5fd - DatadogRUM: f97fbd0290ecec5bc952fb03a6a1ae068649243f - DatadogSDKReactNative: 241bf982c16ceff03d94a58e6d005e55c47b99c6 - DatadogSDKReactNativeSessionReplay: bba36092686e3183e97c1a0c7f4ca8142582ae28 - DatadogSDKReactNativeWebView: 6da060df20e235abac533e582d9fc2b3a5070840 - DatadogSessionReplay: 0357b911c6ab42c77c93b29761ecc20d9282e971 - DatadogTrace: d714e7c456e612b7f171483d08086a59aa1c4213 - DatadogWebViewTracking: fb9591c09fca82b143f3843a7164a7e782175c53 + DatadogCore: d2f51c7fb4308cf3c25e55e2e7242e5d558ee71d + DatadogCrashReporting: f636f1d1c534572c0b0abcdc59df244c884d825d + DatadogInternal: 7837b2ce3d525d429682532eeda697b181299fdc + DatadogLogs: 250894b5a99da5b924a019049c0d0326823cdbd6 + DatadogRUM: 0d2a60e1abb8aacfb8827ef84f6d5deb4d5026c8 + DatadogSDKReactNative: 8e0f39de38621d4d7ed961a74d8a216fd3a38321 + DatadogSDKReactNativeSessionReplay: f9288c8e981dcc65d1f727b01421ee9a7601e75f + DatadogSDKReactNativeWebView: 993527f6c5d38e0fcc4804a6a60c334dd199dc5b + DatadogSessionReplay: 6bc71888e2b41dd0de3325f06f0c0b3cee0e6df4 + DatadogTrace: f59e933074cd285ad7e9f5af991f8fe04b095991 + DatadogWebViewTracking: 9bc92b4147aeed47eb1911451f651094aa6dd6c1 DoubleConversion: cb417026b2400c8f53ae97020b2be961b59470cb fast_float: 06eeec4fe712a76acc9376682e4808b05ce978b6 FBLazyVector: e32d34492c519a2194ec9d7f5e7a79d11b73f91c diff --git a/example-new-architecture/ios/Podfile.lock b/example-new-architecture/ios/Podfile.lock index 281513566..c0cd90bf6 100644 --- a/example-new-architecture/ios/Podfile.lock +++ b/example-new-architecture/ios/Podfile.lock @@ -1,22 +1,22 @@ PODS: - boost (1.84.0) - - DatadogCore (3.0.0): - - DatadogInternal (= 3.0.0) - - DatadogCrashReporting (3.0.0): - - DatadogInternal (= 3.0.0) + - DatadogCore (3.1.0): + - DatadogInternal (= 3.1.0) + - DatadogCrashReporting (3.1.0): + - DatadogInternal (= 3.1.0) - PLCrashReporter (~> 1.12.0) - - DatadogInternal (3.0.0) - - DatadogLogs (3.0.0): - - DatadogInternal (= 3.0.0) - - DatadogRUM (3.0.0): - - DatadogInternal (= 3.0.0) + - DatadogInternal (3.1.0) + - DatadogLogs (3.1.0): + - DatadogInternal (= 3.1.0) + - DatadogRUM (3.1.0): + - DatadogInternal (= 3.1.0) - DatadogSDKReactNative (2.12.1): - - DatadogCore (= 3.0.0) - - DatadogCrashReporting (= 3.0.0) - - DatadogLogs (= 3.0.0) - - DatadogRUM (= 3.0.0) - - DatadogTrace (= 3.0.0) - - DatadogWebViewTracking (= 3.0.0) + - DatadogCore (= 3.1.0) + - DatadogCrashReporting (= 3.1.0) + - DatadogLogs (= 3.1.0) + - DatadogRUM (= 3.1.0) + - DatadogTrace (= 3.1.0) + - DatadogWebViewTracking (= 3.1.0) - DoubleConversion - glog - hermes-engine @@ -38,12 +38,12 @@ PODS: - ReactCommon/turbomodule/core - Yoga - DatadogSDKReactNative/Tests (2.12.1): - - DatadogCore (= 3.0.0) - - DatadogCrashReporting (= 3.0.0) - - DatadogLogs (= 3.0.0) - - DatadogRUM (= 3.0.0) - - DatadogTrace (= 3.0.0) - - DatadogWebViewTracking (= 3.0.0) + - DatadogCore (= 3.1.0) + - DatadogCrashReporting (= 3.1.0) + - DatadogLogs (= 3.1.0) + - DatadogRUM (= 3.1.0) + - DatadogTrace (= 3.1.0) + - DatadogWebViewTracking (= 3.1.0) - DoubleConversion - glog - hermes-engine @@ -64,11 +64,11 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - DatadogTrace (3.0.0): - - DatadogInternal (= 3.0.0) + - DatadogTrace (3.1.0): + - DatadogInternal (= 3.1.0) - OpenTelemetrySwiftApi (= 1.13.1) - - DatadogWebViewTracking (3.0.0): - - DatadogInternal (= 3.0.0) + - DatadogWebViewTracking (3.1.0): + - DatadogInternal (= 3.1.0) - DoubleConversion (1.1.6) - fast_float (6.1.4) - FBLazyVector (0.76.9) @@ -1850,14 +1850,14 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: boost: 1dca942403ed9342f98334bf4c3621f011aa7946 - DatadogCore: 0c39ba4ef3dee02071f235f2fb56af7e29f4ceb0 - DatadogCrashReporting: 604e65e524cb2fc22cda39fd9c9ea5fd3bddf24e - DatadogInternal: 442673a7fb5329299b489b91ed705aa2085380f7 - DatadogLogs: 0146a0e3140ba62fd42729593c0910d692aea5fd - DatadogRUM: f97fbd0290ecec5bc952fb03a6a1ae068649243f - DatadogSDKReactNative: 0a80aa75958d595a99be54d2838db53eda7a08af - DatadogTrace: d714e7c456e612b7f171483d08086a59aa1c4213 - DatadogWebViewTracking: fb9591c09fca82b143f3843a7164a7e782175c53 + DatadogCore: d2f51c7fb4308cf3c25e55e2e7242e5d558ee71d + DatadogCrashReporting: f636f1d1c534572c0b0abcdc59df244c884d825d + DatadogInternal: 7837b2ce3d525d429682532eeda697b181299fdc + DatadogLogs: 250894b5a99da5b924a019049c0d0326823cdbd6 + DatadogRUM: 0d2a60e1abb8aacfb8827ef84f6d5deb4d5026c8 + DatadogSDKReactNative: 069ea9876220b2d09b0f4b180ce571b1b6ecbb35 + DatadogTrace: f59e933074cd285ad7e9f5af991f8fe04b095991 + DatadogWebViewTracking: 9bc92b4147aeed47eb1911451f651094aa6dd6c1 DoubleConversion: f16ae600a246532c4020132d54af21d0ddb2a385 fast_float: 06eeec4fe712a76acc9376682e4808b05ce978b6 FBLazyVector: 7605ea4810e0e10ae4815292433c09bf4324ba45 diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index fac339d80..8838c9be5 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -1,34 +1,34 @@ PODS: - boost (1.84.0) - - DatadogCore (3.0.0): - - DatadogInternal (= 3.0.0) - - DatadogCrashReporting (3.0.0): - - DatadogInternal (= 3.0.0) + - DatadogCore (3.1.0): + - DatadogInternal (= 3.1.0) + - DatadogCrashReporting (3.1.0): + - DatadogInternal (= 3.1.0) - PLCrashReporter (~> 1.12.0) - - DatadogInternal (3.0.0) - - DatadogLogs (3.0.0): - - DatadogInternal (= 3.0.0) - - DatadogRUM (3.0.0): - - DatadogInternal (= 3.0.0) + - DatadogInternal (3.1.0) + - DatadogLogs (3.1.0): + - DatadogInternal (= 3.1.0) + - DatadogRUM (3.1.0): + - DatadogInternal (= 3.1.0) - DatadogSDKReactNative (2.12.1): - - DatadogCore (= 3.0.0) - - DatadogCrashReporting (= 3.0.0) - - DatadogLogs (= 3.0.0) - - DatadogRUM (= 3.0.0) - - DatadogTrace (= 3.0.0) - - DatadogWebViewTracking (= 3.0.0) + - DatadogCore (= 3.1.0) + - DatadogCrashReporting (= 3.1.0) + - DatadogLogs (= 3.1.0) + - DatadogRUM (= 3.1.0) + - DatadogTrace (= 3.1.0) + - DatadogWebViewTracking (= 3.1.0) - React-Core - DatadogSDKReactNative/Tests (2.12.1): - - DatadogCore (= 3.0.0) - - DatadogCrashReporting (= 3.0.0) - - DatadogLogs (= 3.0.0) - - DatadogRUM (= 3.0.0) - - DatadogTrace (= 3.0.0) - - DatadogWebViewTracking (= 3.0.0) + - DatadogCore (= 3.1.0) + - DatadogCrashReporting (= 3.1.0) + - DatadogLogs (= 3.1.0) + - DatadogRUM (= 3.1.0) + - DatadogTrace (= 3.1.0) + - DatadogWebViewTracking (= 3.1.0) - React-Core - DatadogSDKReactNativeSessionReplay (2.13.2): - DatadogSDKReactNative - - DatadogSessionReplay (= 3.0.0) + - DatadogSessionReplay (= 3.1.0) - DoubleConversion - glog - hermes-engine @@ -51,7 +51,7 @@ PODS: - Yoga - DatadogSDKReactNativeSessionReplay/Tests (2.13.2): - DatadogSDKReactNative - - DatadogSessionReplay (= 3.0.0) + - DatadogSessionReplay (= 3.1.0) - DoubleConversion - glog - hermes-engine @@ -74,24 +74,24 @@ PODS: - ReactCommon/turbomodule/core - Yoga - DatadogSDKReactNativeWebView (2.12.1): - - DatadogInternal (= 3.0.0) + - DatadogInternal (= 3.1.0) - DatadogSDKReactNative - - DatadogWebViewTracking (= 3.0.0) + - DatadogWebViewTracking (= 3.1.0) - React-Core - DatadogSDKReactNativeWebView/Tests (2.12.1): - - DatadogInternal (= 3.0.0) + - DatadogInternal (= 3.1.0) - DatadogSDKReactNative - - DatadogWebViewTracking (= 3.0.0) + - DatadogWebViewTracking (= 3.1.0) - React-Core - react-native-webview - React-RCTText - - DatadogSessionReplay (3.0.0): - - DatadogInternal (= 3.0.0) - - DatadogTrace (3.0.0): - - DatadogInternal (= 3.0.0) + - DatadogSessionReplay (3.1.0): + - DatadogInternal (= 3.1.0) + - DatadogTrace (3.1.0): + - DatadogInternal (= 3.1.0) - OpenTelemetrySwiftApi (= 1.13.1) - - DatadogWebViewTracking (3.0.0): - - DatadogInternal (= 3.0.0) + - DatadogWebViewTracking (3.1.0): + - DatadogInternal (= 3.1.0) - DoubleConversion (1.1.6) - fast_float (6.1.4) - FBLazyVector (0.76.9) @@ -1988,17 +1988,17 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: boost: 1dca942403ed9342f98334bf4c3621f011aa7946 - DatadogCore: 0c39ba4ef3dee02071f235f2fb56af7e29f4ceb0 - DatadogCrashReporting: 604e65e524cb2fc22cda39fd9c9ea5fd3bddf24e - DatadogInternal: 442673a7fb5329299b489b91ed705aa2085380f7 - DatadogLogs: 0146a0e3140ba62fd42729593c0910d692aea5fd - DatadogRUM: f97fbd0290ecec5bc952fb03a6a1ae068649243f - DatadogSDKReactNative: e430b3f4d7adb0fac07b61e2fb65ceacdaf82b3e - DatadogSDKReactNativeSessionReplay: 4b2a3d166a79581f18522795b40141c34cf3685d - DatadogSDKReactNativeWebView: 35dc2b9736e1aaa82b366bf6b8a8a959a9b088c5 - DatadogSessionReplay: 0357b911c6ab42c77c93b29761ecc20d9282e971 - DatadogTrace: d714e7c456e612b7f171483d08086a59aa1c4213 - DatadogWebViewTracking: fb9591c09fca82b143f3843a7164a7e782175c53 + DatadogCore: d2f51c7fb4308cf3c25e55e2e7242e5d558ee71d + DatadogCrashReporting: f636f1d1c534572c0b0abcdc59df244c884d825d + DatadogInternal: 7837b2ce3d525d429682532eeda697b181299fdc + DatadogLogs: 250894b5a99da5b924a019049c0d0326823cdbd6 + DatadogRUM: 0d2a60e1abb8aacfb8827ef84f6d5deb4d5026c8 + DatadogSDKReactNative: af351a4e1ce08124c290c52de94b0062a166cc67 + DatadogSDKReactNativeSessionReplay: dcbd55d9d0f2b86026996a8b7ec9654922d5dfe1 + DatadogSDKReactNativeWebView: 096ac87eb753b6a217b93441983264b9837c3b7e + DatadogSessionReplay: 6bc71888e2b41dd0de3325f06f0c0b3cee0e6df4 + DatadogTrace: f59e933074cd285ad7e9f5af991f8fe04b095991 + DatadogWebViewTracking: 9bc92b4147aeed47eb1911451f651094aa6dd6c1 DoubleConversion: f16ae600a246532c4020132d54af21d0ddb2a385 fast_float: 06eeec4fe712a76acc9376682e4808b05ce978b6 FBLazyVector: 7605ea4810e0e10ae4815292433c09bf4324ba45 diff --git a/packages/core/DatadogSDKReactNative.podspec b/packages/core/DatadogSDKReactNative.podspec index a2f41ddd1..c0d235304 100644 --- a/packages/core/DatadogSDKReactNative.podspec +++ b/packages/core/DatadogSDKReactNative.podspec @@ -19,14 +19,14 @@ Pod::Spec.new do |s| s.dependency "React-Core" # /!\ Remember to keep the versions in sync with DatadogSDKReactNativeSessionReplay.podspec - s.dependency 'DatadogCore', '3.0.0' - s.dependency 'DatadogLogs', '3.0.0' - s.dependency 'DatadogTrace', '3.0.0' - s.dependency 'DatadogRUM', '3.0.0' - s.dependency 'DatadogCrashReporting', '3.0.0' + s.dependency 'DatadogCore', '3.1.0' + s.dependency 'DatadogLogs', '3.1.0' + s.dependency 'DatadogTrace', '3.1.0' + s.dependency 'DatadogRUM', '3.1.0' + s.dependency 'DatadogCrashReporting', '3.1.0' # DatadogWebViewTracking is not available for tvOS - s.ios.dependency 'DatadogWebViewTracking', '3.0.0' + s.ios.dependency 'DatadogWebViewTracking', '3.1.0' s.test_spec 'Tests' do |test_spec| test_spec.source_files = 'ios/Tests/**/*.{swift,json}' diff --git a/packages/core/android/build.gradle b/packages/core/android/build.gradle index a8d09d4d1..59bb75cc3 100644 --- a/packages/core/android/build.gradle +++ b/packages/core/android/build.gradle @@ -201,16 +201,16 @@ dependencies { // This breaks builds if the React Native target is below 0.76.0. as it relies on Gradle 8.5.0. // To avoid this, we enforce 1.0.0-beta01 on RN < 0.76.0 if (reactNativeMinorVersion < 76) { - implementation("com.datadoghq:dd-sdk-android-rum:3.0.0") { + implementation("com.datadoghq:dd-sdk-android-rum:3.1.0") { exclude group: "androidx.metrics", module: "metrics-performance" } implementation "androidx.metrics:metrics-performance:1.0.0-beta01" } else { - implementation "com.datadoghq:dd-sdk-android-rum:3.0.0" + implementation "com.datadoghq:dd-sdk-android-rum:3.1.0" } - implementation "com.datadoghq:dd-sdk-android-logs:3.0.0" - implementation "com.datadoghq:dd-sdk-android-trace:3.0.0" - implementation "com.datadoghq:dd-sdk-android-webview:3.0.0" + implementation "com.datadoghq:dd-sdk-android-logs:3.1.0" + implementation "com.datadoghq:dd-sdk-android-trace:3.1.0" + implementation "com.datadoghq:dd-sdk-android-webview:3.1.0" implementation "com.google.code.gson:gson:2.10.0" testImplementation "org.junit.platform:junit-platform-launcher:1.6.2" testImplementation "org.junit.jupiter:junit-jupiter-api:5.6.2" diff --git a/packages/react-native-session-replay/DatadogSDKReactNativeSessionReplay.podspec b/packages/react-native-session-replay/DatadogSDKReactNativeSessionReplay.podspec index 3a2d5ff48..6a5d0b78f 100644 --- a/packages/react-native-session-replay/DatadogSDKReactNativeSessionReplay.podspec +++ b/packages/react-native-session-replay/DatadogSDKReactNativeSessionReplay.podspec @@ -23,7 +23,7 @@ Pod::Spec.new do |s| s.dependency "React-Core" # /!\ Remember to keep the version in sync with DatadogSDKReactNative.podspec - s.dependency 'DatadogSessionReplay', '3.0.0' + s.dependency 'DatadogSessionReplay', '3.1.0' s.dependency 'DatadogSDKReactNative' s.test_spec 'Tests' do |test_spec| diff --git a/packages/react-native-session-replay/android/build.gradle b/packages/react-native-session-replay/android/build.gradle index 6d6fe20b1..a0d77f2ff 100644 --- a/packages/react-native-session-replay/android/build.gradle +++ b/packages/react-native-session-replay/android/build.gradle @@ -216,8 +216,8 @@ dependencies { api "com.facebook.react:react-android:$reactNativeVersion" } implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" - implementation "com.datadoghq:dd-sdk-android-session-replay:3.0.0" - implementation "com.datadoghq:dd-sdk-android-internal:3.0.0" + implementation "com.datadoghq:dd-sdk-android-session-replay:3.1.0" + implementation "com.datadoghq:dd-sdk-android-internal:3.1.0" implementation project(path: ':datadog_mobile-react-native') testImplementation "org.junit.platform:junit-platform-launcher:1.6.2" diff --git a/packages/react-native-webview/DatadogSDKReactNativeWebView.podspec b/packages/react-native-webview/DatadogSDKReactNativeWebView.podspec index 26e160bbc..080a853d8 100644 --- a/packages/react-native-webview/DatadogSDKReactNativeWebView.podspec +++ b/packages/react-native-webview/DatadogSDKReactNativeWebView.podspec @@ -23,8 +23,8 @@ Pod::Spec.new do |s| end # /!\ Remember to keep the version in sync with DatadogSDKReactNative.podspec - s.dependency 'DatadogWebViewTracking', '3.0.0' - s.dependency 'DatadogInternal', '3.0.0' + s.dependency 'DatadogWebViewTracking', '3.1.0' + s.dependency 'DatadogInternal', '3.1.0' s.dependency 'DatadogSDKReactNative' s.test_spec 'Tests' do |test_spec| diff --git a/packages/react-native-webview/android/build.gradle b/packages/react-native-webview/android/build.gradle index 098eae019..87ca1b7e7 100644 --- a/packages/react-native-webview/android/build.gradle +++ b/packages/react-native-webview/android/build.gradle @@ -190,7 +190,7 @@ dependencies { implementation "com.facebook.react:react-android:$reactNativeVersion" } - implementation "com.datadoghq:dd-sdk-android-webview:3.0.0" + implementation "com.datadoghq:dd-sdk-android-webview:3.1.0" implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation project(path: ':datadog_mobile-react-native') From 2a4f7727c8c744c0ae877fbc749e7f36a0e400f6 Mon Sep 17 00:00:00 2001 From: Sergio Barrio Date: Fri, 5 Sep 2025 17:47:29 +0200 Subject: [PATCH 083/410] Use native sdk's core instance instead of the one inside RN SDK wrapper --- .../com/datadog/reactnative/DdSdkNativeInitialization.kt | 2 ++ .../src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt | 6 +----- .../src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt | 7 +------ 3 files changed, 4 insertions(+), 11 deletions(-) diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkNativeInitialization.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkNativeInitialization.kt index 4388ad5f6..ee55d08fe 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkNativeInitialization.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkNativeInitialization.kt @@ -65,7 +65,9 @@ class DdSdkNativeInitialization internal constructor( datadog.initialize(appContext, sdkConfiguration, trackingConsent) Rum.enable(rumConfiguration, Datadog.getInstance()) + Logs.enable(logsConfiguration, Datadog.getInstance()) + Trace.enable(traceConfiguration, Datadog.getInstance()) } diff --git a/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt b/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt index 958ba521b..df44a6b5f 100644 --- a/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt +++ b/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt @@ -22,11 +22,7 @@ class DdSdk( ddTelemetry: DdTelemetry = DdTelemetry() ) : ReactContextBaseJavaModule(reactContext) { - private val implementation = DdSdkImplementation( - reactContext, - datadog = datadogWrapper, - ddTelemetry - ) + private val implementation = DdSdkImplementation(reactContext, datadog = datadogWrapper, ddTelemetry) private var lifecycleEventListener: LifecycleEventListener? = null override fun getName(): String = DdSdkImplementation.NAME diff --git a/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt b/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt index 327d8ffc0..f034b292c 100644 --- a/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt +++ b/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt @@ -164,12 +164,7 @@ internal class DdSdkTest { answer.getArgument(0).run() true } - testedBridgeSdk = DdSdkImplementation( - mockReactContext, - mockDatadog, - mockDdTelemetry, - TestUiThreadExecutor() - ) + testedBridgeSdk = DdSdkImplementation(mockReactContext, mockDatadog, mockDdTelemetry, TestUiThreadExecutor()) DatadogSDKWrapperStorage.onInitializedListeners.clear() } From 0fd48f4a916555c82565b25b659c16f2b9c3c837 Mon Sep 17 00:00:00 2001 From: Marco Saia Date: Mon, 22 Sep 2025 16:20:27 +0200 Subject: [PATCH 084/410] Fixed internal testing tools and unit tests --- .../com/datadog/reactnative/DdSdkNativeInitialization.kt | 2 -- .../src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt | 6 +++++- .../src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt | 7 ++++++- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkNativeInitialization.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkNativeInitialization.kt index ee55d08fe..4388ad5f6 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkNativeInitialization.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkNativeInitialization.kt @@ -65,9 +65,7 @@ class DdSdkNativeInitialization internal constructor( datadog.initialize(appContext, sdkConfiguration, trackingConsent) Rum.enable(rumConfiguration, Datadog.getInstance()) - Logs.enable(logsConfiguration, Datadog.getInstance()) - Trace.enable(traceConfiguration, Datadog.getInstance()) } diff --git a/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt b/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt index df44a6b5f..958ba521b 100644 --- a/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt +++ b/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt @@ -22,7 +22,11 @@ class DdSdk( ddTelemetry: DdTelemetry = DdTelemetry() ) : ReactContextBaseJavaModule(reactContext) { - private val implementation = DdSdkImplementation(reactContext, datadog = datadogWrapper, ddTelemetry) + private val implementation = DdSdkImplementation( + reactContext, + datadog = datadogWrapper, + ddTelemetry + ) private var lifecycleEventListener: LifecycleEventListener? = null override fun getName(): String = DdSdkImplementation.NAME diff --git a/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt b/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt index f034b292c..327d8ffc0 100644 --- a/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt +++ b/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt @@ -164,7 +164,12 @@ internal class DdSdkTest { answer.getArgument(0).run() true } - testedBridgeSdk = DdSdkImplementation(mockReactContext, mockDatadog, mockDdTelemetry, TestUiThreadExecutor()) + testedBridgeSdk = DdSdkImplementation( + mockReactContext, + mockDatadog, + mockDdTelemetry, + TestUiThreadExecutor() + ) DatadogSDKWrapperStorage.onInitializedListeners.clear() } From b4022578509e35294271cbb02731829810d70028 Mon Sep 17 00:00:00 2001 From: Sergio Barrio Date: Fri, 19 Sep 2025 17:37:31 +0200 Subject: [PATCH 085/410] Expose clearUserInfo API --- .../ios/Sources/DdSdkImplementation.swift | 8 +-- packages/core/ios/Tests/DdSdkTests.swift | 67 +++++++++++++++++++ 2 files changed, 70 insertions(+), 5 deletions(-) diff --git a/packages/core/ios/Sources/DdSdkImplementation.swift b/packages/core/ios/Sources/DdSdkImplementation.swift index 997068ddf..2c5d4cdf3 100644 --- a/packages/core/ios/Sources/DdSdkImplementation.swift +++ b/packages/core/ios/Sources/DdSdkImplementation.swift @@ -143,7 +143,7 @@ public class DdSdkImplementation: NSObject { resolve(nil) } - + @objc public func addUserExtraInfo( extraInfo: NSDictionary, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock @@ -155,15 +155,13 @@ public class DdSdkImplementation: NSObject { } @objc - public func clearUserInfo(resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) { + public func clearUserInfo(resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { Datadog.clearUserInfo() resolve(nil) } @objc - public func setTrackingConsent( - trackingConsent: NSString, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock - ) { + public func setTrackingConsent(trackingConsent: NSString, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { Datadog.set(trackingConsent: (trackingConsent as NSString?).asTrackingConsent()) resolve(nil) } diff --git a/packages/core/ios/Tests/DdSdkTests.swift b/packages/core/ios/Tests/DdSdkTests.swift index 4a5d13f2e..5d0237530 100644 --- a/packages/core/ios/Tests/DdSdkTests.swift +++ b/packages/core/ios/Tests/DdSdkTests.swift @@ -651,6 +651,73 @@ class DdSdkTests: XCTestCase { XCTFail("extra-info-4 is not of expected type or value") } } + + func testClearUserInfo() throws { + let bridge = DdSdkImplementation( + mainDispatchQueue: DispatchQueueMock(), + jsDispatchQueue: DispatchQueueMock(), + jsRefreshRateMonitor: JSRefreshRateMonitor(), + RUMMonitorProvider: { MockRUMMonitor() }, + RUMMonitorInternalProvider: { nil } + ) + bridge.initialize( + configuration: .mockAny(), + resolve: mockResolve, + reject: mockReject + ) + + bridge.setUserInfo( + userInfo: NSDictionary( + dictionary: [ + "id": "id_123", + "name": "John Doe", + "email": "john@doe.com", + "extraInfo": [ + "extra-info-1": 123, + "extra-info-2": "abc", + "extra-info-3": true, + "extra-info-4": [ + "nested-extra-info-1": 456 + ], + ], + ] + ), + resolve: mockResolve, + reject: mockReject + ) + + var ddContext = try XCTUnwrap(CoreRegistry.default as? DatadogCore).contextProvider.read() + var userInfo = try XCTUnwrap(ddContext.userInfo) + + XCTAssertEqual(userInfo.id, "id_123") + XCTAssertEqual(userInfo.name, "John Doe") + XCTAssertEqual(userInfo.email, "john@doe.com") + XCTAssertEqual(userInfo.extraInfo["extra-info-1"] as? Int64, 123) + XCTAssertEqual(userInfo.extraInfo["extra-info-2"] as? String, "abc") + XCTAssertEqual(userInfo.extraInfo["extra-info-3"] as? Bool, true) + + if let extraInfo4Encodable = userInfo.extraInfo["extra-info-4"] + as? DatadogSDKReactNative.AnyEncodable, + let extraInfo4Dict = extraInfo4Encodable.value as? [String: Int] + { + XCTAssertEqual(extraInfo4Dict, ["nested-extra-info-1": 456]) + } else { + XCTFail("extra-info-4 is not of expected type or value") + } + + bridge.clearUserInfo(resolve: mockResolve, reject: mockReject) + + ddContext = try XCTUnwrap(CoreRegistry.default as? DatadogCore).contextProvider.read() + userInfo = try XCTUnwrap(ddContext.userInfo) + + XCTAssertEqual(userInfo.id, nil) + XCTAssertEqual(userInfo.name, nil) + XCTAssertEqual(userInfo.email, nil) + XCTAssertEqual(userInfo.extraInfo["extra-info-1"] as? Int64, nil) + XCTAssertEqual(userInfo.extraInfo["extra-info-2"] as? String, nil) + XCTAssertEqual(userInfo.extraInfo["extra-info-3"] as? Bool, nil) + XCTAssertEqual(userInfo.extraInfo["extra-info-4"] as? [String: Int], nil) + } func testClearUserInfo() throws { let bridge = DdSdkImplementation( From 1b2baa74a698015b764bf1c57a322c62f473b553 Mon Sep 17 00:00:00 2001 From: Sergio Barrio Date: Mon, 29 Sep 2025 12:20:44 +0200 Subject: [PATCH 086/410] Update attribute API --- packages/core/ios/Tests/DdSdkTests.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/core/ios/Tests/DdSdkTests.swift b/packages/core/ios/Tests/DdSdkTests.swift index 5d0237530..cba49f84f 100644 --- a/packages/core/ios/Tests/DdSdkTests.swift +++ b/packages/core/ios/Tests/DdSdkTests.swift @@ -651,7 +651,7 @@ class DdSdkTests: XCTestCase { XCTFail("extra-info-4 is not of expected type or value") } } - + func testClearUserInfo() throws { let bridge = DdSdkImplementation( mainDispatchQueue: DispatchQueueMock(), @@ -704,12 +704,12 @@ class DdSdkTests: XCTestCase { } else { XCTFail("extra-info-4 is not of expected type or value") } - + bridge.clearUserInfo(resolve: mockResolve, reject: mockReject) - + ddContext = try XCTUnwrap(CoreRegistry.default as? DatadogCore).contextProvider.read() userInfo = try XCTUnwrap(ddContext.userInfo) - + XCTAssertEqual(userInfo.id, nil) XCTAssertEqual(userInfo.name, nil) XCTAssertEqual(userInfo.email, nil) From 2485aa256d97fb47c3380af2f9a5c84dcd188e06 Mon Sep 17 00:00:00 2001 From: Sergio Barrio Date: Thu, 16 Oct 2025 11:01:21 +0200 Subject: [PATCH 087/410] JS refresh rate normalization --- .../reactnative/DdSdkImplementation.kt | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt index 6741e971e..29fdcdce0 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt @@ -354,6 +354,32 @@ class DdSdkImplementation( return display.supportedModes.maxOf { it.refreshRate.toDouble() } } + // endregion + + val maxDeviceFrameTimeMs = 1000.0 / maxDeviceDisplayHz + val budgetFrameTimeMs = 1000.0 / frameBudgetHz + + if (listOf( + maxDeviceDisplayHz, frameTimeMs, frameBudgetHz, budgetFrameTimeMs, maxDeviceFrameTimeMs + ).any { !it.isFinite() || it <= 0.0 } + ) return 1.0 / DEFAULT_REFRESH_HZ + + + var normalizedFrameTimeMs = frameTimeMs / (maxDeviceFrameTimeMs / budgetFrameTimeMs) + + normalizedFrameTimeMs = max(normalizedFrameTimeMs, maxDeviceFrameTimeMs) + + return normalizedFrameTimeMs / 1000.0 // in seconds + } + + @Suppress("CyclomaticComplexMethod") + private fun getMaxDisplayRefreshRate(context: Context?): Double { + val dm = context?.getSystemService(Context.DISPLAY_SERVICE) as? DisplayManager ?: return 60.0 + val display: Display = dm.getDisplay(Display.DEFAULT_DISPLAY) ?: return DEFAULT_REFRESH_HZ + + return display.supportedModes.maxOf { it.refreshRate.toDouble() } + } + // endregion internal companion object { internal const val DEFAULT_APP_VERSION = "?" From 7645ae5cf26ac0674e568976e3543c33437d2d93 Mon Sep 17 00:00:00 2001 From: Sergio Barrio Date: Fri, 10 Oct 2025 14:52:21 +0200 Subject: [PATCH 088/410] Expose view Attributes API --- .../ios/Sources/DdSdkImplementation.swift | 25 +++++++++++-------- packages/core/ios/Tests/MockRUMMonitor.swift | 16 ------------ 2 files changed, 15 insertions(+), 26 deletions(-) diff --git a/packages/core/ios/Sources/DdSdkImplementation.swift b/packages/core/ios/Sources/DdSdkImplementation.swift index 2c5d4cdf3..5b91039b3 100644 --- a/packages/core/ios/Sources/DdSdkImplementation.swift +++ b/packages/core/ios/Sources/DdSdkImplementation.swift @@ -143,7 +143,7 @@ public class DdSdkImplementation: NSObject { resolve(nil) } - + @objc public func addUserExtraInfo( extraInfo: NSDictionary, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock @@ -155,13 +155,15 @@ public class DdSdkImplementation: NSObject { } @objc - public func clearUserInfo(resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { + public func clearUserInfo(resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) { Datadog.clearUserInfo() resolve(nil) } @objc - public func setTrackingConsent(trackingConsent: NSString, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { + public func setTrackingConsent( + trackingConsent: NSString, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock + ) { Datadog.set(trackingConsent: (trackingConsent as NSString?).asTrackingConsent()) resolve(nil) } @@ -173,7 +175,8 @@ public class DdSdkImplementation: NSObject { ) { let castedAttributes = castAttributesToSwift(attributes) let castedConfig = castAttributesToSwift(config) - DdTelemetry.sendTelemetryLog(message: message as String, attributes: castedAttributes, config: castedConfig) + DdTelemetry.sendTelemetryLog( + message: message as String, attributes: castedAttributes, config: castedConfig) resolve(nil) } @@ -184,13 +187,13 @@ public class DdSdkImplementation: NSObject { resolve(nil) } -#if os(iOS) @objc public func telemetryError(message: NSString, stack: NSString, kind: NSString, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { DdTelemetry.telemetryError(id: "datadog_react_native:\(String(describing: kind)):\(message)", message: message as String, kind: kind as String, stack: stack as String) resolve(nil) } +#if os(iOS) @objc public func consumeWebviewEvent( message: NSString, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock @@ -207,19 +210,21 @@ public class DdSdkImplementation: NSObject { resolve(nil) } + #endif - @objc - public func clearAllData(resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { + public func clearAllData(resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) { Datadog.clearAllData() resolve(nil) } - func overrideReactNativeTelemetry(rnConfiguration: DdSdkConfiguration) -> Void { + func overrideReactNativeTelemetry(rnConfiguration: DdSdkConfiguration) { DdTelemetry.overrideTelemetryConfiguration( - initializationType: rnConfiguration.configurationForTelemetry?.initializationType as? String, - reactNativeVersion: rnConfiguration.configurationForTelemetry?.reactNativeVersion as? String, + initializationType: rnConfiguration.configurationForTelemetry?.initializationType + as? String, + reactNativeVersion: rnConfiguration.configurationForTelemetry?.reactNativeVersion + as? String, reactVersion: rnConfiguration.configurationForTelemetry?.reactVersion as? String, trackCrossPlatformLongTasks: rnConfiguration.longTaskThresholdMs != 0, trackErrors: rnConfiguration.configurationForTelemetry?.trackErrors, diff --git a/packages/core/ios/Tests/MockRUMMonitor.swift b/packages/core/ios/Tests/MockRUMMonitor.swift index c692f6e7c..4a1d0cd92 100644 --- a/packages/core/ios/Tests/MockRUMMonitor.swift +++ b/packages/core/ios/Tests/MockRUMMonitor.swift @@ -10,22 +10,6 @@ @testable import DatadogSDKReactNative internal class MockRUMMonitor: RUMMonitorProtocol { - func addViewAttribute(forKey key: DatadogInternal.AttributeKey, value: any DatadogInternal.AttributeValue) { - // not implemented - } - - func addViewAttributes(_ attributes: [DatadogInternal.AttributeKey : any DatadogInternal.AttributeValue]) { - // not implemented - } - - func removeViewAttribute(forKey key: DatadogInternal.AttributeKey) { - // not implemented - } - - func removeViewAttributes(forKeys keys: [DatadogInternal.AttributeKey]) { - // not implemented - } - func currentSessionID(completion: @escaping (String?) -> Void) { // not implemented } From d892763e6263519412ec2e88fb06c431ec22fc89 Mon Sep 17 00:00:00 2001 From: Sergio Barrio Date: Mon, 27 Oct 2025 16:28:37 +0100 Subject: [PATCH 089/410] Handle optional String on removeAttributes --- .../main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt index 29fdcdce0..f02395ed8 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt @@ -115,8 +115,7 @@ class DdSdkImplementation( fun removeAttributes(keys: ReadableArray, promise: Promise) { val keysArray = mutableListOf() for (i in 0 until keys.size()) { - val key: String = keys.getString(i) - keysArray.add(key) + keys.getString(i)?.let { if (it.isNotBlank()) keysArray.add(it) } } val keysStringArray = keysArray.toTypedArray() From 7f3b5f7353e7a5103e22e6dfbe513c7c30305bbf Mon Sep 17 00:00:00 2001 From: Sergio Barrio Date: Mon, 27 Oct 2025 11:37:44 +0100 Subject: [PATCH 090/410] Bump minSdkVersion to 23 --- packages/core/android/build.gradle | 20 ++++--------------- packages/core/android/gradle.properties | 2 +- .../android/gradle.properties | 2 +- .../android/gradle.properties | 2 +- .../android/gradle.properties | 2 +- 5 files changed, 8 insertions(+), 20 deletions(-) diff --git a/packages/core/android/build.gradle b/packages/core/android/build.gradle index 59bb75cc3..1344b2531 100644 --- a/packages/core/android/build.gradle +++ b/packages/core/android/build.gradle @@ -195,22 +195,10 @@ dependencies { } implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" compileOnly "com.squareup.okhttp3:okhttp:3.12.13" - - // dd-sdk-android-rum requires androidx.metrics:metrics-performance. - // From 2.21.0, it uses 1.0.0-beta02, which requires Gradle 8.6.0. - // This breaks builds if the React Native target is below 0.76.0. as it relies on Gradle 8.5.0. - // To avoid this, we enforce 1.0.0-beta01 on RN < 0.76.0 - if (reactNativeMinorVersion < 76) { - implementation("com.datadoghq:dd-sdk-android-rum:3.1.0") { - exclude group: "androidx.metrics", module: "metrics-performance" - } - implementation "androidx.metrics:metrics-performance:1.0.0-beta01" - } else { - implementation "com.datadoghq:dd-sdk-android-rum:3.1.0" - } - implementation "com.datadoghq:dd-sdk-android-logs:3.1.0" - implementation "com.datadoghq:dd-sdk-android-trace:3.1.0" - implementation "com.datadoghq:dd-sdk-android-webview:3.1.0" + implementation "com.datadoghq:dd-sdk-android-rum:3.2.0" + implementation "com.datadoghq:dd-sdk-android-logs:3.2.0" + implementation "com.datadoghq:dd-sdk-android-trace:3.2.0" + implementation "com.datadoghq:dd-sdk-android-webview:3.2.0" implementation "com.google.code.gson:gson:2.10.0" testImplementation "org.junit.platform:junit-platform-launcher:1.6.2" testImplementation "org.junit.jupiter:junit-jupiter-api:5.6.2" diff --git a/packages/core/android/gradle.properties b/packages/core/android/gradle.properties index c9f7a205e..65f975c3e 100644 --- a/packages/core/android/gradle.properties +++ b/packages/core/android/gradle.properties @@ -1,5 +1,5 @@ DdSdkReactNative_kotlinVersion=1.8.21 -DdSdkReactNative_minSdkVersion=21 +DdSdkReactNative_minSdkVersion=23 DdSdkReactNative_compileSdkVersion=33 DdSdkReactNative_buildToolsVersion=33.0.0 DdSdkReactNative_targetSdkVersion=33 diff --git a/packages/internal-testing-tools/android/gradle.properties b/packages/internal-testing-tools/android/gradle.properties index 2a3186ab3..7a783c97d 100644 --- a/packages/internal-testing-tools/android/gradle.properties +++ b/packages/internal-testing-tools/android/gradle.properties @@ -1,5 +1,5 @@ DatadogInternalTesting_kotlinVersion=1.8.21 -DatadogInternalTesting_minSdkVersion=21 +DatadogInternalTesting_minSdkVersion=23 DatadogInternalTesting_compileSdkVersion=33 DatadogInternalTesting_buildToolsVersion=33.0.0 DatadogInternalTesting_targetSdkVersion=33 diff --git a/packages/react-native-session-replay/android/gradle.properties b/packages/react-native-session-replay/android/gradle.properties index 9f1573be5..d072f557e 100644 --- a/packages/react-native-session-replay/android/gradle.properties +++ b/packages/react-native-session-replay/android/gradle.properties @@ -1,6 +1,6 @@ DatadogSDKReactNativeSessionReplay_kotlinVersion=1.8.21 DatadogSDKReactNativeSessionReplay_compileSdkVersion=33 -DatadogSDKReactNativeSessionReplay_minSdkVersion=21 +DatadogSDKReactNativeSessionReplay_minSdkVersion=23 DatadogSDKReactNativeSessionReplay_buildToolsVersion=33.0.0 DatadogSDKReactNativeSessionReplay_targetSdkVersion=33 android.useAndroidX=true diff --git a/packages/react-native-webview/android/gradle.properties b/packages/react-native-webview/android/gradle.properties index 622c7b6b9..25112d024 100644 --- a/packages/react-native-webview/android/gradle.properties +++ b/packages/react-native-webview/android/gradle.properties @@ -1,5 +1,5 @@ DatadogSDKReactNativeWebView_kotlinVersion=1.7.21 -DatadogSDKReactNativeWebView_minSdkVersion=21 +DatadogSDKReactNativeWebView_minSdkVersion=23 DatadogSDKReactNativeWebView_compileSdkVersion=33 DatadogSDKReactNativeWebView_buildToolsVersion=33.0.0 DatadogSDKReactNativeWebView_targetSdkVersion=33 From c28e1fcfd61b20b95bc7d9c2f110931d8e1ca2d0 Mon Sep 17 00:00:00 2001 From: Carlos Nogueira Date: Wed, 5 Nov 2025 15:58:05 +0000 Subject: [PATCH 091/410] Expose sdk iOS config option `trackMemoryWarnings` --- packages/core/ios/Sources/DdSdkConfiguration.swift | 5 ++++- .../ios/Sources/DdSdkNativeInitialization.swift | 1 + .../core/ios/Sources/RNDdSdkConfiguration.swift | 9 +++++++-- packages/core/src/DdSdkReactNative.tsx | 3 ++- .../core/src/DdSdkReactNativeConfiguration.tsx | 14 +++++++++++++- .../DdSdkReactNativeConfiguration.test.ts | 3 +++ .../__tests__/initialization.test.tsx | 1 + .../__tests__/FileBasedConfiguration.test.ts | 3 +++ packages/core/src/types.tsx | 3 ++- 9 files changed, 36 insertions(+), 6 deletions(-) diff --git a/packages/core/ios/Sources/DdSdkConfiguration.swift b/packages/core/ios/Sources/DdSdkConfiguration.swift index 7a76cf39d..288e2a539 100644 --- a/packages/core/ios/Sources/DdSdkConfiguration.swift +++ b/packages/core/ios/Sources/DdSdkConfiguration.swift @@ -76,6 +76,7 @@ public class DdSdkConfiguration: NSObject { public var trackWatchdogTerminations: Bool public var batchProcessingLevel: Datadog.Configuration.BatchProcessingLevel public var initialResourceThreshold: Double? = nil + public var trackMemoryWarnings: Bool public init( clientToken: String, @@ -108,7 +109,8 @@ public class DdSdkConfiguration: NSObject { appHangThreshold: Double?, trackWatchdogTerminations: Bool, batchProcessingLevel: Datadog.Configuration.BatchProcessingLevel, - initialResourceThreshold: Double? + initialResourceThreshold: Double?, + trackMemoryWarnings: Bool = true ) { self.clientToken = clientToken self.env = env @@ -141,6 +143,7 @@ public class DdSdkConfiguration: NSObject { self.trackWatchdogTerminations = trackWatchdogTerminations self.batchProcessingLevel = batchProcessingLevel self.initialResourceThreshold = initialResourceThreshold + self.trackMemoryWarnings = trackMemoryWarnings } } diff --git a/packages/core/ios/Sources/DdSdkNativeInitialization.swift b/packages/core/ios/Sources/DdSdkNativeInitialization.swift index b55f90561..969f72abf 100644 --- a/packages/core/ios/Sources/DdSdkNativeInitialization.swift +++ b/packages/core/ios/Sources/DdSdkNativeInitialization.swift @@ -201,6 +201,7 @@ public class DdSdkNativeInitialization: NSObject { }, onSessionStart: DdSdkSessionStartedListener.instance.rumSessionListener, customEndpoint: customRUMEndpointURL, + trackMemoryWarnings: configuration.trackMemoryWarnings, telemetrySampleRate: (configuration.telemetrySampleRate as? NSNumber)?.floatValue ?? Float(DefaultConfiguration.telemetrySampleRate) ) } diff --git a/packages/core/ios/Sources/RNDdSdkConfiguration.swift b/packages/core/ios/Sources/RNDdSdkConfiguration.swift index 66437c481..6869d1711 100644 --- a/packages/core/ios/Sources/RNDdSdkConfiguration.swift +++ b/packages/core/ios/Sources/RNDdSdkConfiguration.swift @@ -43,6 +43,7 @@ extension NSDictionary { let trackWatchdogTerminations = object(forKey: "trackWatchdogTerminations") as? Bool let batchProcessingLevel = object(forKey: "batchProcessingLevel") as? NSString let initialResourceThreshold = object(forKey: "initialResourceThreshold") as? Double + let trackMemoryWarnings = object(forKey: "trackMemoryWarnings") as? Bool return DdSdkConfiguration( clientToken: (clientToken != nil) ? clientToken! : String(), @@ -75,7 +76,8 @@ extension NSDictionary { appHangThreshold: appHangThreshold, trackWatchdogTerminations: trackWatchdogTerminations ?? DefaultConfiguration.trackWatchdogTerminations, batchProcessingLevel: batchProcessingLevel.asBatchProcessingLevel(), - initialResourceThreshold: initialResourceThreshold + initialResourceThreshold: initialResourceThreshold, + trackMemoryWarnings: trackMemoryWarnings ?? DefaultConfiguration.trackMemoryWarnings ) } @@ -206,6 +208,7 @@ internal struct DefaultConfiguration { static let bundleLogsWithRum = true static let bundleLogsWithTraces = true static let trackWatchdogTerminations = false + static let trackMemoryWarnings = true } extension Dictionary where Key == String, Value == AnyObject { @@ -244,6 +247,7 @@ extension Dictionary where Key == String, Value == AnyObject { let trackWatchdogTerminations = configuration["trackWatchdogTerminations"] as? Bool let batchProcessingLevel = configuration["batchProcessingLevel"] as? NSString let initialResourceThreshold = configuration["initialResourceThreshold"] as? Double + let trackMemoryWarnings = configuration["trackMemoryWarnings"] as? Bool return DdSdkConfiguration( clientToken: clientToken ?? String(), @@ -279,7 +283,8 @@ extension Dictionary where Key == String, Value == AnyObject { appHangThreshold: appHangThreshold, trackWatchdogTerminations: trackWatchdogTerminations ?? DefaultConfiguration.trackWatchdogTerminations, batchProcessingLevel: batchProcessingLevel.asBatchProcessingLevel(), - initialResourceThreshold: initialResourceThreshold + initialResourceThreshold: initialResourceThreshold, + trackMemoryWarnings: trackMemoryWarnings ?? DefaultConfiguration.trackMemoryWarnings ) } } diff --git a/packages/core/src/DdSdkReactNative.tsx b/packages/core/src/DdSdkReactNative.tsx index 8360a695b..b1ea4f4c1 100644 --- a/packages/core/src/DdSdkReactNative.tsx +++ b/packages/core/src/DdSdkReactNative.tsx @@ -397,7 +397,8 @@ export class DdSdkReactNative { configuration.resourceTracingSamplingRate, configuration.trackWatchdogTerminations, configuration.batchProcessingLevel, - configuration.initialResourceThreshold + configuration.initialResourceThreshold, + configuration.trackMemoryWarnings ); }; diff --git a/packages/core/src/DdSdkReactNativeConfiguration.tsx b/packages/core/src/DdSdkReactNativeConfiguration.tsx index 44debb2d2..4ec1ef883 100644 --- a/packages/core/src/DdSdkReactNativeConfiguration.tsx +++ b/packages/core/src/DdSdkReactNativeConfiguration.tsx @@ -133,7 +133,8 @@ export const DEFAULTS = { bundleLogsWithTraces: true, useAccessibilityLabel: true, trackWatchdogTerminations: false, - batchProcessingLevel: BatchProcessingLevel.MEDIUM + batchProcessingLevel: BatchProcessingLevel.MEDIUM, + trackMemoryWarnings: true }; /** @@ -328,6 +329,16 @@ export class DdSdkReactNativeConfiguration { public trackWatchdogTerminations: boolean = DEFAULTS.trackWatchdogTerminations; + /** + * Enables tracking of memory warnings as RUM events. + * + * When enabled, the SDK will automatically record a RUM event each time the app + * receives a memory warning from the operating system. + * + * **Note:** This setting is only supported on **iOS**. It has no effect on other platforms. + */ + public trackMemoryWarnings: boolean = DEFAULTS.trackMemoryWarnings; + /** * Specifies a custom prop to name RUM actions on elements having an `onPress` prop. * @@ -469,6 +480,7 @@ export type PartialInitializationConfiguration = { readonly bundleLogsWithTraces?: boolean; readonly batchProcessingLevel?: BatchProcessingLevel; readonly initialResourceThreshold?: number; + readonly trackMemoryWarnings?: boolean; }; const setConfigurationAttribute = < diff --git a/packages/core/src/__tests__/DdSdkReactNativeConfiguration.test.ts b/packages/core/src/__tests__/DdSdkReactNativeConfiguration.test.ts index b1522ef7f..60a9d13ff 100644 --- a/packages/core/src/__tests__/DdSdkReactNativeConfiguration.test.ts +++ b/packages/core/src/__tests__/DdSdkReactNativeConfiguration.test.ts @@ -60,6 +60,7 @@ describe('DdSdkReactNativeConfiguration', () => { "trackErrors": false, "trackFrustrations": true, "trackInteractions": false, + "trackMemoryWarnings": true, "trackResources": false, "trackWatchdogTerminations": false, "trackingConsent": "granted", @@ -170,6 +171,7 @@ describe('DdSdkReactNativeConfiguration', () => { "trackErrors": true, "trackFrustrations": true, "trackInteractions": true, + "trackMemoryWarnings": true, "trackResources": true, "trackWatchdogTerminations": false, "trackingConsent": "pending", @@ -246,6 +248,7 @@ describe('DdSdkReactNativeConfiguration', () => { "trackErrors": false, "trackFrustrations": false, "trackInteractions": false, + "trackMemoryWarnings": true, "trackResources": false, "trackWatchdogTerminations": false, "trackingConsent": "granted", diff --git a/packages/core/src/sdk/DatadogProvider/__tests__/initialization.test.tsx b/packages/core/src/sdk/DatadogProvider/__tests__/initialization.test.tsx index c654cd24b..8ace4b731 100644 --- a/packages/core/src/sdk/DatadogProvider/__tests__/initialization.test.tsx +++ b/packages/core/src/sdk/DatadogProvider/__tests__/initialization.test.tsx @@ -100,6 +100,7 @@ describe('DatadogProvider', () => { "telemetrySampleRate": 20, "trackBackgroundEvents": false, "trackFrustrations": true, + "trackMemoryWarnings": true, "trackNonFatalAnrs": undefined, "trackWatchdogTerminations": false, "trackingConsent": "granted", diff --git a/packages/core/src/sdk/FileBasedConfiguration/__tests__/FileBasedConfiguration.test.ts b/packages/core/src/sdk/FileBasedConfiguration/__tests__/FileBasedConfiguration.test.ts index 6d3ee2e44..1670e5fe2 100644 --- a/packages/core/src/sdk/FileBasedConfiguration/__tests__/FileBasedConfiguration.test.ts +++ b/packages/core/src/sdk/FileBasedConfiguration/__tests__/FileBasedConfiguration.test.ts @@ -62,6 +62,7 @@ describe('FileBasedConfiguration', () => { "trackErrors": true, "trackFrustrations": true, "trackInteractions": true, + "trackMemoryWarnings": true, "trackResources": true, "trackWatchdogTerminations": false, "trackingConsent": "not_granted", @@ -154,6 +155,7 @@ describe('FileBasedConfiguration', () => { "trackErrors": true, "trackFrustrations": true, "trackInteractions": true, + "trackMemoryWarnings": true, "trackResources": true, "trackWatchdogTerminations": false, "trackingConsent": "not_granted", @@ -205,6 +207,7 @@ describe('FileBasedConfiguration', () => { "trackErrors": false, "trackFrustrations": true, "trackInteractions": false, + "trackMemoryWarnings": true, "trackResources": false, "trackWatchdogTerminations": false, "trackingConsent": "granted", diff --git a/packages/core/src/types.tsx b/packages/core/src/types.tsx index c8d9821cc..5c7d64cec 100644 --- a/packages/core/src/types.tsx +++ b/packages/core/src/types.tsx @@ -69,7 +69,8 @@ export class DdSdkConfiguration { readonly resourceTracingSamplingRate: number, readonly trackWatchdogTerminations: boolean | undefined, readonly batchProcessingLevel: BatchProcessingLevel, // eslint-disable-next-line no-empty-function - readonly initialResourceThreshold: number | undefined + readonly initialResourceThreshold: number | undefined, + readonly trackMemoryWarnings: boolean ) {} } From 87e16e56332e04f0a5ae87671bfb1bfdb242bc9f Mon Sep 17 00:00:00 2001 From: Carlos Nogueira Date: Mon, 3 Nov 2025 18:05:04 +0000 Subject: [PATCH 092/410] Expose `setAccountInfo` API to JS layer add account info to `applyEventMapper` --- .../datadog/reactnative/DatadogSDKWrapper.kt | 20 ++++- .../com/datadog/reactnative/DatadogWrapper.kt | 27 ++++++ .../reactnative/DdSdkImplementation.kt | 41 +++++++++ .../kotlin/com/datadog/reactnative/DdSdk.kt | 26 ++++++ .../kotlin/com/datadog/reactnative/DdSdk.kt | 26 ++++++ packages/core/ios/Sources/DdSdk.mm | 32 +++++++ .../ios/Sources/DdSdkImplementation.swift | 38 ++++++++ packages/core/jest/mock.js | 9 ++ packages/core/src/DdSdkReactNative.tsx | 66 ++++++++++++++ .../AccountInfoSingleton.ts | 46 ++++++++++ .../__tests__/AccountInfoSingleton.test.ts | 90 +++++++++++++++++++ .../src/sdk/AccountInfoSingleton/types.ts | 11 +++ .../core/src/sdk/EventMappers/EventMapper.ts | 5 ++ packages/core/src/specs/NativeDdSdk.ts | 17 ++++ packages/core/src/types.tsx | 27 ++++++ 15 files changed, 480 insertions(+), 1 deletion(-) create mode 100644 packages/core/src/sdk/AccountInfoSingleton/AccountInfoSingleton.ts create mode 100644 packages/core/src/sdk/AccountInfoSingleton/__tests__/AccountInfoSingleton.test.ts create mode 100644 packages/core/src/sdk/AccountInfoSingleton/types.ts diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogSDKWrapper.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogSDKWrapper.kt index 06151d834..56a15373c 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogSDKWrapper.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogSDKWrapper.kt @@ -89,7 +89,25 @@ internal class DatadogSDKWrapper : DatadogWrapper { override fun clearUserInfo() { Datadog.clearUserInfo() } - + + override fun setAccountInfo( + id: String, + name: String?, + extraInfo: Map + ) { + Datadog.setAccountInfo(id, name, extraInfo) + } + + override fun addAccountExtraInfo( + extraInfo: Map + ) { + Datadog.addAccountExtraInfo(extraInfo) + } + + override fun clearAccountInfo() { + Datadog.clearAccountInfo() + } + override fun addRumGlobalAttribute(key: String, value: Any?) { this.getRumMonitor().addAttribute(key, value) } diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogWrapper.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogWrapper.kt index d6395b18b..c72f2faef 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogWrapper.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogWrapper.kt @@ -91,6 +91,33 @@ interface DatadogWrapper { */ fun clearUserInfo() + /** + * Sets the account information. + * + * @param id a unique account identifier (relevant to your business domain) + * @param name (nullable) the account name + * @param extraInfo additional information. An extra information can be + * nested up to 8 levels deep. Keys using more than 8 levels will be sanitized by SDK. + */ + fun setAccountInfo( + id: String, + name: String?, + extraInfo: Map + ) + + /** + * Sets the account information. + * @param extraInfo: The additional information. (To set the id or name please use setAccountInfo). + */ + fun addAccountExtraInfo( + extraInfo: Map + ) + + /** + * Clears the account information. + */ + fun clearAccountInfo() + /** Adds a global attribute. * diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt index f02395ed8..02123ee8f 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt @@ -168,6 +168,47 @@ class DdSdkImplementation( promise.resolve(null) } + /** + * Set the account information. + * @param accountInfo The account object (use builtin attributes: 'id', 'name', and any custom + * attribute inside 'extraInfo'). + */ + fun setAccountInfo(accountInfo: ReadableMap, promise: Promise) { + val accountInfoMap = accountInfo.toHashMap().toMutableMap() + val id = accountInfoMap["id"] as? String + val name = accountInfoMap["name"] as? String + val extraInfo = (accountInfoMap["extraInfo"] as? Map<*, *>)?.filterKeys { it is String } + ?.mapKeys { it.key as String } + ?.mapValues { it.value } ?: emptyMap() + + if (id != null) { + datadog.setAccountInfo(id, name, extraInfo) + } + + promise.resolve(null) + } + + /** + * Sets the account extra information. + * @param accountExtraInfo: The additional information. (To set the id or name please use setAccountInfo). + */ + fun addAccountExtraInfo( + accountExtraInfo: ReadableMap, promise: Promise + ) { + val extraInfoMap = accountExtraInfo.toHashMap().toMutableMap() + + datadog.addAccountExtraInfo(extraInfoMap) + promise.resolve(null) + } + + /** + * Clears the account information. + */ + fun clearAccountInfo(promise: Promise) { + datadog.clearAccountInfo() + promise.resolve(null) + } + /** * Set the tracking consent regarding the data collection. * @param trackingConsent Consent, which can take one of the following values: 'pending', diff --git a/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdSdk.kt b/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdSdk.kt index a9d430081..421812545 100644 --- a/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdSdk.kt +++ b/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdSdk.kt @@ -106,6 +106,32 @@ class DdSdk( implementation.clearUserInfo(promise) } + /** + * Set the account information. + * @param account The account object (use builtin attributes: 'id', 'name', and any custom * attribute inside 'extraInfo'). + */ + @ReactMethod + override fun setAccountInfo(account: ReadableMap, promise: Promise) { + implementation.setAccountInfo(account, promise) + } + + /** + * Sets the account information. + * @param extraAccountInfo: The additional information. (To set the id or name please use setAccountInfo). + */ + @ReactMethod + override fun addAccountExtraInfo(extraInfo: ReadableMap, promise: Promise) { + implementation.addAccountExtraInfo(extraInfo, promise) + } + + /** + * Clears the account information. + */ + @ReactMethod + override fun clearAccountInfo(promise: Promise) { + implementation.clearAccountInfo(promise) + } + /** * Set the tracking consent regarding the data collection. * @param trackingConsent Consent, which can take one of the following values: 'pending', diff --git a/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt b/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt index 958ba521b..ef91ca549 100644 --- a/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt +++ b/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt @@ -132,6 +132,32 @@ class DdSdk( implementation.clearUserInfo(promise) } + /** + * Set the account information. + * @param account The account object (use builtin attributes: 'id', 'name', and any custom * attribute inside 'extraInfo'). + */ + @ReactMethod + fun setAccountInfo(account: ReadableMap, promise: Promise) { + implementation.setAccountInfo(account, promise) + } + + /** + * Sets the account information. + * @param extraAccountInfo: The additional information. (To set the id or name please use setAccountInfo). + */ + @ReactMethod + fun addAccountExtraInfo(extraInfo: ReadableMap, promise: Promise) { + implementation.addAccountExtraInfo(extraInfo, promise) + } + + /** + * Clears the account information. + */ + @ReactMethod + fun clearAccountInfo(promise: Promise) { + implementation.clearAccountInfo(promise) + } + /** * Set the tracking consent regarding the data collection. * @param trackingConsent Consent, which can take one of the following values: 'pending', diff --git a/packages/core/ios/Sources/DdSdk.mm b/packages/core/ios/Sources/DdSdk.mm index 06736a69e..c05ac6a7c 100644 --- a/packages/core/ios/Sources/DdSdk.mm +++ b/packages/core/ios/Sources/DdSdk.mm @@ -79,6 +79,26 @@ + (void)initFromNative { [self clearUserInfo:resolve reject:reject]; } +RCT_REMAP_METHOD(setAccountInfo, withAccountInfo:(NSDictionary*)accountInfo + withResolver:(RCTPromiseResolveBlock)resolve + withRejecter:(RCTPromiseRejectBlock)reject) +{ + [self setAccountInfo:accountInfo resolve:resolve reject:reject]; +} + +RCT_REMAP_METHOD(addAccountExtraInfo, withAccountExtraInfo:(NSDictionary*)extraInfo + withResolver:(RCTPromiseResolveBlock)resolve + withRejecter:(RCTPromiseRejectBlock)reject) +{ + [self addAccountExtraInfo:extraInfo resolve:resolve reject:reject]; +} + +RCT_EXPORT_METHOD(clearAccountInfo:(RCTPromiseResolveBlock)resolve + withRejecter:(RCTPromiseRejectBlock)reject) +{ + [self clearAccountInfo:resolve reject:reject]; +} + RCT_REMAP_METHOD(setTrackingConsent, withTrackingConsent:(NSString*)trackingConsent withResolver:(RCTPromiseResolveBlock)resolve withRejecter:(RCTPromiseRejectBlock)reject) @@ -191,6 +211,18 @@ -(void)addUserExtraInfo:(NSDictionary *)extraInfo resolve:(RCTPromiseResolveBloc [self.ddSdkImplementation addUserExtraInfoWithExtraInfo:extraInfo resolve:resolve reject:reject]; } +- (void)setAccountInfo:(NSDictionary *)accountInfo resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + [self.ddSdkImplementation setAccountInfoWithAccountInfo:accountInfo resolve:resolve reject:reject]; +} + +- (void)clearAccountInfo:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + [self.ddSdkImplementation clearAccountInfoWithResolve:resolve reject:reject]; +} + +-(void)addAccountExtraInfo:(NSDictionary *)extraInfo resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + [self.ddSdkImplementation addAccountExtraInfoWithExtraInfo:extraInfo resolve:resolve reject:reject]; +} + - (void)sendTelemetryLog:(NSString *)message attributes:(NSDictionary *)attributes config:(NSDictionary *)config resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { [self.ddSdkImplementation sendTelemetryLogWithMessage:message attributes:attributes config:config resolve:resolve reject:reject]; } diff --git a/packages/core/ios/Sources/DdSdkImplementation.swift b/packages/core/ios/Sources/DdSdkImplementation.swift index 5b91039b3..db8c4e938 100644 --- a/packages/core/ios/Sources/DdSdkImplementation.swift +++ b/packages/core/ios/Sources/DdSdkImplementation.swift @@ -160,6 +160,44 @@ public class DdSdkImplementation: NSObject { resolve(nil) } + @objc + public func setAccountInfo( + accountInfo: NSDictionary, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock + ) { + let castedAccountInfo = castAttributesToSwift(accountInfo) + let id = castedAccountInfo["id"] as? String + let name = castedAccountInfo["name"] as? String + var extraInfo: [AttributeKey: AttributeValue] = [:] + + if let extraInfoEncodable = castedAccountInfo["extraInfo"] as? AnyEncodable, + let extraInfoDict = extraInfoEncodable.value as? [String: Any] + { + extraInfo = castAttributesToSwift(extraInfoDict) + } + + if let validId = id { + Datadog.setAccountInfo(id: validId, name: name, extraInfo: extraInfo) + } + + resolve(nil) + } + + @objc + public func addAccountExtraInfo( + extraInfo: NSDictionary, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock + ) { + let castedExtraInfo = castAttributesToSwift(extraInfo) + + Datadog.addAccountExtraInfo(castedExtraInfo) + resolve(nil) + } + + @objc + public func clearAccountInfo(resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) { + Datadog.clearAccountInfo() + resolve(nil) + } + @objc public func setTrackingConsent( trackingConsent: NSString, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock diff --git a/packages/core/jest/mock.js b/packages/core/jest/mock.js index 0dc9ec138..9f5ee2c41 100644 --- a/packages/core/jest/mock.js +++ b/packages/core/jest/mock.js @@ -36,6 +36,15 @@ module.exports = { clearUserInfo: jest .fn() .mockImplementation(() => new Promise(resolve => resolve())), + setAccountInfo: jest + .fn() + .mockImplementation(() => new Promise(resolve => resolve())), + addAccountExtraInfo: jest + .fn() + .mockImplementation(() => new Promise(resolve => resolve())), + clearAccountInfo: jest + .fn() + .mockImplementation(() => new Promise(resolve => resolve())), addAttribute: jest .fn() .mockImplementation(() => new Promise(resolve => resolve())), diff --git a/packages/core/src/DdSdkReactNative.tsx b/packages/core/src/DdSdkReactNative.tsx index b1ea4f4c1..e07ba4e34 100644 --- a/packages/core/src/DdSdkReactNative.tsx +++ b/packages/core/src/DdSdkReactNative.tsx @@ -30,6 +30,7 @@ import { DdRumErrorTracking } from './rum/instrumentation/DdRumErrorTracking'; import { DdBabelInteractionTracking } from './rum/instrumentation/interactionTracking/DdBabelInteractionTracking'; import { DdRumUserInteractionTracking } from './rum/instrumentation/interactionTracking/DdRumUserInteractionTracking'; import { DdRumResourceTracking } from './rum/instrumentation/resourceTracking/DdRumResourceTracking'; +import { AccountInfoSingleton } from './sdk/AccountInfoSingleton/AccountInfoSingleton'; import { AttributesSingleton } from './sdk/AttributesSingleton/AttributesSingleton'; import type { Attributes } from './sdk/AttributesSingleton/types'; import { registerNativeBridge } from './sdk/DatadogInternalBridge/DdSdkInternalNativeBridge'; @@ -299,6 +300,71 @@ export class DdSdkReactNative { UserInfoSingleton.getInstance().setUserInfo(updatedUserInfo); }; + /** + * Sets the account information. + * @param id: A mandatory unique account identifier (relevant to your business domain). + * @param name: The account name. + * @param extraInfo: Additional information. + * @returns a Promise. + */ + static setAccountInfo = async (accountInfo: { + id: string; + name?: string; + extraInfo?: Record; + }): Promise => { + InternalLog.log( + `Setting account ${JSON.stringify(accountInfo)}`, + SdkVerbosity.DEBUG + ); + + await DdSdk.setAccountInfo(accountInfo); + AccountInfoSingleton.getInstance().setAccountInfo(accountInfo); + }; + + /** + * Clears the account information. + * @returns a Promise. + */ + static clearAccountInfo = async (): Promise => { + InternalLog.log('Clearing account info', SdkVerbosity.DEBUG); + await DdSdk.clearAccountInfo(); + AccountInfoSingleton.getInstance().clearAccountInfo(); + }; + + /** + * Set the account information. + * @param extraAccountInfo: The additional information. (To set the id or name please use setAccountInfo). + * @returns a Promise. + */ + static addAccountExtraInfo = async ( + extraAccountInfo: Record + ): Promise => { + InternalLog.log( + `Adding extra account info ${JSON.stringify(extraAccountInfo)}`, + SdkVerbosity.DEBUG + ); + + const accountInfo = AccountInfoSingleton.getInstance().getAccountInfo(); + if (!accountInfo) { + InternalLog.log( + 'Skipped adding Account Extra Info: Account Info is currently undefined. An account ID must be set before adding extra info. Please call setAccountInfo() first.', + SdkVerbosity.WARN + ); + + return; + } + + const extraInfo = { + ...accountInfo.extraInfo, + ...extraAccountInfo + }; + + await DdSdk.addAccountExtraInfo(extraInfo); + AccountInfoSingleton.getInstance().addAccountExtraInfo( + extraAccountInfo + ); + }; + /** * Set the tracking consent regarding the data collection. * @param trackingConsent: One of TrackingConsent values. diff --git a/packages/core/src/sdk/AccountInfoSingleton/AccountInfoSingleton.ts b/packages/core/src/sdk/AccountInfoSingleton/AccountInfoSingleton.ts new file mode 100644 index 000000000..5f2be8dea --- /dev/null +++ b/packages/core/src/sdk/AccountInfoSingleton/AccountInfoSingleton.ts @@ -0,0 +1,46 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +import type { AccountInfo } from './types'; + +class AccountInfoProvider { + private accountInfo: AccountInfo | undefined = undefined; + + setAccountInfo = (accountInfo: AccountInfo) => { + this.accountInfo = accountInfo; + }; + + addAccountExtraInfo = (extraInfo: AccountInfo['extraInfo']) => { + if (!this.accountInfo) { + return; + } + + this.accountInfo.extraInfo = { + ...this.accountInfo.extraInfo, + ...extraInfo + }; + }; + + getAccountInfo = (): AccountInfo | undefined => { + return this.accountInfo; + }; + + clearAccountInfo = () => { + this.accountInfo = undefined; + }; +} + +export class AccountInfoSingleton { + private static accountInfoProvider = new AccountInfoProvider(); + + static getInstance = (): AccountInfoProvider => { + return AccountInfoSingleton.accountInfoProvider; + }; + + static reset = () => { + AccountInfoSingleton.accountInfoProvider = new AccountInfoProvider(); + }; +} diff --git a/packages/core/src/sdk/AccountInfoSingleton/__tests__/AccountInfoSingleton.test.ts b/packages/core/src/sdk/AccountInfoSingleton/__tests__/AccountInfoSingleton.test.ts new file mode 100644 index 000000000..9af387619 --- /dev/null +++ b/packages/core/src/sdk/AccountInfoSingleton/__tests__/AccountInfoSingleton.test.ts @@ -0,0 +1,90 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +import { AccountInfoSingleton } from '../AccountInfoSingleton'; + +describe('AccountInfoSingleton', () => { + beforeEach(() => { + AccountInfoSingleton.reset(); + }); + + it('returns undefined by default', () => { + expect( + AccountInfoSingleton.getInstance().getAccountInfo() + ).toBeUndefined(); + }); + + it('stores and returns account info after `setAccountInfo`', () => { + const info = { + id: 'test', + name: 'test user', + extraInfo: { premium: true } + }; + + AccountInfoSingleton.getInstance().setAccountInfo(info); + + expect(AccountInfoSingleton.getInstance().getAccountInfo()).toEqual( + info + ); + }); + + it('adds extra account info with `addAccountExtraInfo`', () => { + const info = { + id: 'test', + name: 'test user', + extraInfo: { premium: true } + }; + + AccountInfoSingleton.getInstance().setAccountInfo(info); + AccountInfoSingleton.getInstance().addAccountExtraInfo({ + testGroup: 'A' + }); + + expect(AccountInfoSingleton.getInstance().getAccountInfo()).toEqual({ + ...info, + extraInfo: { ...info.extraInfo, testGroup: 'A' } + }); + }); + + it('clears account info with `clearAccountInfo`', () => { + AccountInfoSingleton.getInstance().setAccountInfo({ + id: 'test', + name: 'test user', + extraInfo: { premium: true } + }); + + AccountInfoSingleton.getInstance().clearAccountInfo(); + + expect( + AccountInfoSingleton.getInstance().getAccountInfo() + ).toBeUndefined(); + }); + + it('`reset()` replaces the provider and clears stored account info', () => { + const instanceBefore = AccountInfoSingleton.getInstance(); + + AccountInfoSingleton.getInstance().setAccountInfo({ + id: 'test', + name: 'test user', + extraInfo: { premium: true } + }); + + AccountInfoSingleton.reset(); + + const instanceAfter = AccountInfoSingleton.getInstance(); + + expect(instanceAfter).not.toBe(instanceBefore); + + expect(instanceAfter.getAccountInfo()).toBeUndefined(); + }); + + it('getInstance returns the same provider between calls (singleton behavior)', () => { + const a = AccountInfoSingleton.getInstance(); + const b = AccountInfoSingleton.getInstance(); + + expect(a).toBe(b); + }); +}); diff --git a/packages/core/src/sdk/AccountInfoSingleton/types.ts b/packages/core/src/sdk/AccountInfoSingleton/types.ts new file mode 100644 index 000000000..1dceb0958 --- /dev/null +++ b/packages/core/src/sdk/AccountInfoSingleton/types.ts @@ -0,0 +1,11 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +export type AccountInfo = { + readonly id: string; + readonly name?: string; + extraInfo?: Record; +}; diff --git a/packages/core/src/sdk/EventMappers/EventMapper.ts b/packages/core/src/sdk/EventMappers/EventMapper.ts index 9ca252d72..e1cbaae19 100644 --- a/packages/core/src/sdk/EventMappers/EventMapper.ts +++ b/packages/core/src/sdk/EventMappers/EventMapper.ts @@ -7,6 +7,8 @@ import { InternalLog } from '../../InternalLog'; import { SdkVerbosity } from '../../SdkVerbosity'; import { DdSdk } from '../../sdk/DdSdk'; +import { AccountInfoSingleton } from '../AccountInfoSingleton/AccountInfoSingleton'; +import type { AccountInfo } from '../AccountInfoSingleton/types'; import { AttributesSingleton } from '../AttributesSingleton/AttributesSingleton'; import type { Attributes } from '../AttributesSingleton/types'; import { UserInfoSingleton } from '../UserInfoSingleton/UserInfoSingleton'; @@ -16,6 +18,7 @@ import { deepClone } from './utils/deepClone'; export type AdditionalEventDataForMapper = { userInfo?: UserInfo; + accountInfo?: AccountInfo; attributes: Attributes; }; @@ -66,9 +69,11 @@ export class EventMapper { // formatting const userInfo = UserInfoSingleton.getInstance().getUserInfo(); + const accountInfo = AccountInfoSingleton.getInstance().getAccountInfo(); const attributes = AttributesSingleton.getInstance().getAttributes(); const initialEvent = this.formatRawEventForMapper(rawEvent, { userInfo, + accountInfo, attributes }); diff --git a/packages/core/src/specs/NativeDdSdk.ts b/packages/core/src/specs/NativeDdSdk.ts index 70401fe3c..68c9c4711 100644 --- a/packages/core/src/specs/NativeDdSdk.ts +++ b/packages/core/src/specs/NativeDdSdk.ts @@ -67,6 +67,23 @@ export interface Spec extends TurboModule { */ addUserExtraInfo(extraInfo: Object): Promise; + /** + * Set the account information. + * @param account: The account object (use builtin attributes: 'id', 'name', and any custom attribute under extraInfo). + */ + setAccountInfo(account: Object): Promise; + + /** + * Clears the account information. + */ + clearAccountInfo(): Promise; + + /** + * Add custom attributes to the current account information + * @param extraInfo: The extraInfo object containing additional custom attributes + */ + addAccountExtraInfo(extraInfo: Object): Promise; + /** * Set the tracking consent regarding the data collection. * @param trackingConsent: Consent, which can take one of the following values: 'pending', 'granted', 'not_granted'. diff --git a/packages/core/src/types.tsx b/packages/core/src/types.tsx index 5c7d64cec..7f97779ee 100644 --- a/packages/core/src/types.tsx +++ b/packages/core/src/types.tsx @@ -129,6 +129,27 @@ export type DdSdkType = { */ addUserExtraInfo(extraUserInfo: Record): Promise; + /** + * Sets the account information. + * @param id: A unique account identifier (relevant to your business domain) + * @param name: The account name. + * @param extraInfo: Additional information. + */ + setAccountInfo(accountInfo: AccountInfo): Promise; + + /** + * Clears the account information. + */ + clearAccountInfo(): Promise; + + /** + * Add additional account information. + * @param extraAccountInfo: The additional information. (To set the id or name please use setAccountInfo). + */ + addAccountExtraInfo( + extraAccountInfo: Record + ): Promise; + /** * Set the tracking consent regarding the data collection. * @param trackingConsent: Consent, which can take one of the following values: 'pending', 'granted', 'not_granted'. @@ -176,6 +197,12 @@ export type UserInfo = { extraInfo?: object; }; +export type AccountInfo = { + id: string; + name?: string; + extraInfo?: object; +}; + // DdLogs export type LogStatus = 'debug' | 'info' | 'warn' | 'error'; From 5174d55b43e0384c8a73dacbabeabe13c1174452 Mon Sep 17 00:00:00 2001 From: Carlos Nogueira Date: Fri, 7 Nov 2025 14:49:40 +0000 Subject: [PATCH 093/410] Add `userId` and `accountId`to baggage headers Preserve user baggage header when setting session ID Enforced W3 specification for 'baggage' header Only inject Session ID header if propagator=Datadog|W3C Additional test for baggage header and minor warn message improvement --- packages/core/src/rum/DdRum.ts | 32 ++++++++------ packages/core/src/rum/__tests__/DdRum.test.ts | 2 +- packages/core/src/rum/helper.ts | 36 +++++++++++++++ .../distributedTracing/distributedTracing.tsx | 21 +++++++-- .../distributedTracingHeaders.ts | 44 ++++++++++++++++--- .../requestProxy/XHRProxy/XHRProxy.ts | 10 ++++- .../XHRProxy/__tests__/XHRProxy.test.ts | 2 +- .../XHRProxy/baggageHeaderUtils.ts | 36 +-------------- .../DdSdkInternalNativeBridge.tsx | 2 +- 9 files changed, 122 insertions(+), 63 deletions(-) create mode 100644 packages/core/src/rum/helper.ts diff --git a/packages/core/src/rum/DdRum.ts b/packages/core/src/rum/DdRum.ts index bedc6f33a..1f3703e63 100644 --- a/packages/core/src/rum/DdRum.ts +++ b/packages/core/src/rum/DdRum.ts @@ -20,12 +20,19 @@ import { getGlobalInstance } from '../utils/singletonUtils'; import { DefaultTimeProvider } from '../utils/time-provider/DefaultTimeProvider'; import type { TimeProvider } from '../utils/time-provider/TimeProvider'; -import { generateActionEventMapper } from './eventMappers/actionEventMapper'; import type { ActionEventMapper } from './eventMappers/actionEventMapper'; -import { generateErrorEventMapper } from './eventMappers/errorEventMapper'; +import { generateActionEventMapper } from './eventMappers/actionEventMapper'; import type { ErrorEventMapper } from './eventMappers/errorEventMapper'; -import { generateResourceEventMapper } from './eventMappers/resourceEventMapper'; +import { generateErrorEventMapper } from './eventMappers/errorEventMapper'; import type { ResourceEventMapper } from './eventMappers/resourceEventMapper'; +import { generateResourceEventMapper } from './eventMappers/resourceEventMapper'; +import { + clearCachedSessionId, + getCachedAccountId, + getCachedSessionId, + getCachedUserId, + setCachedSessionId +} from './helper'; import type { DatadogTracingContext } from './instrumentation/resourceTracking/distributedTracing/DatadogTracingContext'; import { DatadogTracingIdentifier } from './instrumentation/resourceTracking/distributedTracing/DatadogTracingIdentifier'; import { TracingIdentifier } from './instrumentation/resourceTracking/distributedTracing/TracingIdentifier'; @@ -33,17 +40,12 @@ import { getTracingContext, getTracingContextForPropagators } from './instrumentation/resourceTracking/distributedTracing/distributedTracingHeaders'; -import { - clearCachedSessionId, - getCachedSessionId, - setCachedSessionId -} from './sessionId/sessionIdHelper'; import type { DdRumType, - RumActionType, - ResourceKind, FirstPartyHost, - PropagatorType + PropagatorType, + ResourceKind, + RumActionType } from './types'; const RUM_MODULE = 'com.datadog.reactnative.rum'; @@ -383,7 +385,9 @@ class DdRumWrapper implements DdRumType { url, tracingSamplingRate, firstPartyHosts, - getCachedSessionId() + getCachedSessionId(), + getCachedUserId(), + getCachedAccountId() ); }; @@ -394,7 +398,9 @@ class DdRumWrapper implements DdRumType { return getTracingContextForPropagators( propagators, tracingSamplingRate, - getCachedSessionId() + getCachedSessionId(), + getCachedUserId(), + getCachedAccountId() ); }; diff --git a/packages/core/src/rum/__tests__/DdRum.test.ts b/packages/core/src/rum/__tests__/DdRum.test.ts index 41b873fe9..e4318322b 100644 --- a/packages/core/src/rum/__tests__/DdRum.test.ts +++ b/packages/core/src/rum/__tests__/DdRum.test.ts @@ -17,11 +17,11 @@ import { DdRum } from '../DdRum'; import type { ActionEventMapper } from '../eventMappers/actionEventMapper'; import type { ErrorEventMapper } from '../eventMappers/errorEventMapper'; import type { ResourceEventMapper } from '../eventMappers/resourceEventMapper'; +import { setCachedSessionId } from '../helper'; import { DatadogTracingContext } from '../instrumentation/resourceTracking/distributedTracing/DatadogTracingContext'; import { DatadogTracingIdentifier } from '../instrumentation/resourceTracking/distributedTracing/DatadogTracingIdentifier'; import { TracingIdFormat } from '../instrumentation/resourceTracking/distributedTracing/TracingIdentifier'; import { TracingIdentifierUtils } from '../instrumentation/resourceTracking/distributedTracing/__tests__/__utils__/TracingIdentifierUtils'; -import { setCachedSessionId } from '../sessionId/sessionIdHelper'; import type { FirstPartyHost } from '../types'; import { PropagatorType, RumActionType } from '../types'; diff --git a/packages/core/src/rum/helper.ts b/packages/core/src/rum/helper.ts new file mode 100644 index 000000000..a153b79b5 --- /dev/null +++ b/packages/core/src/rum/helper.ts @@ -0,0 +1,36 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ +let _cachedSessionId: string | undefined; +let _cachedUserId: string | undefined; +let _cachedAccountId: string | undefined; + +export const getCachedSessionId = () => { + return _cachedSessionId; +}; + +export const setCachedSessionId = (sessionId: string) => { + _cachedSessionId = sessionId; +}; + +export const clearCachedSessionId = () => { + _cachedSessionId = undefined; +}; + +export const getCachedUserId = () => { + return _cachedUserId; +}; + +export const setCachedUserId = (userId: string) => { + _cachedUserId = userId; +}; + +export const getCachedAccountId = () => { + return _cachedAccountId; +}; + +export const setCachedAccountId = (accountId: string) => { + _cachedAccountId = accountId; +}; diff --git a/packages/core/src/rum/instrumentation/resourceTracking/distributedTracing/distributedTracing.tsx b/packages/core/src/rum/instrumentation/resourceTracking/distributedTracing/distributedTracing.tsx index 9c4fcbff7..2ebb98233 100644 --- a/packages/core/src/rum/instrumentation/resourceTracking/distributedTracing/distributedTracing.tsx +++ b/packages/core/src/rum/instrumentation/resourceTracking/distributedTracing/distributedTracing.tsx @@ -26,6 +26,9 @@ export type DdRumResourceTracingAttributes = rulePsr: number; propagatorTypes: PropagatorType[]; rumSessionId?: string; + userId?: string; + accountId?: string; + baggageHeaders?: Set; } | { tracingStrategy: 'DISCARD'; @@ -43,12 +46,16 @@ export const getTracingAttributes = ({ hostname, firstPartyHostsRegexMap, tracingSamplingRate, - rumSessionId + rumSessionId, + userId, + accountId }: { hostname: Hostname | null; firstPartyHostsRegexMap: RegexMap; tracingSamplingRate: number; rumSessionId?: string; + userId?: string; + accountId?: string; }): DdRumResourceTracingAttributes => { if (hostname === null) { return DISCARDED_TRACE_ATTRIBUTES; @@ -61,7 +68,9 @@ export const getTracingAttributes = ({ return generateTracingAttributesWithSampling( tracingSamplingRate, propagatorsForHost, - rumSessionId + rumSessionId, + userId, + accountId ); } return DISCARDED_TRACE_ATTRIBUTES; @@ -70,7 +79,9 @@ export const getTracingAttributes = ({ export const generateTracingAttributesWithSampling = ( tracingSamplingRate: number, propagatorTypes: PropagatorType[], - rumSessionId?: string + rumSessionId?: string, + userId?: string, + accountId?: string ): DdRumResourceTracingAttributes => { if (!propagatorTypes || propagatorTypes.length === 0) { return DISCARDED_TRACE_ATTRIBUTES; @@ -93,7 +104,9 @@ export const generateTracingAttributesWithSampling = ( tracingStrategy: 'KEEP', rulePsr: tracingSamplingRate / 100, propagatorTypes, - rumSessionId + rumSessionId, + userId, + accountId }; return tracingAttributes; diff --git a/packages/core/src/rum/instrumentation/resourceTracking/distributedTracing/distributedTracingHeaders.ts b/packages/core/src/rum/instrumentation/resourceTracking/distributedTracing/distributedTracingHeaders.ts index 8bfaec669..8ad9137c7 100644 --- a/packages/core/src/rum/instrumentation/resourceTracking/distributedTracing/distributedTracingHeaders.ts +++ b/packages/core/src/rum/instrumentation/resourceTracking/distributedTracing/distributedTracingHeaders.ts @@ -1,6 +1,5 @@ /* - * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. - * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. This product includes software developed at Datadog (https://www.datadoghq.com/). * Copyright 2016-Present Datadog, Inc. */ @@ -29,6 +28,8 @@ export const PARENT_ID_HEADER_KEY = 'x-datadog-parent-id'; export const TAGS_HEADER_KEY = 'x-datadog-tags'; export const DD_TRACE_ID_TAG = '_dd.p.tid'; export const DD_RUM_SESSION_ID_TAG = 'session.id'; +export const DD_RUM_USER_ID_TAG = 'user.id'; +export const DD_RUM_ACCOUNT_ID_TAG = 'account.id'; /** * OTel headers @@ -143,6 +144,29 @@ export const getTracingHeadersFromAttributes = ( } }); + if (hasDatadogOrW3CPropagator) { + if (tracingAttributes.rumSessionId) { + headers.push({ + header: BAGGAGE_HEADER_KEY, + value: `${DD_RUM_SESSION_ID_TAG}=${tracingAttributes.rumSessionId}` + }); + } + + if (tracingAttributes.userId) { + headers.push({ + header: BAGGAGE_HEADER_KEY, + value: `${DD_RUM_USER_ID_TAG}=${tracingAttributes.userId}` + }); + } + + if (tracingAttributes.accountId) { + headers.push({ + header: BAGGAGE_HEADER_KEY, + value: `${DD_RUM_ACCOUNT_ID_TAG}=${tracingAttributes.accountId}` + }); + } + } + if (hasDatadogOrW3CPropagator && tracingAttributes.rumSessionId) { headers.push({ header: BAGGAGE_HEADER_KEY, @@ -157,7 +181,9 @@ export const getTracingContext = ( url: string, tracingSamplingRate: number, firstPartyHosts: FirstPartyHost[], - rumSessionId?: string + rumSessionId?: string, + userId?: string, + accountId?: string ): DatadogTracingContext => { const hostname = URLHostParser(url); const firstPartyHostsRegexMap = firstPartyHostsRegexMapBuilder( @@ -167,7 +193,9 @@ export const getTracingContext = ( hostname, firstPartyHostsRegexMap, tracingSamplingRate, - rumSessionId + rumSessionId, + userId, + accountId }); return getTracingContextForAttributes( @@ -179,13 +207,17 @@ export const getTracingContext = ( export const getTracingContextForPropagators = ( propagators: PropagatorType[], tracingSamplingRate: number, - rumSessionId?: string + rumSessionId?: string, + userId?: string, + accountId?: string ): DatadogTracingContext => { return getTracingContextForAttributes( generateTracingAttributesWithSampling( tracingSamplingRate, propagators, - rumSessionId + rumSessionId, + userId, + accountId ), tracingSamplingRate ); diff --git a/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/XHRProxy.ts b/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/XHRProxy.ts index d48c4f01a..723ace5ed 100644 --- a/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/XHRProxy.ts +++ b/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/XHRProxy.ts @@ -5,7 +5,11 @@ */ import { Timer } from '../../../../../utils/Timer'; -import { getCachedSessionId } from '../../../../sessionId/sessionIdHelper'; +import { + getCachedAccountId, + getCachedSessionId, + getCachedUserId +} from '../../../../helper'; import { BAGGAGE_HEADER_KEY, getTracingHeadersFromAttributes @@ -115,7 +119,9 @@ const proxyOpen = ( hostname, firstPartyHostsRegexMap, tracingSamplingRate, - rumSessionId: getCachedSessionId() + rumSessionId: getCachedSessionId(), + userId: getCachedUserId(), + accountId: getCachedAccountId() }), baggageHeaderEntries: new Set() }; diff --git a/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/__tests__/XHRProxy.test.ts b/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/__tests__/XHRProxy.test.ts index cfc5e0178..048dbadec 100644 --- a/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/__tests__/XHRProxy.test.ts +++ b/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/__tests__/XHRProxy.test.ts @@ -11,7 +11,7 @@ import { InternalLog } from '../../../../../../InternalLog'; import { SdkVerbosity } from '../../../../../../SdkVerbosity'; import { BufferSingleton } from '../../../../../../sdk/DatadogProvider/Buffer/BufferSingleton'; import { DdRum } from '../../../../../DdRum'; -import { setCachedSessionId } from '../../../../../sessionId/sessionIdHelper'; +import { setCachedSessionId } from '../../../../../helper'; import { PropagatorType } from '../../../../../types'; import { XMLHttpRequestMock } from '../../../__tests__/__utils__/XMLHttpRequestMock'; import { TracingIdentifierUtils } from '../../../distributedTracing/__tests__/__utils__/TracingIdentifierUtils'; diff --git a/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/baggageHeaderUtils.ts b/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/baggageHeaderUtils.ts index dccae9ddf..7094ec4e8 100644 --- a/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/baggageHeaderUtils.ts +++ b/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/baggageHeaderUtils.ts @@ -110,7 +110,7 @@ export function formatBaggageHeader(entries: Set): string | null { } const headerValue = formattedParts.join(','); - const byteLength = utf8ByteLength(headerValue); + const byteLength = Buffer.byteLength(headerValue, 'utf8'); if (byteLength > MAX_BYTES) { InternalLog.log( @@ -122,40 +122,6 @@ export function formatBaggageHeader(entries: Set): string | null { return headerValue; } -/** - * Returns the number of bytes needed to encode a string in UTF-8. - * - * Useful as a lightweight alternative to Node.js `Buffer.byteLength()` - * for older environments that do not support it. - * - * @param text - The input string. - * @returns The UTF-8 byte length of the string. - */ -function utf8ByteLength(text: string): number { - let byteLength = text.length; - for (let i = text.length - 1; i >= 0; i--) { - const code = text.charCodeAt(i); - - // 2-byte characters (U+0080 to U+07FF) - if (code > 0x7f && code <= 0x7ff) { - byteLength++; - } - // 3-byte characters (U+0800 to U+FFFF) - else if (code > 0x7ff && code <= 0xffff) { - byteLength += 2; - } - - // Handle surrogate pairs (4-byte characters, e.g. emoji) - // These characters already count as 2 in the initial length - // Encountering the low surrogate already accounts for the full 4 bytes - // (2 from the initial length + 2 for the 3-byte characters logic above) - if (code >= 0xdc00 && code <= 0xdfff) { - i--; // prevents double counting the same character by skipping high surrogate - } - } - return byteLength; -} - /** * Returns a set of valid baggage header characters. */ diff --git a/packages/core/src/sdk/DatadogInternalBridge/DdSdkInternalNativeBridge.tsx b/packages/core/src/sdk/DatadogInternalBridge/DdSdkInternalNativeBridge.tsx index bbae2cd12..34a1e623a 100644 --- a/packages/core/src/sdk/DatadogInternalBridge/DdSdkInternalNativeBridge.tsx +++ b/packages/core/src/sdk/DatadogInternalBridge/DdSdkInternalNativeBridge.tsx @@ -5,7 +5,7 @@ */ import { InternalLog } from '../../InternalLog'; import { SdkVerbosity } from '../../SdkVerbosity'; -import { setCachedSessionId } from '../../rum/sessionId/sessionIdHelper'; +import { setCachedSessionId } from '../../rum/helper'; import { DatadogDefaultEventEmitter } from '../DatadogEventEmitter/DatadogDefaultEventEmitter'; import type { DatadogEventEmitter } from '../DatadogEventEmitter/DatadogEventEmitter'; From 3e8503610f75fba918f7663c3c99a4a72000694c Mon Sep 17 00:00:00 2001 From: Carlos Nogueira Date: Thu, 13 Nov 2025 17:24:34 +0000 Subject: [PATCH 094/410] Cache `userId` and `accountId` when first set --- .../requestProxy/XHRProxy/__tests__/XHRProxy.test.ts | 10 +++++++++- .../sdk/AccountInfoSingleton/AccountInfoSingleton.ts | 3 +++ .../src/sdk/UserInfoSingleton/UserInfoSingleton.ts | 3 +++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/__tests__/XHRProxy.test.ts b/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/__tests__/XHRProxy.test.ts index 048dbadec..a18825771 100644 --- a/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/__tests__/XHRProxy.test.ts +++ b/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/__tests__/XHRProxy.test.ts @@ -11,7 +11,11 @@ import { InternalLog } from '../../../../../../InternalLog'; import { SdkVerbosity } from '../../../../../../SdkVerbosity'; import { BufferSingleton } from '../../../../../../sdk/DatadogProvider/Buffer/BufferSingleton'; import { DdRum } from '../../../../../DdRum'; -import { setCachedSessionId } from '../../../../../helper'; +import { + setCachedSessionId, + setCachedUserId, + setCachedAccountId +} from '../../../../../helper'; import { PropagatorType } from '../../../../../types'; import { XMLHttpRequestMock } from '../../../__tests__/__utils__/XMLHttpRequestMock'; import { TracingIdentifierUtils } from '../../../distributedTracing/__tests__/__utils__/TracingIdentifierUtils'; @@ -90,6 +94,10 @@ afterEach(() => { (Date.now as jest.MockedFunction).mockClear(); jest.spyOn(global.Math, 'random').mockRestore(); DdRum.unregisterResourceEventMapper(); + + setCachedSessionId(undefined as any); + setCachedUserId(undefined as any); + setCachedAccountId(undefined as any); }); describe('XHRProxy', () => { diff --git a/packages/core/src/sdk/AccountInfoSingleton/AccountInfoSingleton.ts b/packages/core/src/sdk/AccountInfoSingleton/AccountInfoSingleton.ts index 5f2be8dea..439c1493a 100644 --- a/packages/core/src/sdk/AccountInfoSingleton/AccountInfoSingleton.ts +++ b/packages/core/src/sdk/AccountInfoSingleton/AccountInfoSingleton.ts @@ -4,6 +4,8 @@ * Copyright 2016-Present Datadog, Inc. */ +import { setCachedAccountId } from '../../rum/helper'; + import type { AccountInfo } from './types'; class AccountInfoProvider { @@ -11,6 +13,7 @@ class AccountInfoProvider { setAccountInfo = (accountInfo: AccountInfo) => { this.accountInfo = accountInfo; + setCachedAccountId(this.accountInfo.id); }; addAccountExtraInfo = (extraInfo: AccountInfo['extraInfo']) => { diff --git a/packages/core/src/sdk/UserInfoSingleton/UserInfoSingleton.ts b/packages/core/src/sdk/UserInfoSingleton/UserInfoSingleton.ts index 3ce23614b..2408fdbf2 100644 --- a/packages/core/src/sdk/UserInfoSingleton/UserInfoSingleton.ts +++ b/packages/core/src/sdk/UserInfoSingleton/UserInfoSingleton.ts @@ -4,6 +4,8 @@ * Copyright 2016-Present Datadog, Inc. */ +import { setCachedUserId } from '../../rum/helper'; + import type { UserInfo } from './types'; class UserInfoProvider { @@ -11,6 +13,7 @@ class UserInfoProvider { setUserInfo = (userInfo: UserInfo) => { this.userInfo = userInfo; + setCachedUserId(this.userInfo.id); }; getUserInfo = (): UserInfo | undefined => { From fcabd0503266d0a2a8b79e9e4b7dab89a71a4772 Mon Sep 17 00:00:00 2001 From: Carlos Nogueira Date: Fri, 21 Nov 2025 11:20:51 +0000 Subject: [PATCH 095/410] Integrate Feature Operations into `core` SDK --- packages/codepush/__mocks__/react-native.ts | 11 +++- packages/core/__mocks__/react-native.ts | 11 +++- .../reactnative/DdRumImplementation.kt | 54 ++++++++++++++++ .../kotlin/com/datadog/reactnative/DdRum.kt | 63 ++++++++++++++++++- .../kotlin/com/datadog/reactnative/DdRum.kt | 56 +++++++++++++++++ packages/core/ios/Sources/DdRum.mm | 43 +++++++++++++ .../ios/Sources/DdRumImplementation.swift | 51 +++++++++++++++ packages/core/jest/mock.js | 9 +++ packages/core/src/index.tsx | 3 +- packages/core/src/rum/DdRum.ts | 54 +++++++++++++++- packages/core/src/rum/types.ts | 42 ++++++++++++- packages/core/src/specs/NativeDdRum.ts | 40 ++++++++++++ packages/core/src/types.tsx | 6 ++ .../__mocks__/react-native.ts | 11 +++- 14 files changed, 447 insertions(+), 7 deletions(-) diff --git a/packages/codepush/__mocks__/react-native.ts b/packages/codepush/__mocks__/react-native.ts index 0c8189840..a87659170 100644 --- a/packages/codepush/__mocks__/react-native.ts +++ b/packages/codepush/__mocks__/react-native.ts @@ -119,7 +119,16 @@ actualRN.NativeModules.DdRum = { new Promise(resolve => resolve('test-session-id') ) - ) as jest.MockedFunction + ) as jest.MockedFunction, + startFeatureOperation: jest.fn().mockImplementation( + () => new Promise(resolve => resolve()) + ) as jest.MockedFunction, + succeedFeatureOperation: jest.fn().mockImplementation( + () => new Promise(resolve => resolve()) + ) as jest.MockedFunction, + failFeatureOperation: jest.fn().mockImplementation( + () => new Promise(resolve => resolve()) + ) as jest.MockedFunction }; module.exports = actualRN; diff --git a/packages/core/__mocks__/react-native.ts b/packages/core/__mocks__/react-native.ts index 73308f711..a4622b6b9 100644 --- a/packages/core/__mocks__/react-native.ts +++ b/packages/core/__mocks__/react-native.ts @@ -155,7 +155,16 @@ actualRN.NativeModules.DdRum = { new Promise(resolve => resolve('test-session-id') ) - ) as jest.MockedFunction + ) as jest.MockedFunction, + startFeatureOperation: jest.fn().mockImplementation( + () => new Promise(resolve => resolve()) + ) as jest.MockedFunction, + succeedFeatureOperation: jest.fn().mockImplementation( + () => new Promise(resolve => resolve()) + ) as jest.MockedFunction, + failFeatureOperation: jest.fn().mockImplementation( + () => new Promise(resolve => resolve()) + ) as jest.MockedFunction }; module.exports = actualRN; diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdRumImplementation.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdRumImplementation.kt index 4e3cd416f..67a299f36 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdRumImplementation.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdRumImplementation.kt @@ -12,6 +12,7 @@ import com.datadog.android.rum.RumAttributes import com.datadog.android.rum.RumErrorSource import com.datadog.android.rum.RumResourceKind import com.datadog.android.rum.RumResourceMethod +import com.datadog.android.rum.featureoperations.FailureReason import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReadableArray import com.facebook.react.bridge.ReadableMap @@ -333,6 +334,59 @@ class DdRumImplementation(private val datadog: DatadogWrapper = DatadogSDKWrappe } } + /** + * Starts a Feature Operation. + * + * @param name Human-readable operation name (e.g., "login_flow"). + * @param operationKey Optional key that uniquely identifies this operation instance. + * @param attributes Additional attributes to attach to the operation. + * @param promise Resolved with `null` when the call completes. + */ + fun startFeatureOperation(name: String, operationKey: String? = null, attributes: ReadableMap, promise: Promise) { + val attributesMap = attributes.toHashMap().toMutableMap() + datadog.getRumMonitor().startFeatureOperation(name, operationKey, attributesMap); + promise.resolve(null) + } + + /** + * Marks a Feature Operation as successfully completed. + * + * @param name The name of the feature operation (for example, `"login_flow"`). + * @param operationKey The key of the operation instance to complete, if one was provided when starting it. + * @param attributes A map of custom attributes to attach to this completion event. + */ + fun succeedFeatureOperation(name: String, operationKey: String? = null, attributes: ReadableMap, promise: Promise) { + val attributesMap = attributes.toHashMap().toMutableMap() + datadog.getRumMonitor().succeedFeatureOperation(name, operationKey, attributesMap) + promise.resolve(null) + } + + + /** + * Marks a Feature Operation as failed. + * + * @param name The name of the feature operation (for example, `"login_flow"`). + * @param operationKey The key of the operation instance to fail, if one was provided when starting it. + * @param failureReason The reason for the failure. Possible values are defined in [FailureReason] + * (e.g., `FailureReason.ERROR`, `FailureReason.ABANDONED`, `FailureReason.OTHER`). + * @param attributes A map of custom attributes to attach to this failure event. + */ + fun failFeatureOperation( + name: String, + operationKey: String? = null, + failureReason: String, + attributes: ReadableMap, + promise: Promise + ) { + val attributesMap = attributes.toHashMap().toMutableMap() + val reason = runCatching { + enumValueOf(failureReason.uppercase()) + }.getOrDefault(FailureReason.OTHER) + + datadog.getRumMonitor().failFeatureOperation(name, operationKey, reason, attributesMap) + promise.resolve(null) + } + // region Internal private fun String.asRumActionType(): RumActionType { diff --git a/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdRum.kt b/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdRum.kt index 6cb2b385b..30788acff 100644 --- a/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdRum.kt +++ b/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdRum.kt @@ -6,6 +6,7 @@ package com.datadog.reactnative +import com.datadog.android.rum.featureoperations.FailureReason import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.ReactMethod @@ -52,7 +53,12 @@ class DdRum( * If not provided, current timestamp will be used. */ @ReactMethod - override fun stopView(key: String, context: ReadableMap, timestampMs: Double, promise: Promise) { + override fun stopView( + key: String, + context: ReadableMap, + timestampMs: Double, + promise: Promise + ) { implementation.stopView(key, context, timestampMs, promise) } @@ -276,4 +282,59 @@ class DdRum( override fun getCurrentSessionId(promise: Promise) { implementation.getCurrentSessionId(promise) } + + /** + * Starts a RUM Feature Operation. + * + * @param name Human-readable operation name (e.g., "login_flow"). + * @param operationKey Optional key that uniquely identifies this operation instance. + * @param attributes Additional attributes to attach to the operation. + * @param promise Resolved with `null` when the call completes. + */ + @ReactMethod + override fun startFeatureOperation( + name: String, + operationKey: String?, + attributes: ReadableMap, + promise: Promise + ) { + implementation.startFeatureOperation(name, operationKey, attributes, promise) + } + + /** + * Marks a Feature Operation as successfully completed. + * + * @param name The name of the feature operation (for example, `"login_flow"`). + * @param operationKey The key of the operation instance to complete, if one was provided when starting it. + * @param attributes A map of custom attributes to attach to this completion event. + */ + @ReactMethod + override fun succeedFeatureOperation( + name: String, + operationKey: String?, + attributes: ReadableMap, + promise: Promise + ) { + implementation.succeedFeatureOperation(name, operationKey, attributes, promise) + } + + /** + * Marks a Feature Operation as failed. + * + * @param name The name of the feature operation (for example, `"login_flow"`). + * @param operationKey The key of the operation instance to fail, if one was provided when starting it. + * @param failureReason The reason for the failure. Possible values are defined in [FailureReason] + * (e.g., `FailureReason.ERROR`, `FailureReason.ABANDONED`, `FailureReason.OTHER`). + * @param attributes A map of custom attributes to attach to this failure event. + */ + @ReactMethod + override fun failFeatureOperation( + name: String, + operationKey: String?, + failureReason: String, + attributes: ReadableMap, + promise: Promise + ) { + implementation.failFeatureOperation(name, operationKey, failureReason, attributes, promise) + } } diff --git a/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdRum.kt b/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdRum.kt index a6c4965ea..4ef8409b2 100644 --- a/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdRum.kt +++ b/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdRum.kt @@ -6,6 +6,7 @@ package com.datadog.reactnative +import com.datadog.android.rum.featureoperations.FailureReason import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.ReactContextBaseJavaModule @@ -266,4 +267,59 @@ class DdRum( fun getCurrentSessionId(promise: Promise) { implementation.getCurrentSessionId(promise) } + + /** + * Starts a RUM Feature Operation. + * + * @param name Human-readable operation name (e.g., "login_flow"). + * @param operationKey Optional key that uniquely identifies this operation instance. + * @param attributes Additional attributes to attach to the operation. + * @param promise Resolved with `null` when the call completes. + */ + @ReactMethod + fun startFeatureOperation( + name: String, + operationKey: String? = null, + attributes: ReadableMap, + promise: Promise + ) { + implementation.startFeatureOperation(name, operationKey, attributes, promise) + } + + /** + * Marks a Feature Operation as successfully completed. + * + * @param name The name of the feature operation (for example, "login_flow"). + * @param operationKey The key of the operation instance to complete, if one was provided. + * @param attributes A map of custom attributes to attach to this completion event. + */ + @ReactMethod + fun succeedFeatureOperation( + name: String, + operationKey: String? = null, + attributes: ReadableMap, + promise: Promise + ) { + implementation.succeedFeatureOperation(name, operationKey, attributes, promise) + } + + /** + * Marks a Feature Operation as failed. + * + * @param name The name of the feature operation (for example, "login_flow"). + * @param operationKey The key of the operation instance to fail, if one was provided. + * @param failureReason The reason for the failure. Values are defined in [FailureReason] + * (e.g., `FailureReason.ERROR`, `FailureReason.ABANDONED`, `FailureReason.OTHER`). + * @param attributes A map of custom attributes to attach to this failure event. + */ + @ReactMethod + fun failFeatureOperation( + name: String, + operationKey: String? = null, + failureReason: String, + attributes: ReadableMap, + promise: Promise + ) { + implementation.failFeatureOperation(name, operationKey, failureReason, attributes, promise) + } } diff --git a/packages/core/ios/Sources/DdRum.mm b/packages/core/ios/Sources/DdRum.mm index f5c324ce8..c891537f3 100644 --- a/packages/core/ios/Sources/DdRum.mm +++ b/packages/core/ios/Sources/DdRum.mm @@ -164,6 +164,37 @@ @implementation DdRum [self getCurrentSessionId:resolve reject:reject]; } +RCT_REMAP_METHOD(startFeatureOperation, + startWithName:(NSString*)name + withOperationKey:(NSString*)operationKey + withAttributes:(NSDictionary*)attributes + withResolver:(RCTPromiseResolveBlock)resolve + withRejecter:(RCTPromiseRejectBlock)reject) +{ + [self startFeatureOperation:name operationKey:operationKey attributes:attributes resolve:resolve reject:reject]; +} + +RCT_REMAP_METHOD(succeedFeatureOperation, + succeedWithName:(NSString*)name + withOperationKey:(NSString*)operationKey + withAttributes:(NSDictionary*)attributes + withResolver:(RCTPromiseResolveBlock)resolve + withRejecter:(RCTPromiseRejectBlock)reject) +{ + [self succeedFeatureOperation:name operationKey:operationKey attributes:attributes resolve:resolve reject:reject]; +} + +RCT_REMAP_METHOD(failFeatureOperation, + failWithName:(NSString*)name + withOperationKey:(NSString*)operationKey + withReason:(NSString*)reason + withAttributes:(NSDictionary*)attributes + withResolver:(RCTPromiseResolveBlock)resolve + withRejecter:(RCTPromiseRejectBlock)reject) +{ + [self failFeatureOperation:name operationKey:operationKey reason:reason attributes:attributes resolve:resolve reject:reject]; +} + // Thanks to this guard, we won't compile this code when we build for the old architecture. #ifdef RCT_NEW_ARCH_ENABLED - (std::shared_ptr)getTurboModule: @@ -257,4 +288,16 @@ - (void)stopView:(NSString *)key context:(NSDictionary *)context timestampMs:(do [self.ddRumImplementation stopViewWithKey:key context:context timestampMs:timestampMs resolve:resolve reject:reject]; } +- (void) startFeatureOperation:(NSString *)name operationKey:(NSString *)operationKey attributes:(NSDictionary *)attributes resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + [self.ddRumImplementation startFeatureOperationWithName:name operationKey:operationKey attributes:attributes resolve:resolve reject:reject]; +} + +- (void) succeedFeatureOperation:(NSString *)name operationKey:(NSString *)operationKey attributes:(NSDictionary *)attributes resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + [self.ddRumImplementation succeedFeatureOperationWithName:name operationKey:operationKey attributes:attributes resolve:resolve reject:reject]; +} + +- (void) failFeatureOperation:(NSString *)name operationKey:(NSString *)operationKey reason:(NSString *)reason attributes:(NSDictionary *)attributes resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + [self.ddRumImplementation failFeatureOperationWithName:name operationKey:operationKey reason:reason attributes:attributes resolve:resolve reject:reject]; +} + @end diff --git a/packages/core/ios/Sources/DdRumImplementation.swift b/packages/core/ios/Sources/DdRumImplementation.swift index 6fac21f82..0ac8a19bf 100644 --- a/packages/core/ios/Sources/DdRumImplementation.swift +++ b/packages/core/ios/Sources/DdRumImplementation.swift @@ -63,6 +63,16 @@ private extension RUMMethod { } } +internal extension RUMFeatureOperationFailureReason { + init(from string: String) { + switch string.lowercased() { + case "error": self = .error + case "abandoned": self = .abandoned + default: self = .other + } + } +} + @objc public class DdRumImplementation: NSObject { internal static let timestampKey = "_dd.timestamp" @@ -236,6 +246,47 @@ public class DdRumImplementation: NSObject { resolve(sessionId) } } + + @objc + public func startFeatureOperation( + name: String, + operationKey: String?, + attributes: NSDictionary, + resolve: @escaping (Any?) -> Void, + reject: RCTPromiseRejectBlock + ){ + let castedAttributes = castAttributesToSwift(attributes) + nativeRUM.startFeatureOperation(name: name, operationKey: operationKey, attributes: castedAttributes) + resolve(nil) + } + + @objc + public func succeedFeatureOperation( + name: String, + operationKey: String?, + attributes: NSDictionary, + resolve: @escaping (Any?) -> Void, + reject: RCTPromiseRejectBlock + ){ + let castedAttributes = castAttributesToSwift(attributes) + nativeRUM.succeedFeatureOperation(name: name, operationKey: operationKey, attributes: castedAttributes) + resolve(nil) + } + + @objc + public func failFeatureOperation( + name: String, + operationKey: String?, + reason: String, + attributes: NSDictionary, + resolve: @escaping (Any?) -> Void, + reject: RCTPromiseRejectBlock + ){ + let castedAttributes = castAttributesToSwift(attributes) + nativeRUM.failFeatureOperation(name: name, operationKey: operationKey, + reason: RUMFeatureOperationFailureReason(from: reason), attributes: castedAttributes) + resolve(nil) + } // MARK: - Private methods diff --git a/packages/core/jest/mock.js b/packages/core/jest/mock.js index 9f5ee2c41..aadc79d27 100644 --- a/packages/core/jest/mock.js +++ b/packages/core/jest/mock.js @@ -154,6 +154,15 @@ module.exports = { .mockImplementation( () => new Promise(resolve => resolve('test-session-id')) ), + startFeatureOperation: jest + .fn() + .mockImplementation(() => new Promise(resolve => resolve())), + succeedFeatureOperation: jest + .fn() + .mockImplementation(() => new Promise(resolve => resolve())), + failFeatureOperation: jest + .fn() + .mockImplementation(() => new Promise() < (resolve => resolve())), setTimeProvider: jest.fn().mockImplementation(() => {}), timeProvider: jest.fn().mockReturnValue(undefined), getTracingContext: jest.fn().mockReturnValue(undefined), diff --git a/packages/core/src/index.tsx b/packages/core/src/index.tsx index 062fecc90..ce39c9dfa 100644 --- a/packages/core/src/index.tsx +++ b/packages/core/src/index.tsx @@ -42,7 +42,7 @@ import { DatadogProvider } from './sdk/DatadogProvider/DatadogProvider'; import { DdSdk } from './sdk/DdSdk'; import { FileBasedConfiguration } from './sdk/FileBasedConfiguration/FileBasedConfiguration'; import { DdTrace } from './trace/DdTrace'; -import { ErrorSource } from './types'; +import { ErrorSource, FeatureOperationFailure } from './types'; import { DefaultTimeProvider } from './utils/time-provider/DefaultTimeProvider'; import type { Timestamp } from './utils/time-provider/TimeProvider'; import { TimeProvider } from './utils/time-provider/TimeProvider'; @@ -57,6 +57,7 @@ export { DdRum, RumActionType, ErrorSource, + FeatureOperationFailure, DdSdkReactNativeConfiguration, DdSdkReactNative, DdSdk, diff --git a/packages/core/src/rum/DdRum.ts b/packages/core/src/rum/DdRum.ts index 1f3703e63..3ae7c10ce 100644 --- a/packages/core/src/rum/DdRum.ts +++ b/packages/core/src/rum/DdRum.ts @@ -13,7 +13,7 @@ import type { Attributes } from '../sdk/AttributesSingleton/types'; import { bufferVoidNativeCall } from '../sdk/DatadogProvider/Buffer/bufferNativeCall'; import { DdSdk } from '../sdk/DdSdk'; import { GlobalState } from '../sdk/GlobalState/GlobalState'; -import type { ErrorSource } from '../types'; +import type { ErrorSource, FeatureOperationFailure } from '../types'; import { validateContext } from '../utils/argsUtils'; import { getErrorContext } from '../utils/errorUtils'; import { getGlobalInstance } from '../utils/singletonUtils'; @@ -133,6 +133,58 @@ class DdRumWrapper implements DdRumType { return this.callNativeStopAction(...nativeCallArgs); }; + startFeatureOperation( + name: string, + operationKey: string | null, + attributes: object + ): Promise { + InternalLog.log( + `Starting feature operation “${name}” (${operationKey})`, + SdkVerbosity.DEBUG + ); + return bufferVoidNativeCall(() => + this.nativeRum.startFeatureOperation(name, operationKey, attributes) + ); + } + + succeedFeatureOperation( + name: string, + operationKey: string | null, + attributes: object + ): Promise { + InternalLog.log( + `Succeding feature operation “${name}” (${operationKey})`, + SdkVerbosity.DEBUG + ); + return bufferVoidNativeCall(() => + this.nativeRum.succeedFeatureOperation( + name, + operationKey, + attributes + ) + ); + } + + failFeatureOperation( + name: string, + operationKey: string | null, + reason: FeatureOperationFailure, + attributes: object + ): Promise { + InternalLog.log( + `Failing feature operation “${name}” (${operationKey})`, + SdkVerbosity.DEBUG + ); + return bufferVoidNativeCall(() => + this.nativeRum.failFeatureOperation( + name, + operationKey, + reason, + attributes + ) + ); + } + setTimeProvider = (timeProvider: TimeProvider): void => { this.timeProvider = timeProvider; }; diff --git a/packages/core/src/rum/types.ts b/packages/core/src/rum/types.ts index 3def7f0e6..b879010f7 100644 --- a/packages/core/src/rum/types.ts +++ b/packages/core/src/rum/types.ts @@ -5,7 +5,7 @@ */ import type { Attributes } from '../sdk/AttributesSingleton/types'; -import type { ErrorSource } from '../types'; +import type { ErrorSource, FeatureOperationFailure } from '../types'; import type { DatadogTracingContext } from './instrumentation/resourceTracking/distributedTracing/DatadogTracingContext'; import type { DatadogTracingIdentifier } from './instrumentation/resourceTracking/distributedTracing/DatadogTracingIdentifier'; @@ -230,6 +230,46 @@ export type DdRumType = { * Generates a unique 128bit Span ID. */ generateSpanId(): DatadogTracingIdentifier; + + /** + * Starts a Feature Operation, representing a high-level logical flow within your application (e.g., `login_flow`). + * @param name - The name of the feature operation (for example, `"login_flow"`). + * @param operationKey - An optional key to uniquely identify a specific instance of this operation when multiple are running concurrently. + * @param attributes - Custom attributes to attach to this operation. + */ + startFeatureOperation( + name: string, + operationKey: string | null, + attributes: object + ): Promise; + + /** + * Marks a Feature Operation as successfully completed. + * Should be called when a previously started operation (via `startFeatureOperation`) finishes without error. + * @param name - The name of the feature operation (for example, `"login_flow"`). + * @param operationKey - The key for the operation instance to complete, if it was specified when starting it. + * @param attributes - Custom attributes to attach to this operation’s completion event. + */ + succeedFeatureOperation( + name: string, + operationKey: string | null, + attributes: object + ): Promise; + + /** + * Marks a Feature Operation as failed. + * Should be called when a previously started operation (via `startFeatureOperation`) ends with an error. + * @param name - The name of the feature operation (for example, `"login_flow"`). + * @param operationKey - The key for the operation instance to fail, if it was specified when starting it. + * @param reason - The reason for the failure. + * @param attributes - Custom attributes to attach to this operation’s failure event. + */ + failFeatureOperation( + name: string, + operationKey: string | null, + reason: FeatureOperationFailure, + attributes: object + ): Promise; }; /** diff --git a/packages/core/src/specs/NativeDdRum.ts b/packages/core/src/specs/NativeDdRum.ts index e31f5b925..9c0c459b3 100644 --- a/packages/core/src/specs/NativeDdRum.ts +++ b/packages/core/src/specs/NativeDdRum.ts @@ -185,6 +185,46 @@ export interface Spec extends TurboModule { * Get current Session ID, or `undefined` if not available. */ getCurrentSessionId(): Promise; + + /** + * Starts a Feature Operation, representing a high-level logical flow within your application (e.g., `login_flow`). + * @param name - The name of the feature operation (for example, `"login_flow"`). + * @param operationKey - An optional key to uniquely identify a specific instance of this operation when multiple are running concurrently. + * @param attributes - Custom attributes to attach to this operation. + */ + startFeatureOperation( + name: string, + operationKey: string | null, + attributes: Object + ): Promise; + + /** + * Marks a Feature Operation as successfully completed. + * Should be called when a previously started operation (via `startFeatureOperation`) finishes without error. + * @param name - The name of the feature operation (for example, `"login_flow"`). + * @param operationKey - The key for the operation instance to complete, if it was specified when starting it. + * @param attributes - Custom attributes to attach to this operation’s completion event. + */ + succeedFeatureOperation( + name: string, + operationKey: string | null, + attributes: Object + ): Promise; + + /** + * Marks a Feature Operation as failed. + * Should be called when a previously started operation (via `startFeatureOperation`) ends with an error. + * @param name - The name of the feature operation (for example, `"login_flow"`). + * @param operationKey - The key for the operation instance to fail, if it was specified when starting it. + * @param reason - The reason for the failure. + * @param attributes - Custom attributes to attach to this operation’s failure event. + */ + failFeatureOperation( + name: string, + operationKey: string | null, + reason: string, + attributes: Object + ): Promise; } // eslint-disable-next-line import/no-default-export diff --git a/packages/core/src/types.tsx b/packages/core/src/types.tsx index 7f97779ee..cd697c04b 100644 --- a/packages/core/src/types.tsx +++ b/packages/core/src/types.tsx @@ -232,3 +232,9 @@ export enum ErrorSource { WEBVIEW = 'WEBVIEW', CUSTOM = 'CUSTOM' } + +export enum FeatureOperationFailure { + ERROR = 'ERROR', + ABANDONED = 'ABANDONED', + OTHER = 'OTHER' +} diff --git a/packages/react-native-apollo-client/__mocks__/react-native.ts b/packages/react-native-apollo-client/__mocks__/react-native.ts index bbac607d3..ded32499f 100644 --- a/packages/react-native-apollo-client/__mocks__/react-native.ts +++ b/packages/react-native-apollo-client/__mocks__/react-native.ts @@ -110,7 +110,16 @@ actualRN.NativeModules.DdRum = { new Promise(resolve => resolve('test-session-id') ) - ) as jest.MockedFunction + ) as jest.MockedFunction, + startFeatureOperation: jest.fn().mockImplementation( + () => new Promise(resolve => resolve()) + ) as jest.MockedFunction, + succeedFeatureOperation: jest.fn().mockImplementation( + () => new Promise(resolve => resolve()) + ) as jest.MockedFunction, + failFeatureOperation: jest.fn().mockImplementation( + () => new Promise(resolve => resolve()) + ) as jest.MockedFunction }; module.exports = actualRN; From 68d150aff03bfc8826361751a2bc11faa497b7d1 Mon Sep 17 00:00:00 2001 From: Sergio Barrio Date: Tue, 18 Nov 2025 09:57:58 +0100 Subject: [PATCH 096/410] Remove defaultPrivacyLevel from Session Replay --- ...lemetryConfigurationEventForgeryFactory.kt | 1 - .../src/SessionReplay.ts | 41 +------------------ .../src/__tests__/SessionReplay.test.ts | 38 +---------------- 3 files changed, 2 insertions(+), 78 deletions(-) diff --git a/packages/core/android/src/test/kotlin/com/datadog/tools/unit/forge/TelemetryConfigurationEventForgeryFactory.kt b/packages/core/android/src/test/kotlin/com/datadog/tools/unit/forge/TelemetryConfigurationEventForgeryFactory.kt index 10e894e6e..684d92c21 100644 --- a/packages/core/android/src/test/kotlin/com/datadog/tools/unit/forge/TelemetryConfigurationEventForgeryFactory.kt +++ b/packages/core/android/src/test/kotlin/com/datadog/tools/unit/forge/TelemetryConfigurationEventForgeryFactory.kt @@ -86,7 +86,6 @@ internal class TelemetryConfigurationEventForgeryFactory : ) } }, - defaultPrivacyLevel = forge.aNullable { aString() }, enablePrivacyForActionName = forge.aNullable { aBool() }, useExcludedActivityUrls = forge.aNullable { aBool() }, useWorkerUrl = forge.aNullable { aBool() }, diff --git a/packages/react-native-session-replay/src/SessionReplay.ts b/packages/react-native-session-replay/src/SessionReplay.ts index 60d4e8ff8..516b8ffb4 100644 --- a/packages/react-native-session-replay/src/SessionReplay.ts +++ b/packages/react-native-session-replay/src/SessionReplay.ts @@ -95,15 +95,6 @@ export interface SessionReplayConfiguration { * Default: `true`. */ startRecordingImmediately?: boolean; - - /** - * Defines the way sensitive content (e.g. text) should be masked. - * - * Default `SessionReplayPrivacy.MASK`. - * @deprecated Use {@link imagePrivacyLevel}, {@link touchPrivacyLevel} and {@link textAndInputPrivacyLevel} instead. - * Note: setting this property (`defaultPrivacyLevel`) will override the individual privacy levels. - */ - defaultPrivacyLevel?: SessionReplayPrivacy; } type InternalBaseSessionReplayConfiguration = { @@ -121,11 +112,8 @@ type InternalPrivacySessionReplayConfiguration = { type InternalSessionReplayConfiguration = InternalBaseSessionReplayConfiguration & InternalPrivacySessionReplayConfiguration; -const DEFAULTS: InternalSessionReplayConfiguration & { - defaultPrivacyLevel: SessionReplayPrivacy; -} = { +const DEFAULTS: InternalSessionReplayConfiguration = { replaySampleRate: 100, - defaultPrivacyLevel: SessionReplayPrivacy.MASK, customEndpoint: '', imagePrivacyLevel: ImagePrivacyLevel.MASK_ALL, touchPrivacyLevel: TouchPrivacyLevel.HIDE, @@ -175,33 +163,6 @@ export class SessionReplayWrapper { DEFAULTS.textAndInputPrivacyLevel }; - // Legacy Default Privacy Level property handling - if (configuration.defaultPrivacyLevel) { - switch (configuration.defaultPrivacyLevel) { - case SessionReplayPrivacy.MASK: - privacyConfig.imagePrivacyLevel = - ImagePrivacyLevel.MASK_ALL; - privacyConfig.touchPrivacyLevel = TouchPrivacyLevel.HIDE; - privacyConfig.textAndInputPrivacyLevel = - TextAndInputPrivacyLevel.MASK_ALL; - break; - case SessionReplayPrivacy.MASK_USER_INPUT: - privacyConfig.imagePrivacyLevel = - ImagePrivacyLevel.MASK_NONE; - privacyConfig.touchPrivacyLevel = TouchPrivacyLevel.HIDE; - privacyConfig.textAndInputPrivacyLevel = - TextAndInputPrivacyLevel.MASK_ALL_INPUTS; - break; - case SessionReplayPrivacy.ALLOW: - privacyConfig.imagePrivacyLevel = - ImagePrivacyLevel.MASK_NONE; - privacyConfig.touchPrivacyLevel = TouchPrivacyLevel.SHOW; - privacyConfig.textAndInputPrivacyLevel = - TextAndInputPrivacyLevel.MASK_SENSITIVE_INPUTS; - break; - } - } - return { ...baseConfig, ...privacyConfig }; }; diff --git a/packages/react-native-session-replay/src/__tests__/SessionReplay.test.ts b/packages/react-native-session-replay/src/__tests__/SessionReplay.test.ts index c755fc6e2..43f449cf7 100644 --- a/packages/react-native-session-replay/src/__tests__/SessionReplay.test.ts +++ b/packages/react-native-session-replay/src/__tests__/SessionReplay.test.ts @@ -9,7 +9,6 @@ import { NativeModules } from 'react-native'; import { ImagePrivacyLevel, SessionReplay, - SessionReplayPrivacy, TextAndInputPrivacyLevel, TouchPrivacyLevel } from '../SessionReplay'; @@ -41,27 +40,9 @@ describe('SessionReplay', () => { ); }); - it('calls native session replay with provided configuration { w defaultPrivacyLevel = ALLOW }', () => { + it('calls native session replay with provided configuration { w custom endpoint }', () => { SessionReplay.enable({ replaySampleRate: 100, - defaultPrivacyLevel: SessionReplayPrivacy.ALLOW, - customEndpoint: 'https://session-replay.example.com' - }); - - expect(NativeModules.DdSessionReplay.enable).toHaveBeenCalledWith( - 100, - 'https://session-replay.example.com', - 'MASK_NONE', - 'SHOW', - 'MASK_SENSITIVE_INPUTS', - true - ); - }); - - it('calls native session replay with provided configuration { w defaultPrivacyLevel = MASK }', () => { - SessionReplay.enable({ - replaySampleRate: 100, - defaultPrivacyLevel: SessionReplayPrivacy.MASK, customEndpoint: 'https://session-replay.example.com' }); @@ -75,23 +56,6 @@ describe('SessionReplay', () => { ); }); - it('calls native session replay with provided configuration { w defaultPrivacyLevel = MASK_USER_INPUT }', () => { - SessionReplay.enable({ - replaySampleRate: 100, - defaultPrivacyLevel: SessionReplayPrivacy.MASK_USER_INPUT, - customEndpoint: 'https://session-replay.example.com' - }); - - expect(NativeModules.DdSessionReplay.enable).toHaveBeenCalledWith( - 100, - 'https://session-replay.example.com', - 'MASK_NONE', - 'HIDE', - 'MASK_ALL_INPUTS', - true - ); - }); - it('calls native session replay with provided configuration { w random privacy levels }', () => { const TIMES = 20; From e862322196b352d5832a41e2a68f164adff91132 Mon Sep 17 00:00:00 2001 From: Sergio Barrio Date: Fri, 28 Nov 2025 11:49:29 +0100 Subject: [PATCH 097/410] Bump Native SDKs to 3.3.0 --- benchmarks/android/app/build.gradle | 2 +- benchmarks/ios/Podfile.lock | 74 +++++++------- example-new-architecture/ios/Podfile.lock | 70 +++++++------- example/ios/Podfile.lock | 96 +++++++++---------- packages/core/DatadogSDKReactNative.podspec | 12 +-- packages/core/android/build.gradle | 8 +- ...DatadogSDKReactNativeSessionReplay.podspec | 2 +- .../android/build.gradle | 4 +- .../DatadogSDKReactNativeWebView.podspec | 4 +- .../react-native-webview/android/build.gradle | 2 +- 10 files changed, 137 insertions(+), 137 deletions(-) diff --git a/benchmarks/android/app/build.gradle b/benchmarks/android/app/build.gradle index 04c240bd0..ba6ae7a53 100644 --- a/benchmarks/android/app/build.gradle +++ b/benchmarks/android/app/build.gradle @@ -129,5 +129,5 @@ dependencies { // Benchmark tools from dd-sdk-android are used for vitals recording // Remember to bump thid alongside the main dd-sdk-android dependencies - implementation("com.datadoghq:dd-sdk-android-benchmark-internal:3.1.0") + implementation("com.datadoghq:dd-sdk-android-benchmark-internal:3.3.0") } diff --git a/benchmarks/ios/Podfile.lock b/benchmarks/ios/Podfile.lock index 5c91024d5..4651f040a 100644 --- a/benchmarks/ios/Podfile.lock +++ b/benchmarks/ios/Podfile.lock @@ -1,22 +1,22 @@ PODS: - boost (1.84.0) - - DatadogCore (3.1.0): - - DatadogInternal (= 3.1.0) - - DatadogCrashReporting (3.1.0): - - DatadogInternal (= 3.1.0) + - DatadogCore (3.3.0): + - DatadogInternal (= 3.3.0) + - DatadogCrashReporting (3.3.0): + - DatadogInternal (= 3.3.0) - PLCrashReporter (~> 1.12.0) - - DatadogInternal (3.1.0) - - DatadogLogs (3.1.0): - - DatadogInternal (= 3.1.0) - - DatadogRUM (3.1.0): - - DatadogInternal (= 3.1.0) - - DatadogSDKReactNative (2.12.1): - - DatadogCore (= 3.1.0) - - DatadogCrashReporting (= 3.1.0) - - DatadogLogs (= 3.1.0) - - DatadogRUM (= 3.1.0) - - DatadogTrace (= 3.1.0) - - DatadogWebViewTracking (= 3.1.0) + - DatadogInternal (3.3.0) + - DatadogLogs (3.3.0): + - DatadogInternal (= 3.3.0) + - DatadogRUM (3.3.0): + - DatadogInternal (= 3.3.0) + - DatadogSDKReactNative (2.13.2): + - DatadogCore (= 3.3.0) + - DatadogCrashReporting (= 3.3.0) + - DatadogLogs (= 3.3.0) + - DatadogRUM (= 3.3.0) + - DatadogTrace (= 3.3.0) + - DatadogWebViewTracking (= 3.3.0) - DoubleConversion - glog - hermes-engine @@ -39,7 +39,7 @@ PODS: - Yoga - DatadogSDKReactNativeSessionReplay (2.13.2): - DatadogSDKReactNative - - DatadogSessionReplay (= 3.1.0) + - DatadogSessionReplay (= 3.3.0) - DoubleConversion - glog - hermes-engine @@ -60,10 +60,10 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - DatadogSDKReactNativeWebView (2.12.1): - - DatadogInternal (= 3.1.0) + - DatadogSDKReactNativeWebView (2.13.2): + - DatadogInternal (= 3.3.0) - DatadogSDKReactNative - - DatadogWebViewTracking (= 3.1.0) + - DatadogWebViewTracking (= 3.3.0) - DoubleConversion - glog - hermes-engine @@ -84,13 +84,13 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - DatadogSessionReplay (3.1.0): - - DatadogInternal (= 3.1.0) - - DatadogTrace (3.1.0): - - DatadogInternal (= 3.1.0) + - DatadogSessionReplay (3.3.0): + - DatadogInternal (= 3.3.0) + - DatadogTrace (3.3.0): + - DatadogInternal (= 3.3.0) - OpenTelemetrySwiftApi (= 1.13.1) - - DatadogWebViewTracking (3.1.0): - - DatadogInternal (= 3.1.0) + - DatadogWebViewTracking (3.3.0): + - DatadogInternal (= 3.3.0) - DoubleConversion (1.1.6) - fast_float (6.1.4) - FBLazyVector (0.78.2) @@ -2070,17 +2070,17 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: boost: 7e761d76ca2ce687f7cc98e698152abd03a18f90 - DatadogCore: d2f51c7fb4308cf3c25e55e2e7242e5d558ee71d - DatadogCrashReporting: f636f1d1c534572c0b0abcdc59df244c884d825d - DatadogInternal: 7837b2ce3d525d429682532eeda697b181299fdc - DatadogLogs: 250894b5a99da5b924a019049c0d0326823cdbd6 - DatadogRUM: 0d2a60e1abb8aacfb8827ef84f6d5deb4d5026c8 - DatadogSDKReactNative: 8e0f39de38621d4d7ed961a74d8a216fd3a38321 - DatadogSDKReactNativeSessionReplay: f9288c8e981dcc65d1f727b01421ee9a7601e75f - DatadogSDKReactNativeWebView: 993527f6c5d38e0fcc4804a6a60c334dd199dc5b - DatadogSessionReplay: 6bc71888e2b41dd0de3325f06f0c0b3cee0e6df4 - DatadogTrace: f59e933074cd285ad7e9f5af991f8fe04b095991 - DatadogWebViewTracking: 9bc92b4147aeed47eb1911451f651094aa6dd6c1 + DatadogCore: 9b1256ac9c27a07087d6214c8546acf756e40be7 + DatadogCrashReporting: 89a00886ef40808bffb8ccb4b6531e472f52e213 + DatadogInternal: 21dac5a7db548da6368a096d0714bdbec66deb6c + DatadogLogs: 355a4ac6bce3f0cb8231819e475c03dbbdd7957c + DatadogRUM: 1b3a47a9b9a5a25890f7fb3aa1f2bd86009d1086 + DatadogSDKReactNative: b364ddf133b4d774f3f7bfb5fc98232f960f5331 + DatadogSDKReactNativeSessionReplay: 73b5b7d46abe2ea8ffcaccb0c6232e49c0e27591 + DatadogSDKReactNativeWebView: 0310cc142fb39e185112e79f196f99f856a96c31 + DatadogSessionReplay: 85e63d3c5e5618c3029726d00595750a73c0920a + DatadogTrace: f13e8c09981787d6cb0a4b7fd1991351fab6d64b + DatadogWebViewTracking: 08fe084b5f57da05c1610fab49ce7bc84226141e DoubleConversion: cb417026b2400c8f53ae97020b2be961b59470cb fast_float: 06eeec4fe712a76acc9376682e4808b05ce978b6 FBLazyVector: e32d34492c519a2194ec9d7f5e7a79d11b73f91c diff --git a/example-new-architecture/ios/Podfile.lock b/example-new-architecture/ios/Podfile.lock index c0cd90bf6..38e172b0e 100644 --- a/example-new-architecture/ios/Podfile.lock +++ b/example-new-architecture/ios/Podfile.lock @@ -1,22 +1,22 @@ PODS: - boost (1.84.0) - - DatadogCore (3.1.0): - - DatadogInternal (= 3.1.0) - - DatadogCrashReporting (3.1.0): - - DatadogInternal (= 3.1.0) + - DatadogCore (3.3.0): + - DatadogInternal (= 3.3.0) + - DatadogCrashReporting (3.3.0): + - DatadogInternal (= 3.3.0) - PLCrashReporter (~> 1.12.0) - - DatadogInternal (3.1.0) - - DatadogLogs (3.1.0): - - DatadogInternal (= 3.1.0) - - DatadogRUM (3.1.0): - - DatadogInternal (= 3.1.0) - - DatadogSDKReactNative (2.12.1): - - DatadogCore (= 3.1.0) - - DatadogCrashReporting (= 3.1.0) - - DatadogLogs (= 3.1.0) - - DatadogRUM (= 3.1.0) - - DatadogTrace (= 3.1.0) - - DatadogWebViewTracking (= 3.1.0) + - DatadogInternal (3.3.0) + - DatadogLogs (3.3.0): + - DatadogInternal (= 3.3.0) + - DatadogRUM (3.3.0): + - DatadogInternal (= 3.3.0) + - DatadogSDKReactNative (2.13.2): + - DatadogCore (= 3.3.0) + - DatadogCrashReporting (= 3.3.0) + - DatadogLogs (= 3.3.0) + - DatadogRUM (= 3.3.0) + - DatadogTrace (= 3.3.0) + - DatadogWebViewTracking (= 3.3.0) - DoubleConversion - glog - hermes-engine @@ -37,13 +37,13 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - DatadogSDKReactNative/Tests (2.12.1): - - DatadogCore (= 3.1.0) - - DatadogCrashReporting (= 3.1.0) - - DatadogLogs (= 3.1.0) - - DatadogRUM (= 3.1.0) - - DatadogTrace (= 3.1.0) - - DatadogWebViewTracking (= 3.1.0) + - DatadogSDKReactNative/Tests (2.13.2): + - DatadogCore (= 3.3.0) + - DatadogCrashReporting (= 3.3.0) + - DatadogLogs (= 3.3.0) + - DatadogRUM (= 3.3.0) + - DatadogTrace (= 3.3.0) + - DatadogWebViewTracking (= 3.3.0) - DoubleConversion - glog - hermes-engine @@ -64,11 +64,11 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - DatadogTrace (3.1.0): - - DatadogInternal (= 3.1.0) + - DatadogTrace (3.3.0): + - DatadogInternal (= 3.3.0) - OpenTelemetrySwiftApi (= 1.13.1) - - DatadogWebViewTracking (3.1.0): - - DatadogInternal (= 3.1.0) + - DatadogWebViewTracking (3.3.0): + - DatadogInternal (= 3.3.0) - DoubleConversion (1.1.6) - fast_float (6.1.4) - FBLazyVector (0.76.9) @@ -1850,14 +1850,14 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: boost: 1dca942403ed9342f98334bf4c3621f011aa7946 - DatadogCore: d2f51c7fb4308cf3c25e55e2e7242e5d558ee71d - DatadogCrashReporting: f636f1d1c534572c0b0abcdc59df244c884d825d - DatadogInternal: 7837b2ce3d525d429682532eeda697b181299fdc - DatadogLogs: 250894b5a99da5b924a019049c0d0326823cdbd6 - DatadogRUM: 0d2a60e1abb8aacfb8827ef84f6d5deb4d5026c8 - DatadogSDKReactNative: 069ea9876220b2d09b0f4b180ce571b1b6ecbb35 - DatadogTrace: f59e933074cd285ad7e9f5af991f8fe04b095991 - DatadogWebViewTracking: 9bc92b4147aeed47eb1911451f651094aa6dd6c1 + DatadogCore: 9b1256ac9c27a07087d6214c8546acf756e40be7 + DatadogCrashReporting: 89a00886ef40808bffb8ccb4b6531e472f52e213 + DatadogInternal: 21dac5a7db548da6368a096d0714bdbec66deb6c + DatadogLogs: 355a4ac6bce3f0cb8231819e475c03dbbdd7957c + DatadogRUM: 1b3a47a9b9a5a25890f7fb3aa1f2bd86009d1086 + DatadogSDKReactNative: 3c756b98ff379907842eb3769d44e6b8b570385f + DatadogTrace: f13e8c09981787d6cb0a4b7fd1991351fab6d64b + DatadogWebViewTracking: 08fe084b5f57da05c1610fab49ce7bc84226141e DoubleConversion: f16ae600a246532c4020132d54af21d0ddb2a385 fast_float: 06eeec4fe712a76acc9376682e4808b05ce978b6 FBLazyVector: 7605ea4810e0e10ae4815292433c09bf4324ba45 diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 8838c9be5..2e3655fd9 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -1,34 +1,34 @@ PODS: - boost (1.84.0) - - DatadogCore (3.1.0): - - DatadogInternal (= 3.1.0) - - DatadogCrashReporting (3.1.0): - - DatadogInternal (= 3.1.0) + - DatadogCore (3.3.0): + - DatadogInternal (= 3.3.0) + - DatadogCrashReporting (3.3.0): + - DatadogInternal (= 3.3.0) - PLCrashReporter (~> 1.12.0) - - DatadogInternal (3.1.0) - - DatadogLogs (3.1.0): - - DatadogInternal (= 3.1.0) - - DatadogRUM (3.1.0): - - DatadogInternal (= 3.1.0) - - DatadogSDKReactNative (2.12.1): - - DatadogCore (= 3.1.0) - - DatadogCrashReporting (= 3.1.0) - - DatadogLogs (= 3.1.0) - - DatadogRUM (= 3.1.0) - - DatadogTrace (= 3.1.0) - - DatadogWebViewTracking (= 3.1.0) + - DatadogInternal (3.3.0) + - DatadogLogs (3.3.0): + - DatadogInternal (= 3.3.0) + - DatadogRUM (3.3.0): + - DatadogInternal (= 3.3.0) + - DatadogSDKReactNative (2.13.2): + - DatadogCore (= 3.3.0) + - DatadogCrashReporting (= 3.3.0) + - DatadogLogs (= 3.3.0) + - DatadogRUM (= 3.3.0) + - DatadogTrace (= 3.3.0) + - DatadogWebViewTracking (= 3.3.0) - React-Core - - DatadogSDKReactNative/Tests (2.12.1): - - DatadogCore (= 3.1.0) - - DatadogCrashReporting (= 3.1.0) - - DatadogLogs (= 3.1.0) - - DatadogRUM (= 3.1.0) - - DatadogTrace (= 3.1.0) - - DatadogWebViewTracking (= 3.1.0) + - DatadogSDKReactNative/Tests (2.13.2): + - DatadogCore (= 3.3.0) + - DatadogCrashReporting (= 3.3.0) + - DatadogLogs (= 3.3.0) + - DatadogRUM (= 3.3.0) + - DatadogTrace (= 3.3.0) + - DatadogWebViewTracking (= 3.3.0) - React-Core - DatadogSDKReactNativeSessionReplay (2.13.2): - DatadogSDKReactNative - - DatadogSessionReplay (= 3.1.0) + - DatadogSessionReplay (= 3.3.0) - DoubleConversion - glog - hermes-engine @@ -51,7 +51,7 @@ PODS: - Yoga - DatadogSDKReactNativeSessionReplay/Tests (2.13.2): - DatadogSDKReactNative - - DatadogSessionReplay (= 3.1.0) + - DatadogSessionReplay (= 3.3.0) - DoubleConversion - glog - hermes-engine @@ -73,25 +73,25 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - DatadogSDKReactNativeWebView (2.12.1): - - DatadogInternal (= 3.1.0) + - DatadogSDKReactNativeWebView (2.13.2): + - DatadogInternal (= 3.3.0) - DatadogSDKReactNative - - DatadogWebViewTracking (= 3.1.0) + - DatadogWebViewTracking (= 3.3.0) - React-Core - - DatadogSDKReactNativeWebView/Tests (2.12.1): - - DatadogInternal (= 3.1.0) + - DatadogSDKReactNativeWebView/Tests (2.13.2): + - DatadogInternal (= 3.3.0) - DatadogSDKReactNative - - DatadogWebViewTracking (= 3.1.0) + - DatadogWebViewTracking (= 3.3.0) - React-Core - react-native-webview - React-RCTText - - DatadogSessionReplay (3.1.0): - - DatadogInternal (= 3.1.0) - - DatadogTrace (3.1.0): - - DatadogInternal (= 3.1.0) + - DatadogSessionReplay (3.3.0): + - DatadogInternal (= 3.3.0) + - DatadogTrace (3.3.0): + - DatadogInternal (= 3.3.0) - OpenTelemetrySwiftApi (= 1.13.1) - - DatadogWebViewTracking (3.1.0): - - DatadogInternal (= 3.1.0) + - DatadogWebViewTracking (3.3.0): + - DatadogInternal (= 3.3.0) - DoubleConversion (1.1.6) - fast_float (6.1.4) - FBLazyVector (0.76.9) @@ -1988,17 +1988,17 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: boost: 1dca942403ed9342f98334bf4c3621f011aa7946 - DatadogCore: d2f51c7fb4308cf3c25e55e2e7242e5d558ee71d - DatadogCrashReporting: f636f1d1c534572c0b0abcdc59df244c884d825d - DatadogInternal: 7837b2ce3d525d429682532eeda697b181299fdc - DatadogLogs: 250894b5a99da5b924a019049c0d0326823cdbd6 - DatadogRUM: 0d2a60e1abb8aacfb8827ef84f6d5deb4d5026c8 - DatadogSDKReactNative: af351a4e1ce08124c290c52de94b0062a166cc67 - DatadogSDKReactNativeSessionReplay: dcbd55d9d0f2b86026996a8b7ec9654922d5dfe1 - DatadogSDKReactNativeWebView: 096ac87eb753b6a217b93441983264b9837c3b7e - DatadogSessionReplay: 6bc71888e2b41dd0de3325f06f0c0b3cee0e6df4 - DatadogTrace: f59e933074cd285ad7e9f5af991f8fe04b095991 - DatadogWebViewTracking: 9bc92b4147aeed47eb1911451f651094aa6dd6c1 + DatadogCore: 9b1256ac9c27a07087d6214c8546acf756e40be7 + DatadogCrashReporting: 89a00886ef40808bffb8ccb4b6531e472f52e213 + DatadogInternal: 21dac5a7db548da6368a096d0714bdbec66deb6c + DatadogLogs: 355a4ac6bce3f0cb8231819e475c03dbbdd7957c + DatadogRUM: 1b3a47a9b9a5a25890f7fb3aa1f2bd86009d1086 + DatadogSDKReactNative: 96c64d4627096497594113ffb0c86ae72490b17c + DatadogSDKReactNativeSessionReplay: 02ea3eefd261341d2ae839882351be3d209376d0 + DatadogSDKReactNativeWebView: 83cd1a58da38a7a4bd554051d6742138e36e3589 + DatadogSessionReplay: 85e63d3c5e5618c3029726d00595750a73c0920a + DatadogTrace: f13e8c09981787d6cb0a4b7fd1991351fab6d64b + DatadogWebViewTracking: 08fe084b5f57da05c1610fab49ce7bc84226141e DoubleConversion: f16ae600a246532c4020132d54af21d0ddb2a385 fast_float: 06eeec4fe712a76acc9376682e4808b05ce978b6 FBLazyVector: 7605ea4810e0e10ae4815292433c09bf4324ba45 diff --git a/packages/core/DatadogSDKReactNative.podspec b/packages/core/DatadogSDKReactNative.podspec index c0d235304..692dd23c8 100644 --- a/packages/core/DatadogSDKReactNative.podspec +++ b/packages/core/DatadogSDKReactNative.podspec @@ -19,14 +19,14 @@ Pod::Spec.new do |s| s.dependency "React-Core" # /!\ Remember to keep the versions in sync with DatadogSDKReactNativeSessionReplay.podspec - s.dependency 'DatadogCore', '3.1.0' - s.dependency 'DatadogLogs', '3.1.0' - s.dependency 'DatadogTrace', '3.1.0' - s.dependency 'DatadogRUM', '3.1.0' - s.dependency 'DatadogCrashReporting', '3.1.0' + s.dependency 'DatadogCore', '3.3.0' + s.dependency 'DatadogLogs', '3.3.0' + s.dependency 'DatadogTrace', '3.3.0' + s.dependency 'DatadogRUM', '3.3.0' + s.dependency 'DatadogCrashReporting', '3.3.0' # DatadogWebViewTracking is not available for tvOS - s.ios.dependency 'DatadogWebViewTracking', '3.1.0' + s.ios.dependency 'DatadogWebViewTracking', '3.3.0' s.test_spec 'Tests' do |test_spec| test_spec.source_files = 'ios/Tests/**/*.{swift,json}' diff --git a/packages/core/android/build.gradle b/packages/core/android/build.gradle index 1344b2531..bf7a2ef9c 100644 --- a/packages/core/android/build.gradle +++ b/packages/core/android/build.gradle @@ -195,10 +195,10 @@ dependencies { } implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" compileOnly "com.squareup.okhttp3:okhttp:3.12.13" - implementation "com.datadoghq:dd-sdk-android-rum:3.2.0" - implementation "com.datadoghq:dd-sdk-android-logs:3.2.0" - implementation "com.datadoghq:dd-sdk-android-trace:3.2.0" - implementation "com.datadoghq:dd-sdk-android-webview:3.2.0" + implementation "com.datadoghq:dd-sdk-android-rum:3.3.0" + implementation "com.datadoghq:dd-sdk-android-logs:3.3.0" + implementation "com.datadoghq:dd-sdk-android-trace:3.3.0" + implementation "com.datadoghq:dd-sdk-android-webview:3.3.0" implementation "com.google.code.gson:gson:2.10.0" testImplementation "org.junit.platform:junit-platform-launcher:1.6.2" testImplementation "org.junit.jupiter:junit-jupiter-api:5.6.2" diff --git a/packages/react-native-session-replay/DatadogSDKReactNativeSessionReplay.podspec b/packages/react-native-session-replay/DatadogSDKReactNativeSessionReplay.podspec index 6a5d0b78f..e0b032a9e 100644 --- a/packages/react-native-session-replay/DatadogSDKReactNativeSessionReplay.podspec +++ b/packages/react-native-session-replay/DatadogSDKReactNativeSessionReplay.podspec @@ -23,7 +23,7 @@ Pod::Spec.new do |s| s.dependency "React-Core" # /!\ Remember to keep the version in sync with DatadogSDKReactNative.podspec - s.dependency 'DatadogSessionReplay', '3.1.0' + s.dependency 'DatadogSessionReplay', '3.3.0' s.dependency 'DatadogSDKReactNative' s.test_spec 'Tests' do |test_spec| diff --git a/packages/react-native-session-replay/android/build.gradle b/packages/react-native-session-replay/android/build.gradle index a0d77f2ff..1b16713db 100644 --- a/packages/react-native-session-replay/android/build.gradle +++ b/packages/react-native-session-replay/android/build.gradle @@ -216,8 +216,8 @@ dependencies { api "com.facebook.react:react-android:$reactNativeVersion" } implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" - implementation "com.datadoghq:dd-sdk-android-session-replay:3.1.0" - implementation "com.datadoghq:dd-sdk-android-internal:3.1.0" + implementation "com.datadoghq:dd-sdk-android-session-replay:3.3.0" + implementation "com.datadoghq:dd-sdk-android-internal:3.3.0" implementation project(path: ':datadog_mobile-react-native') testImplementation "org.junit.platform:junit-platform-launcher:1.6.2" diff --git a/packages/react-native-webview/DatadogSDKReactNativeWebView.podspec b/packages/react-native-webview/DatadogSDKReactNativeWebView.podspec index 080a853d8..000b35477 100644 --- a/packages/react-native-webview/DatadogSDKReactNativeWebView.podspec +++ b/packages/react-native-webview/DatadogSDKReactNativeWebView.podspec @@ -23,8 +23,8 @@ Pod::Spec.new do |s| end # /!\ Remember to keep the version in sync with DatadogSDKReactNative.podspec - s.dependency 'DatadogWebViewTracking', '3.1.0' - s.dependency 'DatadogInternal', '3.1.0' + s.dependency 'DatadogWebViewTracking', '3.3.0' + s.dependency 'DatadogInternal', '3.3.0' s.dependency 'DatadogSDKReactNative' s.test_spec 'Tests' do |test_spec| diff --git a/packages/react-native-webview/android/build.gradle b/packages/react-native-webview/android/build.gradle index 87ca1b7e7..3ff3048d0 100644 --- a/packages/react-native-webview/android/build.gradle +++ b/packages/react-native-webview/android/build.gradle @@ -190,7 +190,7 @@ dependencies { implementation "com.facebook.react:react-android:$reactNativeVersion" } - implementation "com.datadoghq:dd-sdk-android-webview:3.1.0" + implementation "com.datadoghq:dd-sdk-android-webview:3.3.0" implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation project(path: ':datadog_mobile-react-native') From 3d42b9b7ba1668f13d179c07436cb82d04456a5e Mon Sep 17 00:00:00 2001 From: Sergio Barrio Date: Fri, 28 Nov 2025 11:49:57 +0100 Subject: [PATCH 098/410] Solve merge issues and fix failing native tests --- .../reactnative/DdSdkImplementation.kt | 34 +------- .../Sources/DdSdkNativeInitialization.swift | 6 -- packages/core/ios/Tests/DdSdkTests.swift | 81 ++++++------------- .../Sources/RCTDatadogWebViewTracking.swift | 4 +- 4 files changed, 31 insertions(+), 94 deletions(-) diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt index 02123ee8f..8cb165b5f 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt @@ -351,6 +351,7 @@ class DdSdkImplementation( } } + /** * Normalizes frameTime values so when are turned into FPS metrics they are normalized on a range of zero to 60fps. * @param frameTimeSeconds: the frame time to normalize. In seconds. @@ -368,43 +369,16 @@ class DdSdkImplementation( val frameTimeMs = frameTimeSeconds * 1000.0 val frameBudgetHz = fpsBudget ?: DEFAULT_REFRESH_HZ val maxDeviceDisplayHz = deviceDisplayFps ?: getMaxDisplayRefreshRate(context) - ?: 60.0 - - val maxDeviceFrameTimeMs = 1000.0 / maxDeviceDisplayHz - val budgetFrameTimeMs = 1000.0 / frameBudgetHz - - if (listOf( - maxDeviceDisplayHz, frameTimeMs, frameBudgetHz, budgetFrameTimeMs, maxDeviceFrameTimeMs - ).any { !it.isFinite() || it <= 0.0 } - ) return 1.0 / DEFAULT_REFRESH_HZ - - - var normalizedFrameTimeMs = frameTimeMs / (maxDeviceFrameTimeMs / budgetFrameTimeMs) - - normalizedFrameTimeMs = max(normalizedFrameTimeMs, maxDeviceFrameTimeMs) - - return normalizedFrameTimeMs / 1000.0 // in seconds - } - - @Suppress("CyclomaticComplexMethod") - private fun getMaxDisplayRefreshRate(context: Context?): Double { - val dm = context?.getSystemService(Context.DISPLAY_SERVICE) as? DisplayManager ?: return 60.0 - val display: Display = dm.getDisplay(Display.DEFAULT_DISPLAY) ?: return DEFAULT_REFRESH_HZ - - return display.supportedModes.maxOf { it.refreshRate.toDouble() } - } - - // endregion + ?: 60.0 val maxDeviceFrameTimeMs = 1000.0 / maxDeviceDisplayHz val budgetFrameTimeMs = 1000.0 / frameBudgetHz if (listOf( - maxDeviceDisplayHz, frameTimeMs, frameBudgetHz, budgetFrameTimeMs, maxDeviceFrameTimeMs - ).any { !it.isFinite() || it <= 0.0 } + maxDeviceDisplayHz, frameTimeMs, frameBudgetHz, budgetFrameTimeMs, maxDeviceFrameTimeMs + ).any { !it.isFinite() || it <= 0.0 } ) return 1.0 / DEFAULT_REFRESH_HZ - var normalizedFrameTimeMs = frameTimeMs / (maxDeviceFrameTimeMs / budgetFrameTimeMs) normalizedFrameTimeMs = max(normalizedFrameTimeMs, maxDeviceFrameTimeMs) diff --git a/packages/core/ios/Sources/DdSdkNativeInitialization.swift b/packages/core/ios/Sources/DdSdkNativeInitialization.swift index 969f72abf..fc11cd80d 100644 --- a/packages/core/ios/Sources/DdSdkNativeInitialization.swift +++ b/packages/core/ios/Sources/DdSdkNativeInitialization.swift @@ -93,16 +93,10 @@ public class DdSdkNativeInitialization: NSObject { if sdkConfiguration.nativeCrashReportEnabled ?? false { CrashReporting.enable() } -<<<<<<< HEAD -<<<<<<< HEAD #if os(iOS) DatadogSDKWrapper.shared.enableWebviewTracking() #endif -======= ->>>>>>> 0443e0ff (iOS: Always use SDK default core instance) -======= ->>>>>>> 93aa6125 (iOS: Always use SDK default core instance) } func buildSDKConfiguration(configuration: DdSdkConfiguration, defaultAppVersion: String = getDefaultAppVersion()) -> Datadog.Configuration { diff --git a/packages/core/ios/Tests/DdSdkTests.swift b/packages/core/ios/Tests/DdSdkTests.swift index cba49f84f..3f5c0e960 100644 --- a/packages/core/ios/Tests/DdSdkTests.swift +++ b/packages/core/ios/Tests/DdSdkTests.swift @@ -82,7 +82,7 @@ class DdSdkTests: XCTestCase { func testResolvesPromiseAfterInitializationIsDone() throws { let bridge = DispatchQueueMock() let mockJSRefreshRateMonitor = MockJSRefreshRateMonitor() - let mockListener = MockOnCoreInitializedListener() + let mockListener = MockOnSdkInitializedListener() DatadogSDKWrapper.shared.addOnSdkInitializedListener(listener: mockListener.listener) let expectation = self.expectation(description: "Listener is called when promise resolves") @@ -275,7 +275,9 @@ class DdSdkTests: XCTestCase { func testSDKInitializationWithOnInitializedCallback() { var isInitialized = false + var coreFromCallback: DatadogCoreProtocol? = nil DatadogSDKWrapper.shared.addOnSdkInitializedListener(listener: { + core in coreFromCallback = core isInitialized = Datadog.isInitialized() }) @@ -718,13 +720,14 @@ class DdSdkTests: XCTestCase { XCTAssertEqual(userInfo.extraInfo["extra-info-3"] as? Bool, nil) XCTAssertEqual(userInfo.extraInfo["extra-info-4"] as? [String: Int], nil) } - - func testClearUserInfo() throws { + + func testAddingAttribute() { + let rumMonitorMock = MockRUMMonitor() let bridge = DdSdkImplementation( mainDispatchQueue: DispatchQueueMock(), jsDispatchQueue: DispatchQueueMock(), jsRefreshRateMonitor: JSRefreshRateMonitor(), - RUMMonitorProvider: { MockRUMMonitor() }, + RUMMonitorProvider: { rumMonitorMock }, RUMMonitorInternalProvider: { nil } ) bridge.initialize( @@ -733,57 +736,19 @@ class DdSdkTests: XCTestCase { reject: mockReject ) - bridge.setUserInfo( - userInfo: NSDictionary( - dictionary: [ - "id": "id_123", - "name": "John Doe", - "email": "john@doe.com", - "extraInfo": [ - "extra-info-1": 123, - "extra-info-2": "abc", - "extra-info-3": true, - "extra-info-4": [ - "nested-extra-info-1": 456 - ], - ], - ] - ), - resolve: mockResolve, - reject: mockReject - ) - - var ddContext = try XCTUnwrap(CoreRegistry.default as? DatadogCore).contextProvider.read() - var userInfo = try XCTUnwrap(ddContext.userInfo) - - XCTAssertEqual(userInfo.id, "id_123") - XCTAssertEqual(userInfo.name, "John Doe") - XCTAssertEqual(userInfo.email, "john@doe.com") - XCTAssertEqual(userInfo.extraInfo["extra-info-1"] as? Int64, 123) - XCTAssertEqual(userInfo.extraInfo["extra-info-2"] as? String, "abc") - XCTAssertEqual(userInfo.extraInfo["extra-info-3"] as? Bool, true) - - if let extraInfo4Encodable = userInfo.extraInfo["extra-info-4"] - as? DatadogSDKReactNative.AnyEncodable, - let extraInfo4Dict = extraInfo4Encodable.value as? [String: Int] - { - XCTAssertEqual(extraInfo4Dict, ["nested-extra-info-1": 456]) - } else { - XCTFail("extra-info-4 is not of expected type or value") - } - - bridge.clearUserInfo(resolve: mockResolve, reject: mockReject) + bridge.addAttribute(key: "attribute-1", value: NSDictionary(dictionary: ["value": 123]), resolve: mockResolve, reject: mockReject) + bridge.addAttribute(key: "attribute-2", value: NSDictionary(dictionary: ["value": "abc"]), resolve: mockResolve, reject: mockReject) + bridge.addAttribute(key: "attribute-3", value: NSDictionary(dictionary: ["value": true]), resolve: mockResolve, reject: mockReject) + + XCTAssertEqual(rumMonitorMock.addedAttributes["attribute-1"] as? Int64, 123) + XCTAssertEqual(rumMonitorMock.addedAttributes["attribute-2"] as? String, "abc") + XCTAssertEqual(rumMonitorMock.addedAttributes["attribute-3"] as? Bool, true) - ddContext = try XCTUnwrap(CoreRegistry.default as? DatadogCore).contextProvider.read() - userInfo = try XCTUnwrap(ddContext.userInfo) + XCTAssertEqual(GlobalState.globalAttributes["attribute-1"] as? Int64, 123) + XCTAssertEqual(GlobalState.globalAttributes["attribute-2"] as? String, "abc") + XCTAssertEqual(GlobalState.globalAttributes["attribute-3"] as? Bool, true) - XCTAssertEqual(userInfo.id, nil) - XCTAssertEqual(userInfo.name, nil) - XCTAssertEqual(userInfo.email, nil) - XCTAssertEqual(userInfo.extraInfo["extra-info-1"] as? Int64, nil) - XCTAssertEqual(userInfo.extraInfo["extra-info-2"] as? String, nil) - XCTAssertEqual(userInfo.extraInfo["extra-info-3"] as? Bool, nil) - XCTAssertEqual(userInfo.extraInfo["extra-info-4"] as? [String: Int], nil) + GlobalState.globalAttributes.removeAll() } func testRemovingAttribute() { @@ -1542,7 +1507,7 @@ class DdSdkTests: XCTestCase { func testCallsOnSdkInitializedListeners() throws { let bridge = DispatchQueueMock() let mockJSRefreshRateMonitor = MockJSRefreshRateMonitor() - let mockListener = MockOnCoreInitializedListener() + let mockListener = MockOnSdkInitializedListener() DatadogSDKWrapper.shared.addOnSdkInitializedListener(listener: mockListener.listener) @@ -1811,9 +1776,13 @@ extension DdSdkImplementation { } } -class MockOnCoreInitializedListener { +class MockOnSdkInitializedListener { var called = false - func listener() { + var receivedCore: DatadogCoreProtocol? + + lazy var listener: OnSdkInitializedListener = { core in self.called = true + self.receivedCore = core } } + diff --git a/packages/react-native-webview/ios/Sources/RCTDatadogWebViewTracking.swift b/packages/react-native-webview/ios/Sources/RCTDatadogWebViewTracking.swift index 45f11e452..6d3bc3f8d 100644 --- a/packages/react-native-webview/ios/Sources/RCTDatadogWebViewTracking.swift +++ b/packages/react-native-webview/ios/Sources/RCTDatadogWebViewTracking.swift @@ -17,14 +17,14 @@ import DatadogInternal public override init() { super.init() - self.onSdkInitializedListener = { [weak self] in + self.onSdkInitializedListener = { [weak self] (core: DatadogCoreProtocol) in guard let strongSelf = self, let webView = strongSelf.webView else { return } strongSelf.enableWebViewTracking( webView: webView, allowedHosts: strongSelf.allowedHosts, - core: CoreRegistry.default + core: core ) } } From 916943c499dd56318e8d4d6f5ad6771e9396043c Mon Sep 17 00:00:00 2001 From: Sergio Barrio Date: Wed, 3 Dec 2025 17:38:46 +0100 Subject: [PATCH 099/410] Adapt internal testing tools package to changes done for v3 --- .../ios/Sources/DatadogCoreProxy.swift | 2 +- .../Sources/DdInternalTestingImplementation.swift | 12 ++---------- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/packages/internal-testing-tools/ios/Sources/DatadogCoreProxy.swift b/packages/internal-testing-tools/ios/Sources/DatadogCoreProxy.swift index 9f7c3db2c..0d7e1dec1 100644 --- a/packages/internal-testing-tools/ios/Sources/DatadogCoreProxy.swift +++ b/packages/internal-testing-tools/ios/Sources/DatadogCoreProxy.swift @@ -96,7 +96,7 @@ private final class FeatureScopeInterceptor: @unchecked Sendable { let actualWriter: Writer unowned var interception: FeatureScopeInterceptor? - func write(value: T, metadata: M) { + func write(value: T, metadata: M?, completion: @escaping DatadogInternal.CompletionHandler) where T : Encodable, M : Encodable { group.enter() defer { group.leave() } diff --git a/packages/internal-testing-tools/ios/Sources/DdInternalTestingImplementation.swift b/packages/internal-testing-tools/ios/Sources/DdInternalTestingImplementation.swift index 7b1c2862d..b460b86af 100644 --- a/packages/internal-testing-tools/ios/Sources/DdInternalTestingImplementation.swift +++ b/packages/internal-testing-tools/ios/Sources/DdInternalTestingImplementation.swift @@ -14,7 +14,7 @@ import DatadogInternal public class DdInternalTestingImplementation: NSObject { @objc public func clearData(resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { - let coreProxy = (DatadogSDKWrapper.shared.getCoreInstance() as! DatadogCoreProxy) + let coreProxy = CoreRegistry.default as! DatadogCoreProxy coreProxy.waitAndDeleteEvents(ofFeature: "rum") coreProxy.waitAndDeleteEvents(ofFeature: "logging") coreProxy.waitAndDeleteEvents(ofFeature: "tracing") @@ -26,7 +26,7 @@ public class DdInternalTestingImplementation: NSObject { @objc public func getAllEvents(feature: String, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { do { - let coreProxy = (DatadogSDKWrapper.shared.getCoreInstance() as! DatadogCoreProxy) + let coreProxy = CoreRegistry.default as! DatadogCoreProxy let events = coreProxy.waitAndReturnEventsData(ofFeature: feature) let data = try JSONSerialization.data(withJSONObject: events, options: .prettyPrinted) resolve(String(data: data, encoding: String.Encoding.utf8) ?? "") @@ -39,10 +39,6 @@ public class DdInternalTestingImplementation: NSObject { @objc public func enable(resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { - DatadogSDKWrapper.shared.addOnCoreInitializedListener(listener: {core in - let proxiedCore = DatadogCoreProxy(core: core) - DatadogSDKWrapper.shared.setCoreInstance(core: proxiedCore) - }) resolve(nil) } } @@ -51,9 +47,5 @@ public class DdInternalTestingImplementation: NSObject { public class DdInternalTestingNativeInitialization: NSObject { @objc public func enableFromNative() -> Void { - DatadogSDKWrapper.shared.addOnCoreInitializedListener(listener: {core in - let proxiedCore = DatadogCoreProxy(core: core) - DatadogSDKWrapper.shared.setCoreInstance(core: proxiedCore) - }) } } From bdfcf3277bcc68b0939fe7e43677956604834636 Mon Sep 17 00:00:00 2001 From: Sergio Barrio Date: Thu, 4 Dec 2025 12:18:45 +0100 Subject: [PATCH 100/410] Fixed ProxiedCore implementation --- .../ios/Sources/DdInternalTestingImplementation.swift | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/internal-testing-tools/ios/Sources/DdInternalTestingImplementation.swift b/packages/internal-testing-tools/ios/Sources/DdInternalTestingImplementation.swift index b460b86af..41a213efd 100644 --- a/packages/internal-testing-tools/ios/Sources/DdInternalTestingImplementation.swift +++ b/packages/internal-testing-tools/ios/Sources/DdInternalTestingImplementation.swift @@ -39,6 +39,11 @@ public class DdInternalTestingImplementation: NSObject { @objc public func enable(resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { + DatadogSDKWrapper.shared.addOnSdkInitializedListener(listener: {core in + let proxiedCore = DatadogCoreProxy(core: core) + CoreRegistry.unregisterDefault() + CoreRegistry.register(default: proxiedCore) + }) resolve(nil) } } @@ -47,5 +52,10 @@ public class DdInternalTestingImplementation: NSObject { public class DdInternalTestingNativeInitialization: NSObject { @objc public func enableFromNative() -> Void { + DatadogSDKWrapper.shared.addOnSdkInitializedListener(listener: {core in + let proxiedCore = DatadogCoreProxy(core: core) + CoreRegistry.unregisterDefault() + CoreRegistry.register(default: proxiedCore) + }) } } From 29420c994f0ee9490a1ba8f2e0825c0f688afe60 Mon Sep 17 00:00:00 2001 From: Sergio Barrio Date: Thu, 4 Dec 2025 16:49:11 +0100 Subject: [PATCH 101/410] Remove unnecessary isInitialized check on DdLogsImplementation --- packages/core/ios/Sources/Attributes.swift | 7 ---- .../ios/Sources/DdLogsImplementation.swift | 39 +------------------ 2 files changed, 2 insertions(+), 44 deletions(-) diff --git a/packages/core/ios/Sources/Attributes.swift b/packages/core/ios/Sources/Attributes.swift index adf9638f5..25dd8df20 100644 --- a/packages/core/ios/Sources/Attributes.swift +++ b/packages/core/ios/Sources/Attributes.swift @@ -109,10 +109,3 @@ internal struct InternalConfigurationAttributes { /// Expects `Bool` value. static let dropAction = "_dd.action.drop_action" } - -/// Error messages that can be thrown to the JS SDK -internal struct Errors { - /// Error thrown when a log was sent before the SDK was initialized. - /// Not sending the log prevent the logger to be set to a Noop logger. - static let logSentBeforeSDKInit = "DD_INTERNAL_LOG_SENT_BEFORE_SDK_INIT" -} diff --git a/packages/core/ios/Sources/DdLogsImplementation.swift b/packages/core/ios/Sources/DdLogsImplementation.swift index fe3fde092..7c428af45 100644 --- a/packages/core/ios/Sources/DdLogsImplementation.swift +++ b/packages/core/ios/Sources/DdLogsImplementation.swift @@ -13,27 +13,20 @@ import DatadogCore public class DdLogsImplementation: NSObject { private lazy var logger: LoggerProtocol = loggerProvider() private let loggerProvider: () -> LoggerProtocol - private let isSDKInitialized: () -> Bool - internal init(_ loggerProvider: @escaping () -> LoggerProtocol, _ isSDKInitialized: @escaping () -> Bool) { + internal init(_ loggerProvider: @escaping () -> LoggerProtocol) { self.loggerProvider = loggerProvider - self.isSDKInitialized = isSDKInitialized } @objc public override convenience init() { self.init( - { DatadogLogs.Logger.create(with: DatadogSDKWrapper.shared.loggerConfiguration) }, - { Datadog.isInitialized() } + { DatadogLogs.Logger.create(with: DatadogSDKWrapper.shared.loggerConfiguration) } ) } @objc public func debug(message: String, context: NSDictionary, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { - if (!self.isSDKInitialized()) { - reject(nil, Errors.logSentBeforeSDKInit, nil) - return - } let attributes = castAttributesToSwift(context).mergeWithGlobalAttributes() logger.debug(message, error: nil, attributes: attributes) resolve(nil) @@ -41,10 +34,6 @@ public class DdLogsImplementation: NSObject { @objc public func info(message: String, context: NSDictionary, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { - if (!self.isSDKInitialized()) { - reject(nil, Errors.logSentBeforeSDKInit, nil) - return - } let attributes = castAttributesToSwift(context).mergeWithGlobalAttributes() logger.info(message, error: nil, attributes: attributes) resolve(nil) @@ -52,10 +41,6 @@ public class DdLogsImplementation: NSObject { @objc public func warn(message: String, context: NSDictionary, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { - if (!self.isSDKInitialized()) { - reject(nil, Errors.logSentBeforeSDKInit, nil) - return - } let attributes = castAttributesToSwift(context).mergeWithGlobalAttributes() logger.warn(message, error: nil, attributes: attributes) resolve(nil) @@ -63,10 +48,6 @@ public class DdLogsImplementation: NSObject { @objc public func error(message: String, context: NSDictionary, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { - if (!self.isSDKInitialized()) { - reject(nil, Errors.logSentBeforeSDKInit, nil) - return - } let attributes = castAttributesToSwift(context).mergeWithGlobalAttributes() logger.error(message, error: nil, attributes: attributes) resolve(nil) @@ -74,10 +55,6 @@ public class DdLogsImplementation: NSObject { @objc public func debugWithError(message: String, errorKind: String?, errorMessage: String?, stacktrace: String?, context: NSDictionary, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { - if (!self.isSDKInitialized()) { - reject(nil, Errors.logSentBeforeSDKInit, nil) - return - } let attributes = castAttributesToSwift(context).mergeWithGlobalAttributes() logger._internal.log(level: .debug, message: message, errorKind: errorKind, errorMessage: errorMessage, stackTrace: stacktrace, attributes: attributes) resolve(nil) @@ -85,10 +62,6 @@ public class DdLogsImplementation: NSObject { @objc public func infoWithError(message: String, errorKind: String?, errorMessage: String?, stacktrace: String?, context: NSDictionary, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { - if (!self.isSDKInitialized()) { - reject(nil, Errors.logSentBeforeSDKInit, nil) - return - } let attributes = castAttributesToSwift(context).mergeWithGlobalAttributes() logger._internal.log(level: .info, message: message, errorKind: errorKind, errorMessage: errorMessage, stackTrace: stacktrace, attributes: attributes) resolve(nil) @@ -96,10 +69,6 @@ public class DdLogsImplementation: NSObject { @objc public func warnWithError(message: String, errorKind: String?, errorMessage: String?, stacktrace: String?, context: NSDictionary, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { - if (!self.isSDKInitialized()) { - reject(nil, Errors.logSentBeforeSDKInit, nil) - return - } let attributes = castAttributesToSwift(context).mergeWithGlobalAttributes() logger._internal.log(level: .warn, message: message, errorKind: errorKind, errorMessage: errorMessage, stackTrace: stacktrace, attributes: attributes) resolve(nil) @@ -107,10 +76,6 @@ public class DdLogsImplementation: NSObject { @objc public func errorWithError(message: String, errorKind: String?, errorMessage: String?, stacktrace: String?, context: NSDictionary, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { - if (!self.isSDKInitialized()) { - reject(nil, Errors.logSentBeforeSDKInit, nil) - return - } let attributes = castAttributesToSwift(context).mergeWithGlobalAttributes() logger._internal.log(level: .error, message: message, errorKind: errorKind, errorMessage: errorMessage, stackTrace: stacktrace, attributes: attributes) resolve(nil) From c8a217118390658047fa2fd50b21ad92b558ac85 Mon Sep 17 00:00:00 2001 From: Sergio Barrio Date: Thu, 4 Dec 2025 17:42:17 +0100 Subject: [PATCH 102/410] Fix iOS tests --- packages/core/ios/Tests/DdLogsTests.swift | 33 ++--------------------- 1 file changed, 2 insertions(+), 31 deletions(-) diff --git a/packages/core/ios/Tests/DdLogsTests.swift b/packages/core/ios/Tests/DdLogsTests.swift index 60640e807..d3d31a95c 100644 --- a/packages/core/ios/Tests/DdLogsTests.swift +++ b/packages/core/ios/Tests/DdLogsTests.swift @@ -14,7 +14,7 @@ func mockReject(args: String?, arg: String?, err: Error?) {} internal class DdLogsTests: XCTestCase { private let mockNativeLogger = MockNativeLogger() - private lazy var logger = DdLogsImplementation({ self.mockNativeLogger }, { true }) + private lazy var logger = DdLogsImplementation({ self.mockNativeLogger }) private let testMessage_swift: String = "message" private let testMessage_objc: NSString = "message" @@ -80,7 +80,7 @@ internal class DdLogsTests: XCTestCase { let logger = DdLogsImplementation({ [unowned self] in expectation.fulfill() return self.mockNativeLogger - }, { true }) + }) // When (0..<10).forEach { _ in logger.debug(message: "foo", context: [:], resolve: mockResolve, reject: mockReject)} @@ -372,35 +372,6 @@ internal class DdLogsTests: XCTestCase { GlobalState.globalAttributes.keys ) } - - func testDoesNotInitializeLoggerBeforeSdkIsInitialized() throws { - var isInitialized = false - let newLogger = DdLogsImplementation({ self.mockNativeLogger }, { isInitialized }) - - newLogger.debug(message: testMessage_objc as String, context: validTestAttributes_objc, resolve: mockResolve, reject: mockReject) - newLogger.info(message: testMessage_objc as String, context: validTestAttributes_objc, resolve: mockResolve, reject: mockReject) - newLogger.warn(message: testMessage_objc as String, context: validTestAttributes_objc, resolve: mockResolve, reject: mockReject) - newLogger.error(message: testMessage_objc as String, context: validTestAttributes_objc, resolve: mockResolve, reject: mockReject) - newLogger.debugWithError(message: testMessage_objc as String, errorKind: testErrorKind_objc as String, errorMessage: testErrorMessage_objc as String, stacktrace: testErrorStacktrace_objc as String, context: invalidTestAttributes, resolve: mockResolve, reject: mockReject) - newLogger.infoWithError(message: testMessage_objc as String, errorKind: testErrorKind_objc as String, errorMessage: testErrorMessage_objc as String, stacktrace: testErrorStacktrace_objc as String, context: invalidTestAttributes, resolve: mockResolve, reject: mockReject) - newLogger.warnWithError(message: testMessage_objc as String, errorKind: testErrorKind_objc as String, errorMessage: testErrorMessage_objc as String, stacktrace: testErrorStacktrace_objc as String, context: invalidTestAttributes, resolve: mockResolve, reject: mockReject) - newLogger.errorWithError(message: testMessage_objc as String, errorKind: testErrorKind_objc as String, errorMessage: testErrorMessage_objc as String, stacktrace: testErrorStacktrace_objc as String, context: invalidTestAttributes, resolve: mockResolve, reject: mockReject) - - XCTAssertEqual(mockNativeLogger.receivedMethodCalls.count, 0) - - isInitialized = true - - newLogger.debug(message: testMessage_objc as String, context: validTestAttributes_objc, resolve: mockResolve, reject: mockReject) - newLogger.info(message: testMessage_objc as String, context: validTestAttributes_objc, resolve: mockResolve, reject: mockReject) - newLogger.warn(message: testMessage_objc as String, context: validTestAttributes_objc, resolve: mockResolve, reject: mockReject) - newLogger.error(message: testMessage_objc as String, context: validTestAttributes_objc, resolve: mockResolve, reject: mockReject) - newLogger.debugWithError(message: testMessage_objc as String, errorKind: testErrorKind_objc as String, errorMessage: testErrorMessage_objc as String, stacktrace: testErrorStacktrace_objc as String, context: invalidTestAttributes, resolve: mockResolve, reject: mockReject) - newLogger.infoWithError(message: testMessage_objc as String, errorKind: testErrorKind_objc as String, errorMessage: testErrorMessage_objc as String, stacktrace: testErrorStacktrace_objc as String, context: invalidTestAttributes, resolve: mockResolve, reject: mockReject) - newLogger.warnWithError(message: testMessage_objc as String, errorKind: testErrorKind_objc as String, errorMessage: testErrorMessage_objc as String, stacktrace: testErrorStacktrace_objc as String, context: invalidTestAttributes, resolve: mockResolve, reject: mockReject) - newLogger.errorWithError(message: testMessage_objc as String, errorKind: testErrorKind_objc as String, errorMessage: testErrorMessage_objc as String, stacktrace: testErrorStacktrace_objc as String, context: invalidTestAttributes, resolve: mockResolve, reject: mockReject) - - XCTAssertEqual(mockNativeLogger.receivedMethodCalls.count, 8) - } } private class MockNativeLogger: LoggerProtocol { From 0266a89db8fe59726125255ea5a055df1e99624b Mon Sep 17 00:00:00 2001 From: Marco Saia Date: Fri, 10 Oct 2025 11:31:03 +0200 Subject: [PATCH 103/410] Attributes Safe Encoding --- packages/core/src/DdAttributes.ts | 9 +- packages/core/src/DdSdkReactNative.tsx | 33 +-- .../src/DdSdkReactNativeConfiguration.tsx | 17 ++ packages/core/src/logs/DdLogs.ts | 28 +- packages/core/src/nativeModulesTypes.ts | 4 +- packages/core/src/rum/DdRum.ts | 41 +-- .../instrumentation/DdRumErrorTracking.tsx | 33 +-- .../DdRumUserInteractionTracking.tsx | 8 +- .../__tests__/attributesEncoding.test.ts | 266 ++++++++++++++++++ .../__tests__/defaultEncoders.test.ts | 198 +++++++++++++ .../AttributesEncoding/attributesEncoding.tsx | 41 +++ .../AttributesEncoding/defaultEncoders.tsx | 185 ++++++++++++ .../AttributesEncoding/errorUtils.tsx} | 85 +++--- .../src/sdk/AttributesEncoding/helpers.tsx | 134 +++++++++ .../core/src/sdk/AttributesEncoding/types.tsx | 33 +++ .../core/src/sdk/AttributesEncoding/utils.tsx | 31 ++ .../DatadogProvider/Buffer/BoundedBuffer.ts | 6 +- packages/core/src/sdk/DdSdk.ts | 12 - packages/core/src/sdk/DdSdk.tsx | 15 + packages/core/src/sdk/DdSdkInternal.tsx | 105 +++++++ .../core/src/sdk/EventMappers/EventMapper.ts | 4 +- packages/core/src/trace/DdTrace.ts | 6 +- packages/core/src/types.tsx | 5 +- packages/core/src/utils/argsUtils.ts | 35 --- .../react-native-apollo-client/src/helpers.ts | 4 +- 25 files changed, 1173 insertions(+), 165 deletions(-) create mode 100644 packages/core/src/sdk/AttributesEncoding/__tests__/attributesEncoding.test.ts create mode 100644 packages/core/src/sdk/AttributesEncoding/__tests__/defaultEncoders.test.ts create mode 100644 packages/core/src/sdk/AttributesEncoding/attributesEncoding.tsx create mode 100644 packages/core/src/sdk/AttributesEncoding/defaultEncoders.tsx rename packages/core/src/{utils/errorUtils.ts => sdk/AttributesEncoding/errorUtils.tsx} (56%) create mode 100644 packages/core/src/sdk/AttributesEncoding/helpers.tsx create mode 100644 packages/core/src/sdk/AttributesEncoding/types.tsx create mode 100644 packages/core/src/sdk/AttributesEncoding/utils.tsx delete mode 100644 packages/core/src/sdk/DdSdk.ts create mode 100644 packages/core/src/sdk/DdSdk.tsx create mode 100644 packages/core/src/sdk/DdSdkInternal.tsx delete mode 100644 packages/core/src/utils/argsUtils.ts diff --git a/packages/core/src/DdAttributes.ts b/packages/core/src/DdAttributes.ts index 91e20d5b3..9d92e600e 100644 --- a/packages/core/src/DdAttributes.ts +++ b/packages/core/src/DdAttributes.ts @@ -16,5 +16,12 @@ export const DdAttributes = { * Custom fingerprint to an error. * Expects {@link String} value. */ - errorFingerprint: '_dd.error.fingerprint' + errorFingerprint: '_dd.error.fingerprint', + + /** + * Debug ID attached to a log or a RUM event. + * The Debug ID establishes a unique connection between a bundle and its corresponding sourcemap. + * Expects {@link String} value. + */ + debugId: '_dd.debug_id' }; diff --git a/packages/core/src/DdSdkReactNative.tsx b/packages/core/src/DdSdkReactNative.tsx index e07ba4e34..243b11491 100644 --- a/packages/core/src/DdSdkReactNative.tsx +++ b/packages/core/src/DdSdkReactNative.tsx @@ -35,7 +35,7 @@ import { AttributesSingleton } from './sdk/AttributesSingleton/AttributesSinglet import type { Attributes } from './sdk/AttributesSingleton/types'; import { registerNativeBridge } from './sdk/DatadogInternalBridge/DdSdkInternalNativeBridge'; import { BufferSingleton } from './sdk/DatadogProvider/Buffer/BufferSingleton'; -import { DdSdk } from './sdk/DdSdk'; +import { NativeDdSdk } from './sdk/DdSdkInternal'; import { FileBasedConfiguration } from './sdk/FileBasedConfiguration/FileBasedConfiguration'; import { GlobalState } from './sdk/GlobalState/GlobalState'; import { UserInfoSingleton } from './sdk/UserInfoSingleton/UserInfoSingleton'; @@ -84,7 +84,7 @@ export class DdSdkReactNative { SdkVerbosity.WARN ); if (!__DEV__) { - DdSdk.telemetryDebug( + NativeDdSdk.telemetryDebug( 'RN SDK was already initialized in javascript' ); } @@ -95,7 +95,7 @@ export class DdSdkReactNative { registerNativeBridge(); - await DdSdk.initialize( + await NativeDdSdk.initialize( DdSdkReactNative.buildConfiguration(configuration, params) ); @@ -189,7 +189,7 @@ export class DdSdkReactNative { `Adding attribute ${JSON.stringify(value)} for key ${key}`, SdkVerbosity.DEBUG ); - await DdSdk.addAttribute(key, { value }); + await NativeDdSdk.addAttribute(key, { value }); AttributesSingleton.getInstance().addAttribute(key, value); }; @@ -202,7 +202,7 @@ export class DdSdkReactNative { `Removing attribute for key ${key}`, SdkVerbosity.DEBUG ); - await DdSdk.removeAttribute(key); + await NativeDdSdk.removeAttribute(key); AttributesSingleton.getInstance().removeAttribute(key); }; @@ -216,7 +216,7 @@ export class DdSdkReactNative { `Adding attributes ${JSON.stringify(attributes)}`, SdkVerbosity.DEBUG ); - await DdSdk.addAttributes(attributes); + await NativeDdSdk.addAttributes(attributes); AttributesSingleton.getInstance().addAttributes(attributes); }; @@ -229,7 +229,7 @@ export class DdSdkReactNative { `Removing attributes for keys ${JSON.stringify(keys)}`, SdkVerbosity.DEBUG ); - await DdSdk.removeAttributes(keys); + await NativeDdSdk.removeAttributes(keys); AttributesSingleton.getInstance().removeAttributes(keys); }; @@ -252,7 +252,7 @@ export class DdSdkReactNative { SdkVerbosity.DEBUG ); - await DdSdk.setUserInfo(userInfo); + await NativeDdSdk.setUserInfo(userInfo); UserInfoSingleton.getInstance().setUserInfo(userInfo); }; @@ -262,7 +262,7 @@ export class DdSdkReactNative { */ static clearUserInfo = async (): Promise => { InternalLog.log('Clearing user info', SdkVerbosity.DEBUG); - await DdSdk.clearUserInfo(); + await NativeDdSdk.clearUserInfo(); UserInfoSingleton.getInstance().clearUserInfo(); }; @@ -296,7 +296,7 @@ export class DdSdkReactNative { } }; - await DdSdk.addUserExtraInfo(extraUserInfo); + await NativeDdSdk.addUserExtraInfo(extraUserInfo); UserInfoSingleton.getInstance().setUserInfo(updatedUserInfo); }; @@ -317,7 +317,7 @@ export class DdSdkReactNative { SdkVerbosity.DEBUG ); - await DdSdk.setAccountInfo(accountInfo); + await NativeDdSdk.setAccountInfo(accountInfo); AccountInfoSingleton.getInstance().setAccountInfo(accountInfo); }; @@ -327,7 +327,7 @@ export class DdSdkReactNative { */ static clearAccountInfo = async (): Promise => { InternalLog.log('Clearing account info', SdkVerbosity.DEBUG); - await DdSdk.clearAccountInfo(); + await NativeDdSdk.clearAccountInfo(); AccountInfoSingleton.getInstance().clearAccountInfo(); }; @@ -359,7 +359,7 @@ export class DdSdkReactNative { ...extraAccountInfo }; - await DdSdk.addAccountExtraInfo(extraInfo); + await NativeDdSdk.addAccountExtraInfo(extraInfo); AccountInfoSingleton.getInstance().addAccountExtraInfo( extraAccountInfo ); @@ -372,7 +372,7 @@ export class DdSdkReactNative { */ static setTrackingConsent = (consent: TrackingConsent): Promise => { InternalLog.log(`Setting consent ${consent}`, SdkVerbosity.DEBUG); - return DdSdk.setTrackingConsent(consent); + return NativeDdSdk.setTrackingConsent(consent); }; /** @@ -381,7 +381,7 @@ export class DdSdkReactNative { */ static clearAllData = (): Promise => { InternalLog.log('Clearing all data', SdkVerbosity.DEBUG); - return DdSdk.clearAllData(); + return NativeDdSdk.clearAllData(); }; private static buildConfiguration = ( @@ -464,7 +464,8 @@ export class DdSdkReactNative { configuration.trackWatchdogTerminations, configuration.batchProcessingLevel, configuration.initialResourceThreshold, - configuration.trackMemoryWarnings + configuration.trackMemoryWarnings, + configuration.attributeEncoders ); }; diff --git a/packages/core/src/DdSdkReactNativeConfiguration.tsx b/packages/core/src/DdSdkReactNativeConfiguration.tsx index 4ec1ef883..bcdf8e877 100644 --- a/packages/core/src/DdSdkReactNativeConfiguration.tsx +++ b/packages/core/src/DdSdkReactNativeConfiguration.tsx @@ -12,6 +12,7 @@ import type { ErrorEventMapper } from './rum/eventMappers/errorEventMapper'; import type { ResourceEventMapper } from './rum/eventMappers/resourceEventMapper'; import type { FirstPartyHost } from './rum/types'; import { PropagatorType } from './rum/types'; +import type { AttributeEncoder } from './sdk/AttributesEncoding/types'; import type { LogEventMapper } from './types'; export enum VitalsUpdateFrequency { @@ -323,6 +324,22 @@ export class DdSdkReactNativeConfiguration { */ public initialResourceThreshold?: number; + /** + * Optional list of custom encoders for attributes. + * + * Each encoder defines how to detect (`check`) and transform (`encode`) + * values of a specific type that is not handled by the built-in encoders + * (e.g., domain-specific objects, custom classes). + * + * These encoders are applied before the built-in ones. If an encoder + * successfully `check` a value, its `encode` result will be used. + * + * Example use cases: + * - Serializing a custom `UUID` class into a string + * - Handling third-party library objects that are not JSON-serializable + */ + public attributeEncoders: AttributeEncoder[] = []; + /** * Determines whether the SDK should track application termination by the watchdog on iOS. Default: `false`. */ diff --git a/packages/core/src/logs/DdLogs.ts b/packages/core/src/logs/DdLogs.ts index e1936d211..9b99dc5ee 100644 --- a/packages/core/src/logs/DdLogs.ts +++ b/packages/core/src/logs/DdLogs.ts @@ -8,8 +8,8 @@ import { DdAttributes } from '../DdAttributes'; import { DATADOG_MESSAGE_PREFIX, InternalLog } from '../InternalLog'; import { SdkVerbosity } from '../SdkVerbosity'; import type { DdNativeLogsType } from '../nativeModulesTypes'; +import { encodeAttributes } from '../sdk/AttributesEncoding/attributesEncoding'; import type { ErrorSource, LogEventMapper } from '../types'; -import { validateContext } from '../utils/argsUtils'; import { getGlobalInstance } from '../utils/singletonUtils'; import { generateEventMapper } from './eventMapper'; @@ -37,7 +37,7 @@ const isLogWithError = ( typeof args[1] === 'string' || typeof args[2] === 'string' || typeof args[3] === 'string' || - typeof args[4] === 'object' || + (args[4] !== undefined && args[4] !== null) || typeof args[5] === 'string' ); }; @@ -55,12 +55,12 @@ class DdLogsWrapper implements DdLogsType { args[1], args[2], args[3], - validateContext(args[4]), + args[4] ?? {}, 'debug', args[5] ); } - return this.log(args[0], validateContext(args[1]), 'debug'); + return this.log(args[0], args[1] ?? {}, 'debug'); }; info = (...args: LogArguments | LogWithErrorArguments): Promise => { @@ -70,12 +70,12 @@ class DdLogsWrapper implements DdLogsType { args[1], args[2], args[3], - validateContext(args[4]), + args[4] ?? {}, 'info', args[5] ); } - return this.log(args[0], validateContext(args[1]), 'info'); + return this.log(args[0], args[1] ?? {}, 'info'); }; warn = (...args: LogArguments | LogWithErrorArguments): Promise => { @@ -85,12 +85,12 @@ class DdLogsWrapper implements DdLogsType { args[1], args[2], args[3], - validateContext(args[4]), + args[4] ?? {}, 'warn', args[5] ); } - return this.log(args[0], validateContext(args[1]), 'warn'); + return this.log(args[0], args[1] ?? {}, 'warn'); }; error = (...args: LogArguments | LogWithErrorArguments): Promise => { @@ -100,13 +100,13 @@ class DdLogsWrapper implements DdLogsType { args[1], args[2], args[3], - validateContext(args[4]), + args[4] ?? {}, 'error', args[5], args[6] ); } - return this.log(args[0], validateContext(args[1]), 'error'); + return this.log(args[0], args[1] ?? {}, 'error'); }; /** @@ -161,7 +161,10 @@ class DdLogsWrapper implements DdLogsType { this.printLogTracked(event.message, status); try { - return await this.nativeLogs[status](event.message, event.context); + return await this.nativeLogs[status]( + event.message, + encodeAttributes(event.context) + ); } catch (error) { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore @@ -204,8 +207,9 @@ class DdLogsWrapper implements DdLogsType { this.printLogTracked(mappedEvent.message, status); try { + const encodedContext = encodeAttributes(mappedEvent.context); const updatedContext = { - ...mappedEvent.context, + ...encodedContext, [DdAttributes.errorSourceType]: 'react-native' }; diff --git a/packages/core/src/nativeModulesTypes.ts b/packages/core/src/nativeModulesTypes.ts index b05fb6e95..9b6d71e06 100644 --- a/packages/core/src/nativeModulesTypes.ts +++ b/packages/core/src/nativeModulesTypes.ts @@ -4,6 +4,7 @@ * Copyright 2016-Present Datadog, Inc. */ +import type { AttributeEncoder } from './sdk/AttributesEncoding/types'; import type { Spec as NativeDdLogs } from './specs/NativeDdLogs'; import type { Spec as NativeDdRum } from './specs/NativeDdRum'; import type { Spec as NativeDdSdk } from './specs/NativeDdSdk'; @@ -36,7 +37,8 @@ export class DdNativeSdkConfiguration { readonly sampleRate: number, readonly site: string, readonly trackingConsent: string, - readonly additionalConfiguration: object // eslint-disable-next-line no-empty-function + readonly additionalConfiguration: object, + readonly attributeEncoders: AttributeEncoder[] // eslint-disable-next-line no-empty-function ) {} } diff --git a/packages/core/src/rum/DdRum.ts b/packages/core/src/rum/DdRum.ts index 3ae7c10ce..b921df580 100644 --- a/packages/core/src/rum/DdRum.ts +++ b/packages/core/src/rum/DdRum.ts @@ -8,14 +8,14 @@ import type { GestureResponderEvent } from 'react-native'; import { DdAttributes } from '../DdAttributes'; import { InternalLog } from '../InternalLog'; import { SdkVerbosity } from '../SdkVerbosity'; +import { debugId } from '../metro/debugIdResolver'; import type { DdNativeRumType } from '../nativeModulesTypes'; +import { encodeAttributes } from '../sdk/AttributesEncoding/attributesEncoding'; import type { Attributes } from '../sdk/AttributesSingleton/types'; import { bufferVoidNativeCall } from '../sdk/DatadogProvider/Buffer/bufferNativeCall'; -import { DdSdk } from '../sdk/DdSdk'; +import { NativeDdSdk } from '../sdk/DdSdkInternal'; import { GlobalState } from '../sdk/GlobalState/GlobalState'; import type { ErrorSource, FeatureOperationFailure } from '../types'; -import { validateContext } from '../utils/argsUtils'; -import { getErrorContext } from '../utils/errorUtils'; import { getGlobalInstance } from '../utils/singletonUtils'; import { DefaultTimeProvider } from '../utils/time-provider/DefaultTimeProvider'; import type { TimeProvider } from '../utils/time-provider/TimeProvider'; @@ -76,7 +76,7 @@ class DdRumWrapper implements DdRumType { this.nativeRum.startView( key, name, - validateContext(context), + encodeAttributes(context), timestampMs ) ); @@ -89,7 +89,7 @@ class DdRumWrapper implements DdRumType { ): Promise => { InternalLog.log(`Stopping RUM View #${key}`, SdkVerbosity.DEBUG); return bufferVoidNativeCall(() => - this.nativeRum.stopView(key, validateContext(context), timestampMs) + this.nativeRum.stopView(key, encodeAttributes(context), timestampMs) ); }; @@ -108,7 +108,7 @@ class DdRumWrapper implements DdRumType { this.nativeRum.startAction( type, name, - validateContext(context), + encodeAttributes(context), timestampMs ) ); @@ -199,7 +199,7 @@ class DdRumWrapper implements DdRumType { const mappedEvent = this.actionEventMapper.applyEventMapper({ type, name, - context: validateContext(context), + context, timestampMs, actionContext }); @@ -214,7 +214,7 @@ class DdRumWrapper implements DdRumType { this.nativeRum.addAction( mappedEvent.type, mappedEvent.name, - mappedEvent.context, + encodeAttributes(mappedEvent.context), mappedEvent.timestampMs ) ); @@ -237,7 +237,7 @@ class DdRumWrapper implements DdRumType { key, method, url, - validateContext(context), + encodeAttributes(context), timestampMs ) ); @@ -257,7 +257,7 @@ class DdRumWrapper implements DdRumType { statusCode, kind, size, - context: validateContext(context), + context, timestampMs, resourceContext }); @@ -291,7 +291,7 @@ class DdRumWrapper implements DdRumType { mappedEvent.statusCode, mappedEvent.kind, mappedEvent.size, - mappedEvent.context, + encodeAttributes(mappedEvent.context), mappedEvent.timestampMs ) ); @@ -309,7 +309,7 @@ class DdRumWrapper implements DdRumType { message, source, stacktrace, - context: getErrorContext(validateContext(context)), + context, timestampMs, fingerprint: fingerprint ?? '' }); @@ -318,8 +318,13 @@ class DdRumWrapper implements DdRumType { return generateEmptyPromise(); } InternalLog.log(`Adding RUM Error “${message}”`, SdkVerbosity.DEBUG); - const updatedContext: any = mappedEvent.context; + const updatedContext = encodeAttributes(mappedEvent.context); updatedContext[DdAttributes.errorSourceType] = 'react-native'; + + if (debugId) { + updatedContext[DdAttributes.debugId] = debugId; + } + return bufferVoidNativeCall(() => this.nativeRum.addError( mappedEvent.message, @@ -499,7 +504,7 @@ class DdRumWrapper implements DdRumType { const mappedEvent = this.actionEventMapper.applyEventMapper({ type, name, - context: validateContext(context), + context, timestampMs }); if (!mappedEvent) { @@ -519,7 +524,7 @@ class DdRumWrapper implements DdRumType { this.nativeRum.stopAction( mappedEvent.type, mappedEvent.name, - mappedEvent.context, + encodeAttributes(mappedEvent.context), mappedEvent.timestampMs ) ); @@ -546,20 +551,20 @@ class DdRumWrapper implements DdRumType { return [ args[0], args[1], - validateContext(args[2]), + args[2] ?? {}, args[3] || this.timeProvider.now() ]; } if (isOldStopActionAPI(args)) { if (this.lastActionData) { - DdSdk.telemetryDebug( + NativeDdSdk.telemetryDebug( 'DDdRum.stopAction called with the old signature' ); const { type, name } = this.lastActionData; return [ type, name, - validateContext(args[0]), + args[0] ?? {}, args[1] || this.timeProvider.now() ]; } diff --git a/packages/core/src/rum/instrumentation/DdRumErrorTracking.tsx b/packages/core/src/rum/instrumentation/DdRumErrorTracking.tsx index 3c3ec9f65..b80309858 100644 --- a/packages/core/src/rum/instrumentation/DdRumErrorTracking.tsx +++ b/packages/core/src/rum/instrumentation/DdRumErrorTracking.tsx @@ -8,15 +8,15 @@ import type { ErrorHandlerCallback } from 'react-native'; import { InternalLog } from '../../InternalLog'; import { SdkVerbosity } from '../../SdkVerbosity'; -import { ErrorSource } from '../../types'; +import { errorEncoder } from '../../sdk/AttributesEncoding/defaultEncoders'; import { + ERROR_DEFAULT_NAME, + ERROR_EMPTY_STACKTRACE, getErrorMessage, - getErrorStackTrace, - EMPTY_STACK_TRACE, getErrorName, - DEFAULT_ERROR_NAME, - getErrorContext -} from '../../utils/errorUtils'; + getErrorStackTrace +} from '../../sdk/AttributesEncoding/errorUtils'; +import { ErrorSource } from '../../types'; import { executeWithDelay } from '../../utils/jsUtils'; import { DdRum } from '../DdRum'; @@ -72,7 +72,7 @@ export class DdRumErrorTracking { const stacktrace = getErrorStackTrace(error); this.reportError(message, ErrorSource.SOURCE, stacktrace, { '_dd.error.is_crash': isFatal, - '_dd.error.raw': error + '_dd.error.raw': errorEncoder.encode(error) }).then(async () => { DdRumErrorTracking.isInDefaultErrorHandler = true; try { @@ -96,24 +96,24 @@ export class DdRumErrorTracking { return; } - let stack: string = EMPTY_STACK_TRACE; - let errorName: string = DEFAULT_ERROR_NAME; + let stack: string = ERROR_EMPTY_STACKTRACE; + let errorName: string = ERROR_DEFAULT_NAME; for (let i = 0; i < params.length; i += 1) { const param = params[i]; const paramStack = getErrorStackTrace(param); - if (paramStack !== EMPTY_STACK_TRACE) { + if (paramStack !== ERROR_EMPTY_STACKTRACE) { stack = paramStack; } const paramErrorName = getErrorName(param); - if (paramErrorName !== DEFAULT_ERROR_NAME) { + if (paramErrorName !== ERROR_DEFAULT_NAME) { errorName = paramErrorName; } if ( - errorName !== DEFAULT_ERROR_NAME && - stack !== EMPTY_STACK_TRACE + errorName !== ERROR_DEFAULT_NAME && + stack !== ERROR_EMPTY_STACKTRACE ) { break; } @@ -140,11 +140,6 @@ export class DdRumErrorTracking { stacktrace: string, context: object = {} ): Promise => { - return DdRum.addError( - message, - source, - stacktrace, - getErrorContext(context) - ); + return DdRum.addError(message, source, stacktrace, context); }; } diff --git a/packages/core/src/rum/instrumentation/interactionTracking/DdRumUserInteractionTracking.tsx b/packages/core/src/rum/instrumentation/interactionTracking/DdRumUserInteractionTracking.tsx index 03fe98262..0e273435c 100644 --- a/packages/core/src/rum/instrumentation/interactionTracking/DdRumUserInteractionTracking.tsx +++ b/packages/core/src/rum/instrumentation/interactionTracking/DdRumUserInteractionTracking.tsx @@ -8,8 +8,8 @@ import React from 'react'; import { InternalLog } from '../../../InternalLog'; import { SdkVerbosity } from '../../../SdkVerbosity'; -import { DdSdk } from '../../../sdk/DdSdk'; -import { getErrorMessage } from '../../../utils/errorUtils'; +import { getErrorMessage } from '../../../sdk/AttributesEncoding/errorUtils'; +import { NativeDdSdk } from '../../../sdk/DdSdkInternal'; import { BABEL_PLUGIN_TELEMETRY } from '../../constants'; import { DdBabelInteractionTracking } from './DdBabelInteractionTracking'; @@ -72,7 +72,7 @@ export class DdRumUserInteractionTracking { return; } - DdSdk?.sendTelemetryLog( + NativeDdSdk?.sendTelemetryLog( BABEL_PLUGIN_TELEMETRY, DdBabelInteractionTracking.getTelemetryConfig(), { onlyOnce: true } @@ -116,7 +116,7 @@ export class DdRumUserInteractionTracking { }; } } catch (e) { - DdSdk.telemetryDebug(getErrorMessage(e)); + NativeDdSdk.telemetryDebug(getErrorMessage(e)); } const originalMemo = React.memo; diff --git a/packages/core/src/sdk/AttributesEncoding/__tests__/attributesEncoding.test.ts b/packages/core/src/sdk/AttributesEncoding/__tests__/attributesEncoding.test.ts new file mode 100644 index 000000000..9cd2718ff --- /dev/null +++ b/packages/core/src/sdk/AttributesEncoding/__tests__/attributesEncoding.test.ts @@ -0,0 +1,266 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +import { DdSdk } from '../../DdSdk'; +import { encodeAttributes } from '../attributesEncoding'; +import { warn } from '../utils'; + +jest.mock('../utils', () => ({ + ...jest.requireActual('../utils'), + warn: jest.fn() +})); + +const setEncoders = (encoders: any[]) => { + (DdSdk as any)?._setAttributeEncodersForTesting(encoders); +}; + +describe('encodeAttributes', () => { + beforeEach(() => { + (warn as jest.Mock).mockClear(); + setEncoders([]); + }); + + it('wraps root string under context', () => { + const result = encodeAttributes('foo'); + expect(result).toEqual({ context: 'foo' }); + expect(warn).toHaveBeenCalled(); + }); + + it('wraps root number under context', () => { + const result = encodeAttributes(123); + expect(result).toEqual({ context: 123 }); + expect(warn).toHaveBeenCalled(); + }); + + it('wraps root array under context', () => { + const result = encodeAttributes([1, 2, 3]); + expect(result).toEqual({ context: [1, 2, 3] }); + expect(warn).toHaveBeenCalled(); + }); + + it('drops unsupported root function', () => { + const result = encodeAttributes(() => {}); + expect(result).toEqual({}); + expect(warn).toHaveBeenCalled(); + }); + + it('drops unsupported root symbol', () => { + const result = encodeAttributes(Symbol('x')); + expect(result).toEqual({}); + expect(warn).toHaveBeenCalled(); + }); + + it('flattens nested objects using dot syntax', () => { + const input = { user: { profile: { name: 'Alice' } } }; + const result = encodeAttributes(input); + expect(result).toEqual({ 'user.profile.name': 'Alice' }); + expect(warn).not.toHaveBeenCalled(); + }); + + it('keeps arrays as arrays inside objects', () => { + const input = { tags: ['a', 'b'] }; + const result = encodeAttributes(input); + expect(result).toEqual({ tags: ['a', 'b'] }); + }); + + it('flattens nested arrays of objects', () => { + const input = { arr: [{ x: 1 }, { y: 2 }] }; + const result = encodeAttributes(input); + expect(result).toEqual({ + arr: [{ x: 1 }, { y: 2 }] + }); + }); + + it('applies custom attribute encoders before built-in ones', () => { + setEncoders([ + { + check: (v: any): v is Date => v instanceof Date, + encode: (d: Date) => 'CUSTOM_DATE' + } + ]); + + const result = encodeAttributes({ now: new Date() }); + expect(result).toEqual({ now: 'CUSTOM_DATE' }); + }); + + it('applies built-in Date encoder if no custom encoder is provided', () => { + const date = new Date('2020-01-01T12:00:00Z'); + const result = encodeAttributes({ now: date }); + expect(typeof result.now).toBe('string'); + expect(result.now).toContain('2020'); + }); + + it('applies built-in Error encoder', () => { + const error = new Error('boom'); + const result = encodeAttributes({ err: error }); + expect(result['err.name']).toBe('Error'); + expect(result['err.message']).toBe('boom'); + expect(result['err.stack']).toContain('Error: boom'); + }); + + it('applies built-in Map encoder', () => { + const map = new Map([ + ['k1', 1], + ['k2', { nested: 'yes' }] + ]); + const result = encodeAttributes({ data: map }); + expect(Array.isArray(result.data)).toBe(true); + expect(result.data).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + key: 'k1', + keyType: 'string', + value: 1 + }), + expect.objectContaining({ + key: 'k2', + keyType: 'string', + 'value.nested': 'yes' + }) + ]) + ); + }); + + it('drops unsupported nested values', () => { + const input = { valid: 'ok', bad: () => {} }; + const result = encodeAttributes(input); + expect(result).toEqual({ valid: 'ok' }); + }); + + it('handles deeply nested objects', () => { + const deep = { level1: { level2: { level3: { value: 42 } } } }; + const result = encodeAttributes(deep); + expect(result).toEqual({ 'level1.level2.level3.value': 42 }); + }); + + it('handles object with manual dot keys', () => { + const input = { 'user.profile.name': 'Bob' }; + const result = encodeAttributes(input); + expect(result).toEqual({ 'user.profile.name': 'Bob' }); + }); + + it('handles array with mixed values', () => { + const input = [1, 'two', { nested: true }]; + const result = encodeAttributes(input); + expect(result).toEqual({ context: [1, 'two', { nested: true }] }); + }); + + it('handles empty object gracefully', () => { + const result = encodeAttributes({}); + expect(result).toEqual({}); + expect(warn).not.toHaveBeenCalled(); + }); + + it('handles empty array gracefully at root', () => { + const result = encodeAttributes([]); + expect(result).toEqual({ context: [] }); + expect(warn).toHaveBeenCalled(); + }); + + it('handles NaN and Infinity by dropping them', () => { + const result = encodeAttributes({ + bad1: NaN, + bad2: Infinity, + good: 42 + }); + expect(result).toEqual({ good: 42 }); + }); + + it('flattens object nested inside array', () => { + const input = { arr: [{ foo: 'bar' }] }; + const result = encodeAttributes(input); + expect(result).toEqual({ arr: [{ foo: 'bar' }] }); + }); + + it('handles array of arrays correctly', () => { + const input = { + matrix: [ + [1, 2], + [3, 4] + ] + }; + const result = encodeAttributes(input); + expect(result).toEqual({ + matrix: [ + [1, 2], + [3, 4] + ] + }); + }); + + it('drops functions inside arrays', () => { + const input = { arr: [1, () => {}, 3] }; + const result = encodeAttributes(input); + expect(result.arr).toEqual([1, 3]); + }); + + it('encodes nested Maps inside objects', () => { + const map = new Map([['nested', new Map([['k', 'v']])]]); + const result = encodeAttributes({ outer: map }); + expect(result.outer).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + key: 'nested', + value: expect.arrayContaining([ + expect.objectContaining({ key: 'k', value: 'v' }) + ]) + }) + ]) + ); + }); + + it('handles deeply nested array of objects', () => { + const input = { items: [[{ foo: 'bar' }]] }; + const result = encodeAttributes(input); + expect(result.items).toEqual([[{ foo: 'bar' }]]); + }); + + it('handles objects with undefined values by dropping them', () => { + const input = { a: 1, b: undefined, c: 'ok' }; + const result = encodeAttributes(input); + expect(result).toEqual({ a: 1, c: 'ok' }); + }); + + it('custom encoder can override primitive handling', () => { + setEncoders([ + { + check: (v: any): v is number => typeof v === 'number', + encode: (n: number) => `num:${n}` + } + ]); + const result = encodeAttributes({ a: 5 }); + expect(result).toEqual({ a: 'num:5' }); + }); + + it('handles object with both dot syntax and nested keys without collisions', () => { + const input = { + 'user.profile.name': 'Alice', + user: { profile: { age: 30 } } + }; + const result = encodeAttributes(input); + expect(result).toEqual({ + 'user.profile.name': 'Alice', + 'user.profile.age': 30 + }); + }); + + it('handles null and undefined keys in Map', () => { + const map = new Map([ + [null, 'nullKey'], + [undefined, 'undefinedKey'] + ]); + const result = encodeAttributes({ myMap: map }); + expect(result.myMap).toEqual( + expect.arrayContaining([ + expect.objectContaining({ key: 'null', value: 'nullKey' }), + expect.objectContaining({ + key: 'undefined', + value: 'undefinedKey' + }) + ]) + ); + }); +}); diff --git a/packages/core/src/sdk/AttributesEncoding/__tests__/defaultEncoders.test.ts b/packages/core/src/sdk/AttributesEncoding/__tests__/defaultEncoders.test.ts new file mode 100644 index 000000000..88c0c0242 --- /dev/null +++ b/packages/core/src/sdk/AttributesEncoding/__tests__/defaultEncoders.test.ts @@ -0,0 +1,198 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +import { + stringEncoder, + numberEncoder, + booleanEncoder, + nullishEncoder, + arrayEncoder, + dateEncoder, + errorEncoder, + mapEncoder +} from '../defaultEncoders'; +import { warn } from '../utils'; + +jest.mock('../utils', () => ({ + ...jest.requireActual('../utils'), + warn: jest.fn() +})); + +describe('default encoders', () => { + beforeEach(() => { + (warn as jest.Mock).mockClear(); + }); + + describe('stringEncoder', () => { + it('encodes a string directly', () => { + expect(stringEncoder.check('foo')).toBe(true); + expect(stringEncoder.encode('foo')).toBe('foo'); + }); + it('rejects non-strings', () => { + expect(stringEncoder.check(123)).toBe(false); + }); + }); + + describe('numberEncoder', () => { + it('encodes finite numbers', () => { + expect(numberEncoder.check(42)).toBe(true); + expect(numberEncoder.encode(42)).toBe(42); + }); + it('drops NaN and Infinity', () => { + expect(numberEncoder.encode(NaN)).toBeUndefined(); + expect(numberEncoder.encode(Infinity)).toBeUndefined(); + }); + }); + + describe('booleanEncoder', () => { + it('encodes booleans directly', () => { + expect(booleanEncoder.check(true)).toBe(true); + expect(booleanEncoder.encode(true)).toBe(true); + expect(booleanEncoder.encode(false)).toBe(false); + }); + }); + + describe('nullishEncoder', () => { + it('encodes null and undefined directly', () => { + expect(nullishEncoder.check(null)).toBe(true); + expect(nullishEncoder.check(undefined)).toBe(true); + expect(nullishEncoder.encode(null)).toBeNull(); + expect(nullishEncoder.encode(undefined)).toBeUndefined(); + }); + it('rejects non-nullish values', () => { + expect(nullishEncoder.check('')).toBe(false); + }); + }); + + describe('arrayEncoder', () => { + it('encodes array of primitives', () => { + const result = arrayEncoder.encode([1, 'a', true]); + expect(result).toEqual([1, 'a', true]); + }); + it('encodes nested objects inside array', () => { + const result = arrayEncoder.encode([{ foo: 'bar' }]); + expect((result as Record[])[0]).toHaveProperty( + 'foo', + 'bar' + ); + }); + it('encodes nested arrays recursively', () => { + const result = arrayEncoder.encode([[1, 2], ['a']]); + expect(result).toEqual([[1, 2], ['a']]); + }); + }); + + describe('dateEncoder', () => { + it('encodes Date to string', () => { + const date = new Date('2020-01-01T00:00:00Z'); + expect(dateEncoder.check(date)).toBe(true); + expect(dateEncoder.encode(date)).toEqual(String(date)); + }); + it('rejects non-Date values', () => { + expect(dateEncoder.check('2020-01-01')).toBe(false); + }); + }); + + describe('errorEncoder', () => { + it('encodes Error with name, message, and stack', () => { + const error = new Error('boom'); + const result = errorEncoder.encode(error) as Record; + expect(result.name).toBe('Error'); + expect(result.message).toBe('boom'); + expect(result.stack).toContain('Error: boom'); + }); + + it('removes duplicate fields like stack', () => { + const err = { + message: 'fail' + } as Record; + + err.name = 'CustomError'; + err.stacktrace = 'custom-stack'; + err.stack = 'error-stacktrace'; + err.componentStack = 'component-stack'; + + const result = errorEncoder.encode(err) as Record; + expect(result.name).toBe('CustomError'); + expect(result.message).toBe('fail'); + expect(result.stack).toBe('custom-stack'); + expect(result).not.toHaveProperty('stacktrace'); + expect(result).toHaveProperty('componentStack'); + }); + + it('encodes error with cause', () => { + const cause = new Error('inner'); + const err: any = new Error('outer'); + err.cause = cause; + const result = errorEncoder.encode(err) as Record; + expect(result.cause).toBe(cause); + }); + }); + + describe('mapEncoder', () => { + it('encodes map with string keys', () => { + const map = new Map([ + ['a', 1], + ['b', 'str'] + ]); + const result = mapEncoder.encode(map); + expect(result).toEqual( + expect.arrayContaining([ + { key: 'a', keyType: 'string', value: 1 }, + { key: 'b', keyType: 'string', value: 'str' } + ]) + ); + }); + + it('encodes map with object key', () => { + const keyObj = { toString: () => 'objKey' }; + const map = new Map([[keyObj, 123]]); + const result = mapEncoder.encode(map); + expect((result as Record[])[0]).toHaveProperty( + 'key', + 'objKey' + ); + expect((result as Record[])[0]).toHaveProperty( + 'keyType', + 'object' + ); + }); + + it('encodes map with symbol key', () => { + const map = new Map([[Symbol('s'), 'val']]); + const result = mapEncoder.encode(map); + expect((result as Record[])[0].key).toContain( + 'Symbol(s)' + ); + expect((result as Record[])[0].keyType).toBe('symbol'); + }); + + it('encodes map with null and undefined keys', () => { + const map = new Map([ + [null, 'nullVal'], + [undefined, 'undefVal'] + ]); + const result = mapEncoder.encode(map); + expect(result).toEqual( + expect.arrayContaining([ + { key: 'null', keyType: 'object', value: 'nullVal' }, + { + key: 'undefined', + keyType: 'undefined', + value: 'undefVal' + } + ]) + ); + }); + + it('warns and drops unsupported key types', () => { + const map = new Map([[BigInt(1), 'big']]); + const result = mapEncoder.encode(map); + expect((result as Record[])[0].key).toBe('1'); // bigint stringified + expect(warn).not.toHaveBeenCalled(); // bigint is allowed + }); + }); +}); diff --git a/packages/core/src/sdk/AttributesEncoding/attributesEncoding.tsx b/packages/core/src/sdk/AttributesEncoding/attributesEncoding.tsx new file mode 100644 index 000000000..0f05d7e11 --- /dev/null +++ b/packages/core/src/sdk/AttributesEncoding/attributesEncoding.tsx @@ -0,0 +1,41 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +import { DdSdk } from '../DdSdk'; + +import { builtInEncoders } from './defaultEncoders'; +import { encodeAttributesInPlace } from './helpers'; +import type { Encodable } from './types'; +import { isPlainObject, warn } from './utils'; + +/** + * Encodes arbitrary input into a flat dictionary of attributes. + * - Objects are flattened using dot syntax. Max depth handling is done on the native layer by the + * Android and iOS SDKs. + * - We assume the input does not always conform to Record and we: + * - Fallback to { context: givenValue } if a primitive is passed + * - Apply built-in and consumer encoders to all values + * - Drop values of unsupported types + */ +export function encodeAttributes(input: unknown): Record { + const result: Record = {}; + const allEncoders = [...DdSdk.attributeEncoders, ...builtInEncoders]; + + if (isPlainObject(input)) { + for (const [k, v] of Object.entries(input)) { + encodeAttributesInPlace(v, result, [k], allEncoders); + } + } else { + // Fallback for primitive values passed as root + encodeAttributesInPlace(input, result, ['context'], allEncoders); + warn( + 'Warning: attributes root should be an object.\n' + + 'Received a primitive/array instead, which will be wrapped under the "context" key.' + ); + } + + return result; +} diff --git a/packages/core/src/sdk/AttributesEncoding/defaultEncoders.tsx b/packages/core/src/sdk/AttributesEncoding/defaultEncoders.tsx new file mode 100644 index 000000000..fdee006f1 --- /dev/null +++ b/packages/core/src/sdk/AttributesEncoding/defaultEncoders.tsx @@ -0,0 +1,185 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +/* ---------------------------------------- + * Built-in encoders + * -------------------------------------- */ +import { DdSdk } from '../DdSdk'; + +import { + getErrorMessage, + getErrorName, + getErrorStackTrace +} from './errorUtils'; +import { encodeAttributesInPlace, sanitizeForJson } from './helpers'; +import type { AttributeEncoder, Encodable } from './types'; +import { warn } from './utils'; + +/** Primitives: keep them explicit so the full pipeline is used uniformly. */ +export const stringEncoder: AttributeEncoder = { + check: (v): v is string => typeof v === 'string', + encode: v => v +}; + +export const numberEncoder: AttributeEncoder = { + check: (v): v is number => typeof v === 'number', + encode: v => (Number.isFinite(v) ? v : undefined) // drop non-finite +}; + +export const booleanEncoder: AttributeEncoder = { + check: (v): v is boolean => typeof v === 'boolean', + encode: v => v +}; + +export const nullishEncoder: AttributeEncoder = { + check: (v): v is null | undefined => v === null || v === undefined, + encode: v => v +}; + +/** + * Array encoder: + * - Sanitizes each item through the encoder pipeline. + * - Returns the sanitized array (it may later be flattened by the visitor if it still contains objects). + */ +export const arrayEncoder: AttributeEncoder = { + check: Array.isArray, + encode: (arr: unknown[]) => + arr.map(x => + sanitizeForJson(x, [...DdSdk.attributeEncoders, ...builtInEncoders]) + ) +}; + +/** + * Default Datadog Date Encoder. + * This does not make assumptions on format; uses String(date). + */ +export const dateEncoder: AttributeEncoder = { + check: (v: unknown): v is Date => v instanceof Date, + encode: (d: Date) => String(d) +}; +/* + } else if ('componentStack' in error) { + stack = String(error.componentStack); + } else if ( + 'sourceURL' in error && + 'line' in error && + 'column' in error + ) { + + +*/ +/** + * Extended Error Encoder. + * Serializes name, message, stack, and cause (ES2022+) for Error objects. + * If the error has other enumerable properties, they are included and sanitized. + */ +export const errorEncoder: AttributeEncoder = { + check: (v: unknown): v is Error => v instanceof Error, + encode: (e: any) => { + const extraAttributes: Record = {}; + + // In React Native, some errors have extra fields we want to capture + if (e && typeof e === 'object') { + const allEncoders = [ + ...DdSdk.attributeEncoders, + ...builtInEncoders + ]; + encodeAttributesInPlace(e, extraAttributes, [], allEncoders); + } + + // Remove fields that are duplicated in the dedicated fields below + if ('stacktrace' in e) { + delete extraAttributes['stacktrace']; + } else if ('stack' in e) { + delete extraAttributes['stack']; + } else if ('componentStack' in e) { + delete extraAttributes['componentStack']; + } + + return { + ...extraAttributes, + name: getErrorName(e), + message: getErrorMessage(e), + stack: getErrorStackTrace(e), + cause: (e as any).cause + }; + } +}; + +/** + * Map encoder: + * - Converts Map into an array of entries. + * - Each entry is { key: string, keyType: string, value: Encodable }. + * - Keys are stringified with type info to reduce collision risk. + * - Entries with un-stringifiable keys are dropped (with a warning). + */ +export const mapEncoder: AttributeEncoder> = { + check: (v: unknown): v is Map => v instanceof Map, + encode: (map: Map) => { + const entries: Encodable[] = []; + + for (const [k, v] of map.entries()) { + try { + const keyType = typeof k; + let keyStr: string; + + if (k === null) { + keyStr = 'null'; + } else if (k === undefined) { + keyStr = 'undefined'; + } else if ( + keyType === 'string' || + keyType === 'number' || + keyType === 'boolean' || + keyType === 'bigint' + ) { + keyStr = String(k); + } else if (typeof k === 'symbol') { + keyStr = k.description + ? `Symbol(${k.description})` + : 'Symbol'; + } else if (typeof k === 'object' || typeof k === 'function') { + // Try to get a descriptive form + if (typeof (k as any).toString === 'function') { + keyStr = (k as any).toString(); + } else { + keyStr = Object.prototype.toString.call(k); // e.g. "[object Object]" + } + } else { + warn( + `Dropping Map entry: unsupported key type "${keyType}".` + ); + continue; + } + + const allEncoders = [ + ...DdSdk.attributeEncoders, + ...builtInEncoders + ]; + entries.push({ + key: keyStr, + keyType, + value: sanitizeForJson(v, allEncoders) + }); + } catch (err) { + warn(`Failed to encode Map key: ${k}. ERROR: ${String(err)}`); + } + } + + return entries; + } +}; + +export const builtInEncoders = [ + stringEncoder, + numberEncoder, + booleanEncoder, + nullishEncoder, + arrayEncoder, + dateEncoder, + errorEncoder, + mapEncoder +]; diff --git a/packages/core/src/utils/errorUtils.ts b/packages/core/src/sdk/AttributesEncoding/errorUtils.tsx similarity index 56% rename from packages/core/src/utils/errorUtils.ts rename to packages/core/src/sdk/AttributesEncoding/errorUtils.tsx index 5fc16dcbc..b8692959b 100644 --- a/packages/core/src/utils/errorUtils.ts +++ b/packages/core/src/sdk/AttributesEncoding/errorUtils.tsx @@ -3,25 +3,9 @@ * This product includes software developed at Datadog (https://www.datadoghq.com/). * Copyright 2016-Present Datadog, Inc. */ - -import { debugId } from '../metro/debugIdResolver'; - -export const EMPTY_MESSAGE = 'Unknown Error'; -export const EMPTY_STACK_TRACE = ''; -export const DEFAULT_ERROR_NAME = 'Error'; - -export const getErrorMessage = (error: any | undefined): string => { - let message = EMPTY_MESSAGE; - if (error === undefined || error === null) { - message = EMPTY_MESSAGE; - } else if (typeof error === 'object' && 'message' in error) { - message = String(error.message); - } else { - message = String(error); - } - - return message; -}; +export const ERROR_EMPTY_STACKTRACE = ''; +export const ERROR_EMPTY_MESSAGE = 'Unknown Error'; +export const ERROR_DEFAULT_NAME = 'Error'; /** * Will extract the stack from the error, taking the first key found among: @@ -32,13 +16,13 @@ export const getErrorMessage = (error: any | undefined): string => { * generate a stack from this information. */ export const getErrorStackTrace = (error: any | undefined): string => { - let stack = EMPTY_STACK_TRACE; + let stack = ERROR_EMPTY_STACKTRACE; try { if (error === undefined || error === null) { - stack = EMPTY_STACK_TRACE; + stack = ERROR_EMPTY_STACKTRACE; } else if (typeof error === 'string') { - stack = EMPTY_STACK_TRACE; + stack = ERROR_EMPTY_STACKTRACE; } else if (typeof error === 'object') { if ('stacktrace' in error) { stack = String(error.stacktrace); @@ -60,10 +44,52 @@ export const getErrorStackTrace = (error: any | undefined): string => { return stack; }; +export const getErrorMessage = (error: any | undefined): string => { + if (error == null) { + return ERROR_EMPTY_MESSAGE; + } + + // If it's an actual Error (or subclass) + if (error instanceof Error) { + // Prefer .message if defined, otherwise fallback to .toString() + return error.message || error.toString() || ERROR_EMPTY_MESSAGE; + } + + // If it's an object with a message property (not necessarily Error) + if ( + typeof error === 'object' && + 'message' in error && + typeof (error as any).message === 'string' + ) { + return (error as any).message || ERROR_EMPTY_MESSAGE; + } + + // If it’s a primitive (string, number, boolean, symbol) + if ( + typeof error === 'string' || + typeof error === 'number' || + typeof error === 'boolean' || + typeof error === 'symbol' + ) { + return String(error); + } + + // If it has its own toString (not the default Object one) + if ( + typeof error?.toString === 'function' && + error.toString !== Object.prototype.toString + ) { + return error.toString(); + } + + // Fallback + return ERROR_EMPTY_MESSAGE; +}; + export const getErrorName = (error: unknown): string => { try { if (typeof error !== 'object' || error === null) { - return DEFAULT_ERROR_NAME; + return ERROR_DEFAULT_NAME; } if (typeof (error as any).name === 'string') { return (error as any).name; @@ -71,16 +97,5 @@ export const getErrorName = (error: unknown): string => { } catch (e) { // Do nothing } - return DEFAULT_ERROR_NAME; -}; - -export const getErrorContext = (originalContext: any): Record => { - const _debugId = debugId; - if (!_debugId) { - return originalContext; - } - return { - ...originalContext, - '_dd.debug_id': _debugId - }; + return ERROR_DEFAULT_NAME; }; diff --git a/packages/core/src/sdk/AttributesEncoding/helpers.tsx b/packages/core/src/sdk/AttributesEncoding/helpers.tsx new file mode 100644 index 000000000..3636be9e1 --- /dev/null +++ b/packages/core/src/sdk/AttributesEncoding/helpers.tsx @@ -0,0 +1,134 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +import type { AttributeEncoder, Encodable } from './types'; +import { formatPathForLog, isPlainObject, warn } from './utils'; + +/** + * Recursive in-place encoder: flattens values into `out` dictionary. + * Never applies "context", that's only for the root. + */ +export function encodeAttributesInPlace( + input: unknown, + out: Record, + path: string[], + encoders: AttributeEncoder[] +): void { + const value = applyEncoders(input, encoders); + + // Nullish / primitive + if ( + value === null || + value === undefined || + typeof value === 'string' || + typeof value === 'number' || + typeof value === 'boolean' + ) { + out[path.join('.')] = value; + return; + } + + // Arrays + if (Array.isArray(value)) { + const normalize = (x: unknown): Encodable => { + const v = applyEncoders(x, encoders); + + // Primitive / nullish are fine + if ( + v === null || + v === undefined || + typeof v === 'string' || + typeof v === 'number' || + typeof v === 'boolean' + ) { + return v; + } + + if (isPlainObject(v)) { + const nested: Record = {}; + encodeAttributesInPlace(v, nested, [], encoders); + return nested; + } + + if (Array.isArray(v)) { + return v.map(normalize); + } + + // Unsupported + warn( + `Dropped unsupported value in array at '${formatPathForLog( + path + )}': ${String(v)}` + ); + return undefined; + }; + + out[path.join('.')] = value + .map(normalize) + .filter(item => item !== undefined); // drop unsupported + return; + } + + // Plain object + if (isPlainObject(value)) { + for (const [k, v] of Object.entries(value)) { + encodeAttributesInPlace(v, out, [...path, k], encoders); + } + return; + } + + // Unsupported + warn( + `Dropped unsupported value at '${formatPathForLog(path)}': ${String( + value + )}` + ); +} + +/** + * Sanitize unknown input to be JSON-serializable using encoders. + * This does not flatten—it's a pure sanitization pass (used by some encoders). + */ +export function sanitizeForJson( + value: unknown, + encoders: AttributeEncoder[] +): Encodable { + const v = applyEncoders(value, encoders); + + // If still a plain object, sanitize shallowly + if (isPlainObject(v)) { + const out: Record = {}; + for (const [k, val] of Object.entries(v)) { + encodeAttributesInPlace(val, out, [k], encoders); + } + return out; + } + + // If array, sanitize items + if (Array.isArray(v)) { + return v.map(item => sanitizeForJson(item, encoders)); + } + + return v; +} + +export function applyEncoders( + value: unknown, + encoders: AttributeEncoder[] +): Encodable { + for (const enc of encoders) { + try { + if (enc.check(value)) { + return enc.encode(value as never); + } + } catch (err) { + warn(`Encoder error: ${String(err)}`); + return undefined; + } + } + // Not matched by any encoder; leave as-is for the visitor to decide + return value as Encodable; +} diff --git a/packages/core/src/sdk/AttributesEncoding/types.tsx b/packages/core/src/sdk/AttributesEncoding/types.tsx new file mode 100644 index 000000000..1f136d576 --- /dev/null +++ b/packages/core/src/sdk/AttributesEncoding/types.tsx @@ -0,0 +1,33 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +/** + * A value that can safely be encoded as a Datadog Attribute. + */ +export type Encodable = + | string + | number + | boolean + | null + | undefined + | Encodable[] + | { [key: string]: Encodable }; + +export type AttributeKey = string; +export type AttributeValue = Encodable; +export type EncodableAttributes = Record; +export type Attributes = Record; + +/** + * Encoders define how to handle special object types (Date, Error, Map, etc.). + * Each encoder is: + * - check: decides if the value can be handled by this encoder. + * - encode: converts the value into an Encodable. + */ +export interface AttributeEncoder { + check: (value: unknown) => boolean; + encode: (value: T) => Encodable; +} diff --git a/packages/core/src/sdk/AttributesEncoding/utils.tsx b/packages/core/src/sdk/AttributesEncoding/utils.tsx new file mode 100644 index 000000000..11e3adfdf --- /dev/null +++ b/packages/core/src/sdk/AttributesEncoding/utils.tsx @@ -0,0 +1,31 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +import { InternalLog } from '../../InternalLog'; +import { SdkVerbosity } from '../../SdkVerbosity'; + +export function warn(text: string) { + InternalLog.log(`[ATTRIBUTES] ${text}`, SdkVerbosity.WARN); +} + +export function isPlainObject(v: unknown): v is Record { + return !!v && typeof v === 'object' && (v as any).constructor === Object; +} + +/** + * Utility: format a path array into dot/bracket notation (for logs). + */ +export function formatPathForLog(path: (string | number)[]): string { + return path + .map((segment, index) => { + const s = String(segment); + if (/^\d/.test(s)) { + return `[${s}]`; + } + return index === 0 ? s : `.${s}`; + }) + .join(''); +} diff --git a/packages/core/src/sdk/DatadogProvider/Buffer/BoundedBuffer.ts b/packages/core/src/sdk/DatadogProvider/Buffer/BoundedBuffer.ts index dc91a475f..93f9389b0 100644 --- a/packages/core/src/sdk/DatadogProvider/Buffer/BoundedBuffer.ts +++ b/packages/core/src/sdk/DatadogProvider/Buffer/BoundedBuffer.ts @@ -6,8 +6,8 @@ import { InternalLog } from '../../../InternalLog'; import { SdkVerbosity } from '../../../SdkVerbosity'; -import { DdSdk } from '../../../sdk/DdSdk'; -import { getErrorStackTrace } from '../../../utils/errorUtils'; +import { getErrorStackTrace } from '../../AttributesEncoding/errorUtils'; +import { NativeDdSdk } from '../../DdSdkInternal'; import { DatadogBuffer } from './DatadogBuffer'; @@ -206,7 +206,7 @@ export class BoundedBuffer extends DatadogBuffer { private drainTelemetry = () => { Object.values(this.telemetryBuffer).forEach( ({ message, stack, kind, occurrences }) => { - DdSdk.telemetryError( + NativeDdSdk.telemetryError( `${message} happened ${occurrences} times.`, stack, kind diff --git a/packages/core/src/sdk/DdSdk.ts b/packages/core/src/sdk/DdSdk.ts deleted file mode 100644 index b086d96bd..000000000 --- a/packages/core/src/sdk/DdSdk.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. - * This product includes software developed at Datadog (https://www.datadoghq.com/). - * Copyright 2016-Present Datadog, Inc. - */ - -import type { DdNativeSdkType } from '../nativeModulesTypes'; - -// eslint-disable-next-line global-require, @typescript-eslint/no-var-requires -const DdSdk: DdNativeSdkType = require('../specs/NativeDdSdk').default; - -export { DdSdk }; diff --git a/packages/core/src/sdk/DdSdk.tsx b/packages/core/src/sdk/DdSdk.tsx new file mode 100644 index 000000000..a829707e3 --- /dev/null +++ b/packages/core/src/sdk/DdSdk.tsx @@ -0,0 +1,15 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ +import { getGlobalInstance } from '../utils/singletonUtils'; + +import { DdSdkWrapper } from './DdSdkInternal'; +import type { DdSdkType } from './DdSdkInternal'; + +const CORE_MODULE = 'com.datadog.reactnative.core'; +export const DdSdk = getGlobalInstance( + CORE_MODULE, + () => new DdSdkWrapper() +) as DdSdkType; diff --git a/packages/core/src/sdk/DdSdkInternal.tsx b/packages/core/src/sdk/DdSdkInternal.tsx new file mode 100644 index 000000000..7af33d2e3 --- /dev/null +++ b/packages/core/src/sdk/DdSdkInternal.tsx @@ -0,0 +1,105 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +import type { + DdNativeSdkConfiguration, + DdNativeSdkType +} from '../nativeModulesTypes'; + +import type { AttributeEncoder } from './AttributesEncoding/types'; + +// eslint-disable-next-line global-require, @typescript-eslint/no-var-requires +const NativeDdSdk: DdNativeSdkType = require('../specs/NativeDdSdk').default; + +export type DdSdkType = { + readonly attributeEncoders: AttributeEncoder[]; + + /** + * Initializes Datadog's features. + * @param configuration: The configuration to use. + */ + initialize(configuration: DdNativeSdkConfiguration): Promise; +}; + +export class DdSdkWrapper implements DdNativeSdkType { + get attributeEncoders(): AttributeEncoder[] { + return this._attributeEncoders; + } + private _attributeEncoders: AttributeEncoder[] = []; + + initialize(configuration: DdNativeSdkConfiguration): Promise { + this._attributeEncoders = [...configuration.attributeEncoders]; + return NativeDdSdk.initialize(configuration); + } + + getConstants() { + return NativeDdSdk.getConstants(); + } + + setAttributes(attributes: object): Promise { + return NativeDdSdk.setAttributes(attributes); + } + + setUserInfo(user: object): Promise { + return NativeDdSdk.setUserInfo(user); + } + + clearUserInfo(): Promise { + return NativeDdSdk.clearUserInfo(); + } + + addUserExtraInfo(extraInfo: object): Promise { + return NativeDdSdk.addUserExtraInfo(extraInfo); + } + + setTrackingConsent(trackingConsent: string): Promise { + return NativeDdSdk.setTrackingConsent(trackingConsent); + } + + sendTelemetryLog( + message: string, + attributes: object, + config: object + ): Promise { + return NativeDdSdk.sendTelemetryLog(message, attributes, config); + } + + telemetryDebug(message: string): Promise { + return NativeDdSdk.telemetryDebug(message); + } + + telemetryError( + message: string, + stack: string, + kind: string + ): Promise { + return NativeDdSdk.telemetryError(message, stack, kind); + } + + consumeWebviewEvent(message: string): Promise { + return NativeDdSdk.consumeWebviewEvent(message); + } + + clearAllData(): Promise { + return NativeDdSdk.clearAllData(); + } + + addListener(eventType: string): void { + return NativeDdSdk.addListener(eventType); + } + + removeListeners(count: number): void { + return NativeDdSdk.removeListeners(count); + } + + _setAttributeEncodersForTesting( + attributeEncoders: AttributeEncoder[] + ) { + this._attributeEncoders = [...attributeEncoders]; + } +} + +export { NativeDdSdk }; diff --git a/packages/core/src/sdk/EventMappers/EventMapper.ts b/packages/core/src/sdk/EventMappers/EventMapper.ts index e1cbaae19..0f3878fe1 100644 --- a/packages/core/src/sdk/EventMappers/EventMapper.ts +++ b/packages/core/src/sdk/EventMappers/EventMapper.ts @@ -6,11 +6,11 @@ import { InternalLog } from '../../InternalLog'; import { SdkVerbosity } from '../../SdkVerbosity'; -import { DdSdk } from '../../sdk/DdSdk'; import { AccountInfoSingleton } from '../AccountInfoSingleton/AccountInfoSingleton'; import type { AccountInfo } from '../AccountInfoSingleton/types'; import { AttributesSingleton } from '../AttributesSingleton/AttributesSingleton'; import type { Attributes } from '../AttributesSingleton/types'; +import { NativeDdSdk } from '../DdSdkInternal'; import { UserInfoSingleton } from '../UserInfoSingleton/UserInfoSingleton'; import type { UserInfo } from '../UserInfoSingleton/types'; @@ -92,7 +92,7 @@ export class EventMapper { )}: ${error}`, SdkVerbosity.WARN ); - DdSdk.telemetryDebug('Error while running the event mapper'); + NativeDdSdk.telemetryDebug('Error while running the event mapper'); return this.formatMapperEventForNative(backupEvent, backupEvent); } }; diff --git a/packages/core/src/trace/DdTrace.ts b/packages/core/src/trace/DdTrace.ts index 119716914..8c57a6ed2 100644 --- a/packages/core/src/trace/DdTrace.ts +++ b/packages/core/src/trace/DdTrace.ts @@ -7,12 +7,12 @@ import { InternalLog } from '../InternalLog'; import { SdkVerbosity } from '../SdkVerbosity'; import type { DdNativeTraceType } from '../nativeModulesTypes'; +import { encodeAttributes } from '../sdk/AttributesEncoding/attributesEncoding'; import { bufferNativeCallReturningId, bufferNativeCallWithId } from '../sdk/DatadogProvider/Buffer/bufferNativeCall'; import type { DdTraceType } from '../types'; -import { validateContext } from '../utils/argsUtils'; import { getGlobalInstance } from '../utils/singletonUtils'; import { DefaultTimeProvider } from '../utils/time-provider/DefaultTimeProvider'; @@ -33,7 +33,7 @@ class DdTraceWrapper implements DdTraceType { const spanId = bufferNativeCallReturningId(() => this.nativeTrace.startSpan( operation, - validateContext(context), + encodeAttributes(context), timestampMs ) ); @@ -54,7 +54,7 @@ class DdTraceWrapper implements DdTraceType { id => this.nativeTrace.finishSpan( id, - validateContext(context), + encodeAttributes(context), timestampMs ), spanId diff --git a/packages/core/src/types.tsx b/packages/core/src/types.tsx index cd697c04b..6865e2a41 100644 --- a/packages/core/src/types.tsx +++ b/packages/core/src/types.tsx @@ -5,6 +5,7 @@ */ import type { BatchProcessingLevel } from './DdSdkReactNativeConfiguration'; +import type { AttributeEncoder } from './sdk/AttributesEncoding/types'; declare global { // eslint-disable-next-line no-var, vars-on-top @@ -70,7 +71,8 @@ export class DdSdkConfiguration { readonly trackWatchdogTerminations: boolean | undefined, readonly batchProcessingLevel: BatchProcessingLevel, // eslint-disable-next-line no-empty-function readonly initialResourceThreshold: number | undefined, - readonly trackMemoryWarnings: boolean + readonly trackMemoryWarnings: boolean, + readonly attributeEncoders: AttributeEncoder[] ) {} } @@ -224,7 +226,6 @@ export type LogEvent = { export type LogEventMapper = (logEvent: LogEvent) => LogEvent | null; // DdRum - export enum ErrorSource { NETWORK = 'NETWORK', SOURCE = 'SOURCE', diff --git a/packages/core/src/utils/argsUtils.ts b/packages/core/src/utils/argsUtils.ts deleted file mode 100644 index d7a07255f..000000000 --- a/packages/core/src/utils/argsUtils.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* eslint-disable @typescript-eslint/ban-ts-comment */ -/* - * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. - * This product includes software developed at Datadog (https://www.datadoghq.com/). - * Copyright 2016-Present Datadog, Inc. - */ - -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 {}; -}; diff --git a/packages/react-native-apollo-client/src/helpers.ts b/packages/react-native-apollo-client/src/helpers.ts index c7f9a435a..9812bd547 100644 --- a/packages/react-native-apollo-client/src/helpers.ts +++ b/packages/react-native-apollo-client/src/helpers.ts @@ -22,7 +22,7 @@ export const getVariables = (operation: Operation): string | null => { try { return JSON.stringify(operation.variables); } catch (e) { - DdSdk?.telemetryError( + (DdSdk as any)?.telemetryError( _getErrorMessage( ErrorCode.GQL_VARIABLE_RETRIEVAL_ERROR, apolloVersion @@ -61,7 +61,7 @@ export const getOperationType = ( })[0] || null ); } catch (e) { - DdSdk?.telemetryError( + (DdSdk as any)?.telemetryError( _getErrorMessage(ErrorCode.GQL_OPERATION_TYPE_ERROR, apolloVersion), _getErrorStack(e), ErrorCode.GQL_OPERATION_TYPE_ERROR From 7005a3b9631f6deecd80b3e49b695da85d681cf3 Mon Sep 17 00:00:00 2001 From: Marco Saia Date: Fri, 10 Oct 2025 11:32:05 +0200 Subject: [PATCH 104/410] Fixed existing tests --- .../src/__tests__/DdSdkReactNative.test.tsx | 65 ++-- .../DdSdkReactNativeConfiguration.test.ts | 3 + .../DdRumErrorTracking.test.tsx | 132 ++++++++- .../DdRumUserInteractionTracking.test.tsx | 17 +- .../core/src/logs/__tests__/DdLogs.test.ts | 68 ++--- packages/core/src/rum/__tests__/DdRum.test.ts | 54 ++-- .../__tests__/__utils__/XMLHttpRequestMock.ts | 67 +++-- .../XHRProxy/__tests__/XHRProxy.test.ts | 277 +++++++++++------- .../__tests__/DdSdkNativeBridge.test.tsx | 2 +- .../Buffer/__tests__/BoundedBuffer.test.ts | 10 +- .../__tests__/initialization.test.tsx | 1 + .../__tests__/EventMapper.test.ts | 6 +- .../__tests__/FileBasedConfiguration.test.ts | 2 + .../core/src/trace/__tests__/DdTrace.test.ts | 13 +- .../src/utils/__tests__/argsUtils.test.ts | 66 ----- .../src/utils/__tests__/errorUtils.test.ts | 2 +- 16 files changed, 468 insertions(+), 317 deletions(-) delete mode 100644 packages/core/src/utils/__tests__/argsUtils.test.ts diff --git a/packages/core/src/__tests__/DdSdkReactNative.test.tsx b/packages/core/src/__tests__/DdSdkReactNative.test.tsx index 5e6f8c447..b6ecfdf7a 100644 --- a/packages/core/src/__tests__/DdSdkReactNative.test.tsx +++ b/packages/core/src/__tests__/DdSdkReactNative.test.tsx @@ -19,7 +19,7 @@ import { DdRumUserInteractionTracking } from '../rum/instrumentation/interaction import { DdRumResourceTracking } from '../rum/instrumentation/resourceTracking/DdRumResourceTracking'; import { PropagatorType, RumActionType } from '../rum/types'; import { AttributesSingleton } from '../sdk/AttributesSingleton/AttributesSingleton'; -import { DdSdk } from '../sdk/DdSdk'; +import { NativeDdSdk } from '../sdk/DdSdkInternal'; import { GlobalState } from '../sdk/GlobalState/GlobalState'; import { UserInfoSingleton } from '../sdk/UserInfoSingleton/UserInfoSingleton'; import { ErrorSource } from '../types'; @@ -428,7 +428,9 @@ describe('DdSdkReactNative', () => { const ddSdkConfiguration = NativeModules.DdSdk.initialize.mock .calls[0][0] as DdSdkConfiguration; expect( - ddSdkConfiguration.additionalConfiguration['_dd.version'] + (ddSdkConfiguration.additionalConfiguration as { + '_dd.version': string; + })['_dd.version'] ).toBe('2.0.0'); }); @@ -451,10 +453,14 @@ describe('DdSdkReactNative', () => { const ddSdkConfiguration = NativeModules.DdSdk.initialize.mock .calls[0][0] as DdSdkConfiguration; expect( - ddSdkConfiguration.additionalConfiguration['_dd.version'] + (ddSdkConfiguration.additionalConfiguration as { + '_dd.version': string; + })['_dd.version'] ).toBeUndefined(); expect( - ddSdkConfiguration.additionalConfiguration['_dd.version_suffix'] + (ddSdkConfiguration.additionalConfiguration as { + '_dd.version_suffix': string; + })['_dd.version_suffix'] ).toBe('-codepush-3'); }); @@ -478,10 +484,14 @@ describe('DdSdkReactNative', () => { const ddSdkConfiguration = NativeModules.DdSdk.initialize.mock .calls[0][0] as DdSdkConfiguration; expect( - ddSdkConfiguration.additionalConfiguration['_dd.version'] + (ddSdkConfiguration.additionalConfiguration as { + '_dd.version': string; + })['_dd.version'] ).toBe('2.0.0-codepush-3'); expect( - ddSdkConfiguration.additionalConfiguration['_dd.version_suffix'] + (ddSdkConfiguration.additionalConfiguration as { + '_dd.version_suffix': string; + })['_dd.version_suffix'] ).toBeUndefined(); }); @@ -1056,8 +1066,10 @@ describe('DdSdkReactNative', () => { await DdSdkReactNative.addAttribute(key, value); // THEN - expect(DdSdk.addAttribute).toHaveBeenCalledTimes(1); - expect(DdSdk.addAttribute).toHaveBeenCalledWith(key, { value }); + expect(NativeDdSdk.addAttribute).toHaveBeenCalledTimes(1); + expect(NativeDdSdk.addAttribute).toHaveBeenCalledWith(key, { + value + }); expect(AttributesSingleton.getInstance().getAttribute(key)).toEqual( value ); @@ -1075,8 +1087,8 @@ describe('DdSdkReactNative', () => { await DdSdkReactNative.removeAttribute(key); // THEN - expect(DdSdk.removeAttribute).toHaveBeenCalledTimes(1); - expect(DdSdk.removeAttribute).toHaveBeenCalledWith(key); + expect(NativeDdSdk.removeAttribute).toHaveBeenCalledTimes(1); + expect(NativeDdSdk.removeAttribute).toHaveBeenCalledWith(key); expect(AttributesSingleton.getInstance().getAttribute(key)).toEqual( undefined ); @@ -1093,8 +1105,8 @@ describe('DdSdkReactNative', () => { await DdSdkReactNative.addAttributes(attributes); // THEN - expect(DdSdk.addAttributes).toHaveBeenCalledTimes(1); - expect(DdSdk.addAttributes).toHaveBeenCalledWith(attributes); + expect(NativeDdSdk.addAttributes).toHaveBeenCalledTimes(1); + expect(NativeDdSdk.addAttributes).toHaveBeenCalledWith(attributes); expect(AttributesSingleton.getInstance().getAttributes()).toEqual({ foo: 'bar' }); @@ -1111,8 +1123,11 @@ describe('DdSdkReactNative', () => { await DdSdkReactNative.removeAttributes(['foo', 'baz']); // THEN - expect(DdSdk.removeAttributes).toHaveBeenCalledTimes(1); - expect(DdSdk.removeAttributes).toHaveBeenCalledWith(['foo', 'baz']); + expect(NativeDdSdk.removeAttributes).toHaveBeenCalledTimes(1); + expect(NativeDdSdk.removeAttributes).toHaveBeenCalledWith([ + 'foo', + 'baz' + ]); expect(AttributesSingleton.getInstance().getAttributes()).toEqual( {} ); @@ -1135,8 +1150,8 @@ describe('DdSdkReactNative', () => { await DdSdkReactNative.setUserInfo(userInfo); // THEN - expect(DdSdk.setUserInfo).toHaveBeenCalledTimes(1); - expect(DdSdk.setUserInfo).toHaveBeenCalledWith(userInfo); + expect(NativeDdSdk.setUserInfo).toHaveBeenCalledTimes(1); + expect(NativeDdSdk.setUserInfo).toHaveBeenCalledWith(userInfo); expect(UserInfoSingleton.getInstance().getUserInfo()).toEqual( userInfo ); @@ -1156,8 +1171,10 @@ describe('DdSdkReactNative', () => { await DdSdkReactNative.addUserExtraInfo(extraInfo); // THEN - expect(DdSdk.addUserExtraInfo).toHaveBeenCalledTimes(1); - expect(DdSdk.addUserExtraInfo).toHaveBeenCalledWith(extraInfo); + expect(NativeDdSdk.addUserExtraInfo).toHaveBeenCalledTimes(1); + expect(NativeDdSdk.addUserExtraInfo).toHaveBeenCalledWith( + extraInfo + ); expect(UserInfoSingleton.getInstance().getUserInfo()).toEqual({ id: 'id', extraInfo: { @@ -1186,8 +1203,8 @@ describe('DdSdkReactNative', () => { await DdSdkReactNative.clearUserInfo(); // THEN - expect(DdSdk.clearUserInfo).toHaveBeenCalledTimes(1); - expect(DdSdk.setUserInfo).toHaveBeenCalled(); + expect(NativeDdSdk.clearUserInfo).toHaveBeenCalledTimes(1); + expect(NativeDdSdk.setUserInfo).toHaveBeenCalled(); expect(UserInfoSingleton.getInstance().getUserInfo()).toEqual( undefined ); @@ -1204,8 +1221,10 @@ describe('DdSdkReactNative', () => { DdSdkReactNative.setTrackingConsent(consent); // THEN - expect(DdSdk.setTrackingConsent).toHaveBeenCalledTimes(1); - expect(DdSdk.setTrackingConsent).toHaveBeenCalledWith(consent); + expect(NativeDdSdk.setTrackingConsent).toHaveBeenCalledTimes(1); + expect(NativeDdSdk.setTrackingConsent).toHaveBeenCalledWith( + consent + ); }); }); @@ -1215,7 +1234,7 @@ describe('DdSdkReactNative', () => { DdSdkReactNative.clearAllData(); // THEN - expect(DdSdk.clearAllData).toHaveBeenCalledTimes(1); + expect(NativeDdSdk.clearAllData).toHaveBeenCalledTimes(1); }); }); diff --git a/packages/core/src/__tests__/DdSdkReactNativeConfiguration.test.ts b/packages/core/src/__tests__/DdSdkReactNativeConfiguration.test.ts index 60a9d13ff..d13b00df3 100644 --- a/packages/core/src/__tests__/DdSdkReactNativeConfiguration.test.ts +++ b/packages/core/src/__tests__/DdSdkReactNativeConfiguration.test.ts @@ -34,6 +34,7 @@ describe('DdSdkReactNativeConfiguration', () => { "actionEventMapper": null, "additionalConfiguration": {}, "applicationId": "fake-app-id", + "attributeEncoders": [], "batchProcessingLevel": "MEDIUM", "batchSize": "MEDIUM", "bundleLogsWithRum": true, @@ -134,6 +135,7 @@ describe('DdSdkReactNativeConfiguration', () => { "additionalField": "fake-value", }, "applicationId": "fake-app-id", + "attributeEncoders": [], "batchProcessingLevel": "MEDIUM", "batchSize": "LARGE", "bundleLogsWithRum": true, @@ -222,6 +224,7 @@ describe('DdSdkReactNativeConfiguration', () => { "actionEventMapper": null, "additionalConfiguration": {}, "applicationId": "", + "attributeEncoders": [], "batchProcessingLevel": "MEDIUM", "batchSize": "MEDIUM", "bundleLogsWithRum": false, diff --git a/packages/core/src/__tests__/rum/instrumentation/DdRumErrorTracking.test.tsx b/packages/core/src/__tests__/rum/instrumentation/DdRumErrorTracking.test.tsx index fc34de5b0..cfa6b9014 100644 --- a/packages/core/src/__tests__/rum/instrumentation/DdRumErrorTracking.test.tsx +++ b/packages/core/src/__tests__/rum/instrumentation/DdRumErrorTracking.test.tsx @@ -18,13 +18,13 @@ let baseErrorHandlerCalled = false; const baseErrorHandler = (error: any, isFatal?: boolean) => { baseErrorHandlerCalled = true; }; -let originalErrorHandler; +let originalErrorHandler: any; let baseConsoleErrorCalled = false; -const baseConsoleError = (...params: unknown) => { +const baseConsoleError = (...params: unknown[]) => { baseConsoleErrorCalled = true; }; -let originalConsoleError; +let originalConsoleError: any; const flushPromises = () => new Promise(jest.requireActual('timers').setImmediate); @@ -61,11 +61,14 @@ it('M intercept and send a RUM event W onGlobalError() {no message}', async () = // THEN expect(DdRum.addError).toHaveBeenCalledTimes(1); expect(DdRum.addError).toHaveBeenCalledWith( - '[object Object]', + 'Unknown Error', 'SOURCE', 'doSomething() at ./path/to/file.js:67:3', { - '_dd.error.raw': error, + '_dd.error.raw.name': 'Error', + '_dd.error.raw.message': 'Unknown Error', + '_dd.error.raw.cause': undefined, + '_dd.error.raw.stack': 'doSomething() at ./path/to/file.js:67:3', '_dd.error.is_crash': is_fatal, '_dd.error.source_type': 'react-native' }, @@ -94,7 +97,10 @@ it('M intercept and send a RUM event W onGlobalError() {empty stack trace}', asy 'SOURCE', '', { - '_dd.error.raw': error, + '_dd.error.raw.name': 'Error', + '_dd.error.raw.message': 'Something bad happened', + '_dd.error.raw.cause': undefined, + '_dd.error.raw.stack': '', '_dd.error.is_crash': is_fatal, '_dd.error.source_type': 'react-native' }, @@ -121,7 +127,10 @@ it('M intercept and send a RUM event W onGlobalError() {Error object}', async () 'SOURCE', expect.stringContaining('Error: Something bad happened'), { - '_dd.error.raw': error, + '_dd.error.raw.name': error.name, + '_dd.error.raw.message': error.message, + '_dd.error.raw.stack': error.stack, + '_dd.error.raw.cause': undefined, '_dd.error.is_crash': is_fatal, '_dd.error.source_type': 'react-native' }, @@ -155,7 +164,10 @@ it('M intercept and send a RUM event W onGlobalError() {CustomError object}', as 'SOURCE', expect.stringContaining('Error: Something bad happened'), { - '_dd.error.raw': error, + '_dd.error.raw.name': error.name, + '_dd.error.raw.message': error.message, + '_dd.error.raw.stack': error.stack, + '_dd.error.raw.cause': undefined, '_dd.error.is_crash': is_fatal, '_dd.error.source_type': 'react-native' }, @@ -190,9 +202,15 @@ it('M intercept and send a RUM event W onGlobalError() {with source file info}', 'SOURCE', 'at ./path/to/file.js:1038:57', { - '_dd.error.raw': error, '_dd.error.is_crash': is_fatal, - '_dd.error.source_type': 'react-native' + '_dd.error.source_type': 'react-native', + '_dd.error.raw.sourceURL': './path/to/file.js', + '_dd.error.raw.line': 1038, + '_dd.error.raw.column': 57, + '_dd.error.raw.message': 'Something bad happened', + '_dd.error.raw.name': 'Error', + '_dd.error.raw.cause': undefined, + '_dd.error.raw.stack': 'at ./path/to/file.js:1038:57' }, expect.any(Number), '' @@ -224,7 +242,63 @@ it('M intercept and send a RUM event W onGlobalError() {with component stack}', 'SOURCE', 'doSomething() at ./path/to/file.js:67:3,nestedCall() at ./path/to/file.js:1064:9,root() at ./path/to/index.js:10:1', { - '_dd.error.raw': error, + '_dd.error.raw.message': 'Something bad happened', + '_dd.error.raw.name': 'Error', + '_dd.error.raw.stack': [ + 'doSomething() at ./path/to/file.js:67:3', + 'nestedCall() at ./path/to/file.js:1064:9', + 'root() at ./path/to/index.js:10:1' + ].join(','), + '_dd.error.raw.cause': undefined, + '_dd.error.is_crash': is_fatal, + '_dd.error.source_type': 'react-native' + }, + expect.any(Number), + '' + ); + expect(baseErrorHandlerCalled).toStrictEqual(true); +}); + +it('M intercept and send a RUM event W onGlobalError() {with stack and component stack}', async () => { + // GIVEN + DdRumErrorTracking.startTracking(); + const is_fatal = Math.random() < 0.5; + const error = { + stack: [ + 'example() at ./path/to/file.js:77:2', + 'test() at ./path/to/index.js:22:3' + ], + componentStack: [ + 'doSomething() at ./path/to/file.js:67:3', + 'nestedCall() at ./path/to/file.js:1064:9', + 'root() at ./path/to/index.js:10:1' + ], + message: 'Something bad happened' + }; + + // WHEN + DdRumErrorTracking.onGlobalError(error, is_fatal); + await flushPromises(); + + // THEN + expect(DdRum.addError).toHaveBeenCalledTimes(1); + expect(DdRum.addError).toHaveBeenCalledWith( + 'Something bad happened', + 'SOURCE', + 'example() at ./path/to/file.js:77:2,test() at ./path/to/index.js:22:3', + { + '_dd.error.raw.message': 'Something bad happened', + '_dd.error.raw.name': 'Error', + '_dd.error.raw.stack': [ + 'example() at ./path/to/file.js:77:2', + 'test() at ./path/to/index.js:22:3' + ].join(','), + '_dd.error.raw.componentStack': [ + 'doSomething() at ./path/to/file.js:67:3', + 'nestedCall() at ./path/to/file.js:1064:9', + 'root() at ./path/to/index.js:10:1' + ], + '_dd.error.raw.cause': undefined, '_dd.error.is_crash': is_fatal, '_dd.error.source_type': 'react-native' }, @@ -258,7 +332,14 @@ it('M intercept and send a RUM event W onGlobalError() {with stack}', async () = 'SOURCE', 'doSomething() at ./path/to/file.js:67:3,nestedCall() at ./path/to/file.js:1064:9,root() at ./path/to/index.js:10:1', { - '_dd.error.raw': error, + '_dd.error.raw.name': 'Error', + '_dd.error.raw.message': 'Something bad happened', + '_dd.error.raw.cause': undefined, + '_dd.error.raw.stack': [ + 'doSomething() at ./path/to/file.js:67:3', + 'nestedCall() at ./path/to/file.js:1064:9', + 'root() at ./path/to/index.js:10:1' + ].join(','), '_dd.error.is_crash': is_fatal, '_dd.error.source_type': 'react-native' }, @@ -292,7 +373,14 @@ it('M intercept and send a RUM event W onGlobalError() {with stacktrace}', async 'SOURCE', 'doSomething() at ./path/to/file.js:67:3,nestedCall() at ./path/to/file.js:1064:9,root() at ./path/to/index.js:10:1', { - '_dd.error.raw': error, + '_dd.error.raw.name': 'Error', + '_dd.error.raw.message': 'Something bad happened', + '_dd.error.raw.stack': [ + 'doSomething() at ./path/to/file.js:67:3', + 'nestedCall() at ./path/to/file.js:1064:9', + 'root() at ./path/to/index.js:10:1' + ].join(','), + '_dd.error.raw.cause': undefined, '_dd.error.is_crash': is_fatal, '_dd.error.source_type': 'react-native' }, @@ -332,7 +420,14 @@ it('M not report error in console handler W onGlobalError() {with console report 'SOURCE', 'doSomething() at ./path/to/file.js:67:3,nestedCall() at ./path/to/file.js:1064:9,root() at ./path/to/index.js:10:1', { - '_dd.error.raw': error, + '_dd.error.raw.name': 'Error', + '_dd.error.raw.cause': undefined, + '_dd.error.raw.message': 'Something bad happened', + '_dd.error.raw.stack': [ + 'doSomething() at ./path/to/file.js:67:3', + 'nestedCall() at ./path/to/file.js:1064:9', + 'root() at ./path/to/index.js:10:1' + ].join(','), '_dd.error.is_crash': is_fatal, '_dd.error.source_type': 'react-native' }, @@ -483,7 +578,10 @@ describe.each([ const errorMessage = message === undefined || message === null ? 'Unknown Error' - : String(message); + : typeof message?.toString === 'function' && + message.toString !== Object.prototype.toString + ? String(message) + : 'Unknown Error'; expect(DdRum.addError).toHaveBeenCalledTimes(1); expect(DdRum.addError).toHaveBeenCalledWith( errorMessage, @@ -517,7 +615,9 @@ it('M intercept and send a RUM event W on error() {called from RNErrorHandler}', 'SOURCE', expect.stringContaining('Error: Something bad happened'), { - '_dd.error.raw': error, + '_dd.error.raw.name': error.name, + '_dd.error.raw.message': error.message, + '_dd.error.raw.stack': error.stack, '_dd.error.is_crash': is_fatal, '_dd.error.source_type': 'react-native' }, diff --git a/packages/core/src/__tests__/rum/instrumentation/DdRumUserInteractionTracking.test.tsx b/packages/core/src/__tests__/rum/instrumentation/DdRumUserInteractionTracking.test.tsx index d94d9bb82..6ee9e9c71 100644 --- a/packages/core/src/__tests__/rum/instrumentation/DdRumUserInteractionTracking.test.tsx +++ b/packages/core/src/__tests__/rum/instrumentation/DdRumUserInteractionTracking.test.tsx @@ -21,7 +21,7 @@ import React from 'react'; import type { DdNativeRumType } from '../../../nativeModulesTypes'; import { DdRumUserInteractionTracking } from '../../../rum/instrumentation/interactionTracking/DdRumUserInteractionTracking'; import { BufferSingleton } from '../../../sdk/DatadogProvider/Buffer/BufferSingleton'; -import { DdSdk } from '../../../sdk/DdSdk'; +import { NativeDdSdk } from '../../../sdk/DdSdkInternal'; const styles = StyleSheet.create({ button: { @@ -311,7 +311,7 @@ describe('startTracking memoization', () => { // GIVEN DdRumUserInteractionTracking.startTracking({}); let rendersCount = 0; - const DummyComponent = props => { + const DummyComponent = (props: { onPress: () => void }) => { rendersCount++; return ( @@ -343,7 +343,7 @@ describe('startTracking memoization', () => { // GIVEN DdRumUserInteractionTracking.startTracking({}); let rendersCount = 0; - const DummyComponent = props => { + const DummyComponent = (props: { title: string }) => { rendersCount++; return ( @@ -370,7 +370,10 @@ describe('startTracking memoization', () => { // GIVEN DdRumUserInteractionTracking.startTracking({}); let rendersCount = 0; - const DummyComponent = props => { + const DummyComponent = (props: { + onPress: () => void; + title: string; + }) => { rendersCount++; return ( @@ -410,7 +413,7 @@ describe('startTracking memoization', () => { // GIVEN DdRumUserInteractionTracking.startTracking({}); let rendersCount = 0; - const DummyComponent = props => { + const DummyComponent = (props: { onPress: () => void }) => { rendersCount++; return ( @@ -456,7 +459,7 @@ describe('startTracking', () => { jest.setMock('react/jsx-runtime', {}); DdRumUserInteractionTracking.startTracking({}); expect(DdRumUserInteractionTracking['isTracking']).toBe(true); - expect(DdSdk.telemetryDebug).toBeCalledWith( + expect(NativeDdSdk.telemetryDebug).toBeCalledWith( 'React jsx runtime does not export new jsx transform' ); }); @@ -466,7 +469,7 @@ describe('startTracking', () => { DdRumUserInteractionTracking.startTracking({}); expect(DdRumUserInteractionTracking['isTracking']).toBe(true); - expect(DdSdk.telemetryDebug).toBeCalledWith( + expect(NativeDdSdk.telemetryDebug).toBeCalledWith( 'React version does not support new jsx transform' ); }); diff --git a/packages/core/src/logs/__tests__/DdLogs.test.ts b/packages/core/src/logs/__tests__/DdLogs.test.ts index f7e848a0b..bfd252d82 100644 --- a/packages/core/src/logs/__tests__/DdLogs.test.ts +++ b/packages/core/src/logs/__tests__/DdLogs.test.ts @@ -12,7 +12,7 @@ import { InternalLog } from '../../InternalLog'; import { SdkVerbosity } from '../../SdkVerbosity'; import type { DdNativeLogsType } from '../../nativeModulesTypes'; import { ErrorSource } from '../../types'; -import type { LogEventMapper } from '../../types'; +import type { LogEventMapper, LogEvent } from '../../types'; import { DdLogs } from '../DdLogs'; jest.mock('../../InternalLog', () => { @@ -38,7 +38,7 @@ describe('DdLogs', () => { context: { newContext: 'context' }, status: 'info', userInfo: {} - }; + } as LogEvent; }; DdLogs.registerLogEventMapper(logEventMapper); @@ -506,7 +506,7 @@ describe('DdLogs', () => { 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, + 2, expect.anything(), SdkVerbosity.WARN ); @@ -516,12 +516,12 @@ describe('DdLogs', () => { }); it('native context is empty W context is raw type', async () => { - const obj: any = 123; + const obj: any = Symbol('invalid-context'); await DdLogs.debug('message', obj); expect(InternalLog.log).toHaveBeenNthCalledWith( - 1, + 2, expect.anything(), - SdkVerbosity.ERROR + SdkVerbosity.WARN ); expect(NativeModules.DdLogs.debug).toHaveBeenCalledWith( 'message', @@ -549,7 +549,7 @@ describe('DdLogs', () => { 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, + 2, expect.anything(), SdkVerbosity.WARN ); @@ -559,12 +559,12 @@ describe('DdLogs', () => { }); it('native context is empty W context is raw type', async () => { - const obj: any = 123; + const obj: any = Symbol('invalid-context'); await DdLogs.warn('message', obj); expect(InternalLog.log).toHaveBeenNthCalledWith( - 1, + 2, expect.anything(), - SdkVerbosity.ERROR + SdkVerbosity.WARN ); expect(NativeModules.DdLogs.warn).toHaveBeenCalledWith( 'message', @@ -592,7 +592,7 @@ describe('DdLogs', () => { 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, + 2, expect.anything(), SdkVerbosity.WARN ); @@ -602,12 +602,12 @@ describe('DdLogs', () => { }); it('native context is empty W context is raw type', async () => { - const obj: any = 123; + const obj: any = Symbol('invalid-context'); await DdLogs.info('message', obj); expect(InternalLog.log).toHaveBeenNthCalledWith( - 1, + 2, expect.anything(), - SdkVerbosity.ERROR + SdkVerbosity.WARN ); expect(NativeModules.DdLogs.info).toHaveBeenCalledWith( 'message', @@ -635,7 +635,7 @@ describe('DdLogs', () => { 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, + 2, expect.anything(), SdkVerbosity.WARN ); @@ -645,12 +645,12 @@ describe('DdLogs', () => { }); it('native context is empty W context is raw type', async () => { - const obj: any = 123; + const obj: any = Symbol('invalid-context'); await DdLogs.error('message', obj); expect(InternalLog.log).toHaveBeenNthCalledWith( - 1, + 2, expect.anything(), - SdkVerbosity.ERROR + SdkVerbosity.WARN ); expect(NativeModules.DdLogs.error).toHaveBeenCalledWith( 'message', @@ -701,7 +701,7 @@ describe('DdLogs', () => { ]); expect(InternalLog.log).toHaveBeenNthCalledWith( - 1, + 2, expect.anything(), SdkVerbosity.WARN ); @@ -721,7 +721,7 @@ describe('DdLogs', () => { }); it('native context is empty W context is raw type', async () => { - const obj: any = 123; + const obj: any = Symbol('invalid-context'); await DdLogs.debug( 'message', 'kind', @@ -731,9 +731,9 @@ describe('DdLogs', () => { ); expect(InternalLog.log).toHaveBeenNthCalledWith( - 1, + 2, expect.anything(), - SdkVerbosity.ERROR + SdkVerbosity.WARN ); expect( @@ -791,7 +791,7 @@ describe('DdLogs', () => { ]); expect(InternalLog.log).toHaveBeenNthCalledWith( - 1, + 2, expect.anything(), SdkVerbosity.WARN ); @@ -809,7 +809,7 @@ describe('DdLogs', () => { }); it('native context is empty W context is raw type', async () => { - const obj: any = 123; + const obj: any = Symbol('invalid-context'); await DdLogs.warn( 'message', 'kind', @@ -819,9 +819,9 @@ describe('DdLogs', () => { ); expect(InternalLog.log).toHaveBeenNthCalledWith( - 1, + 2, expect.anything(), - SdkVerbosity.ERROR + SdkVerbosity.WARN ); expect( @@ -879,7 +879,7 @@ describe('DdLogs', () => { ]); expect(InternalLog.log).toHaveBeenNthCalledWith( - 1, + 2, expect.anything(), SdkVerbosity.WARN ); @@ -897,7 +897,7 @@ describe('DdLogs', () => { }); it('native context is empty W context is raw type', async () => { - const obj: any = 123; + const obj: any = Symbol('invalid-context'); await DdLogs.info( 'message', 'kind', @@ -907,9 +907,9 @@ describe('DdLogs', () => { ); expect(InternalLog.log).toHaveBeenNthCalledWith( - 1, + 2, expect.anything(), - SdkVerbosity.ERROR + SdkVerbosity.WARN ); expect( @@ -967,7 +967,7 @@ describe('DdLogs', () => { ]); expect(InternalLog.log).toHaveBeenNthCalledWith( - 1, + 2, expect.anything(), SdkVerbosity.WARN ); @@ -987,7 +987,7 @@ describe('DdLogs', () => { }); it('native context is empty W context is raw type', async () => { - const obj: any = 123; + const obj: any = Symbol('invalid-context'); await DdLogs.error( 'message', 'kind', @@ -997,9 +997,9 @@ describe('DdLogs', () => { ); expect(InternalLog.log).toHaveBeenNthCalledWith( - 1, + 2, expect.anything(), - SdkVerbosity.ERROR + SdkVerbosity.WARN ); expect( diff --git a/packages/core/src/rum/__tests__/DdRum.test.ts b/packages/core/src/rum/__tests__/DdRum.test.ts index e4318322b..e1c699fe1 100644 --- a/packages/core/src/rum/__tests__/DdRum.test.ts +++ b/packages/core/src/rum/__tests__/DdRum.test.ts @@ -10,7 +10,7 @@ import { NativeModules } from 'react-native'; import { InternalLog } from '../../InternalLog'; import { SdkVerbosity } from '../../SdkVerbosity'; import { BufferSingleton } from '../../sdk/DatadogProvider/Buffer/BufferSingleton'; -import { DdSdk } from '../../sdk/DdSdk'; +import { NativeDdSdk } from '../../sdk/DdSdkInternal'; import { GlobalState } from '../../sdk/GlobalState/GlobalState'; import { ErrorSource } from '../../types'; import { DdRum } from '../DdRum'; @@ -69,13 +69,13 @@ describe('DdRum', () => { }); test('uses empty context with error when context is invalid or null', async () => { - const context: any = 123; + const context: any = Symbol('invalid-context'); await DdRum.startView('key', 'name', context); expect(InternalLog.log).toHaveBeenNthCalledWith( 2, expect.anything(), - SdkVerbosity.ERROR + SdkVerbosity.WARN ); expect(NativeModules.DdRum.startView).toHaveBeenCalledWith( @@ -122,7 +122,7 @@ describe('DdRum', () => { }); test('uses empty context with error when context is invalid or null', async () => { - const context: any = 123; + const context: any = Symbol('invalid-context'); await DdRum.startView('key', 'name'); await DdRum.stopView('key', context); @@ -130,7 +130,7 @@ describe('DdRum', () => { expect(InternalLog.log).toHaveBeenNthCalledWith( 3, expect.anything(), - SdkVerbosity.ERROR + SdkVerbosity.WARN ); expect(NativeModules.DdRum.stopView).toHaveBeenCalledWith( @@ -177,13 +177,13 @@ describe('DdRum', () => { }); test('uses empty context with error when context is invalid or null', async () => { - const context: any = 123; + const context: any = Symbol('invalid-context'); await DdRum.startAction(RumActionType.SCROLL, 'name', context); expect(InternalLog.log).toHaveBeenNthCalledWith( 2, expect.anything(), - SdkVerbosity.ERROR + SdkVerbosity.WARN ); expect(NativeModules.DdRum.startAction).toHaveBeenCalledWith( @@ -236,7 +236,7 @@ describe('DdRum', () => { }); test('uses empty context with error when context is invalid or null', async () => { - const context: any = 123; + const context: any = Symbol('invalid-context'); await DdRum.startAction(RumActionType.SCROLL, 'name'); await DdRum.stopAction( @@ -248,7 +248,7 @@ describe('DdRum', () => { expect(InternalLog.log).toHaveBeenNthCalledWith( 3, expect.anything(), - SdkVerbosity.ERROR + SdkVerbosity.WARN ); expect(NativeModules.DdRum.stopAction).toHaveBeenCalledWith( @@ -353,14 +353,14 @@ describe('DdRum', () => { }); test('uses empty context with error when context is invalid or null', async () => { - const context: any = 123; + const context: any = Symbol('invalid-context'); await DdRum.startResource('key', 'method', 'url', context); expect(InternalLog.log).toHaveBeenNthCalledWith( - 2, + 3, expect.anything(), - SdkVerbosity.ERROR + SdkVerbosity.WARN ); expect(NativeModules.DdRum.startResource).toHaveBeenCalledWith( @@ -414,15 +414,15 @@ describe('DdRum', () => { }); test('uses empty context with error when context is invalid or null', async () => { - const context: any = 123; + const context: any = Symbol('invalid-context'); await DdRum.startResource('key', 'method', 'url', {}); await DdRum.stopResource('key', 200, 'other', -1, context); expect(InternalLog.log).toHaveBeenNthCalledWith( - 2, + 3, expect.anything(), - SdkVerbosity.ERROR + SdkVerbosity.WARN ); expect(NativeModules.DdRum.stopResource).toHaveBeenCalledWith( @@ -442,7 +442,7 @@ describe('DdRum', () => { await DdRum.stopResource('key', 200, 'other', -1, context); expect(InternalLog.log).toHaveBeenNthCalledWith( - 2, + 3, expect.anything(), SdkVerbosity.WARN ); @@ -1200,13 +1200,13 @@ describe('DdRum', () => { }); test('uses empty context with error when context is invalid or null', async () => { - const context: any = 123; + const context: any = Symbol('invalid-context'); await DdRum.addAction(RumActionType.SCROLL, 'name', context); expect(InternalLog.log).toHaveBeenNthCalledWith( - 1, + 2, expect.anything(), - SdkVerbosity.ERROR + SdkVerbosity.WARN ); expect(NativeModules.DdRum.addAction).toHaveBeenCalledWith( @@ -1222,7 +1222,7 @@ describe('DdRum', () => { await DdRum.addAction(RumActionType.SCROLL, 'name', context); expect(InternalLog.log).toHaveBeenNthCalledWith( - 1, + 2, expect.anything(), SdkVerbosity.WARN ); @@ -1264,7 +1264,7 @@ describe('DdRum', () => { }); test('uses empty context with error when context is invalid or null', async () => { - const context: any = 123; + const context: any = Symbol('invalid-context'); await DdRum.addError( 'error', ErrorSource.CUSTOM, @@ -1273,9 +1273,9 @@ describe('DdRum', () => { ); expect(InternalLog.log).toHaveBeenNthCalledWith( - 1, + 2, expect.anything(), - SdkVerbosity.ERROR + SdkVerbosity.WARN ); expect(NativeModules.DdRum.addError).toHaveBeenCalledWith( @@ -1300,7 +1300,7 @@ describe('DdRum', () => { ); expect(InternalLog.log).toHaveBeenNthCalledWith( - 1, + 2, expect.anything(), SdkVerbosity.WARN ); @@ -1349,7 +1349,7 @@ describe('DdRum', () => { test('does not call the native SDK when startAction has not been called before and using old API', async () => { await DdRum.stopAction({ user: 'me' }, 789); expect(NativeModules.DdRum.stopAction).not.toHaveBeenCalled(); - expect(DdSdk.telemetryDebug).not.toHaveBeenCalled(); + expect(NativeDdSdk.telemetryDebug).not.toHaveBeenCalled(); }); test('calls the native SDK when called with old API', async () => { @@ -1361,7 +1361,7 @@ describe('DdRum', () => { { user: 'me' }, 789 ); - expect(DdSdk.telemetryDebug).toHaveBeenCalledWith( + expect(NativeDdSdk.telemetryDebug).toHaveBeenCalledWith( 'DDdRum.stopAction called with the old signature' ); }); @@ -1375,7 +1375,7 @@ describe('DdRum', () => { {}, 456 ); - expect(DdSdk.telemetryDebug).toHaveBeenCalledWith( + expect(NativeDdSdk.telemetryDebug).toHaveBeenCalledWith( 'DDdRum.stopAction called with the old signature' ); }); diff --git a/packages/core/src/rum/instrumentation/resourceTracking/__tests__/__utils__/XMLHttpRequestMock.ts b/packages/core/src/rum/instrumentation/resourceTracking/__tests__/__utils__/XMLHttpRequestMock.ts index a5725fbf4..7683b63f9 100644 --- a/packages/core/src/rum/instrumentation/resourceTracking/__tests__/__utils__/XMLHttpRequestMock.ts +++ b/packages/core/src/rum/instrumentation/resourceTracking/__tests__/__utils__/XMLHttpRequestMock.ts @@ -12,7 +12,7 @@ export class XMLHttpRequestMock implements XMLHttpRequest { static readonly DONE = 4; public response: any; - public responseType: XMLHttpRequestResponseType; + public responseType: XMLHttpRequestResponseType = ''; public status: number = 0; public readyState: number = XMLHttpRequestMock.UNSENT; public requestHeaders: Map = new Map(); @@ -20,29 +20,50 @@ export class XMLHttpRequestMock implements XMLHttpRequest { // eslint-disable-next-line no-empty-function constructor() {} - responseText: string; - responseURL: string; - responseXML: Document; - statusText: string; - timeout: number; - upload: XMLHttpRequestUpload; - withCredentials: boolean; + responseText: string = ''; + responseURL: string = ''; + responseXML: Document = {} as Document; + statusText: string = ''; + timeout: number = -1; + upload: XMLHttpRequestUpload = {} as XMLHttpRequestUpload; + withCredentials: boolean = false; getAllResponseHeaders = jest.fn(); overrideMimeType = jest.fn(); - DONE: number; - HEADERS_RECEIVED: number; - LOADING: number; - OPENED: number; - UNSENT: number; + DONE = 4 as const; + HEADERS_RECEIVED = 2 as const; + LOADING = 3 as const; + OPENED = 1 as const; + UNSENT = 0 as const; addEventListener = jest.fn(); removeEventListener = jest.fn(); - onabort: (this: XMLHttpRequest, ev: ProgressEvent) => any; - onerror: (this: XMLHttpRequest, ev: ProgressEvent) => any; - onload: (this: XMLHttpRequest, ev: ProgressEvent) => any; - onloadend: (this: XMLHttpRequest, ev: ProgressEvent) => any; - onloadstart: (this: XMLHttpRequest, ev: ProgressEvent) => any; - onprogress: (this: XMLHttpRequest, ev: ProgressEvent) => any; - ontimeout: (this: XMLHttpRequest, ev: ProgressEvent) => any; + onabort: ( + this: XMLHttpRequest, + ev: ProgressEvent + ) => any = ev => {}; + onerror: ( + this: XMLHttpRequest, + ev: ProgressEvent + ) => any = ev => {}; + onload: ( + this: XMLHttpRequest, + ev: ProgressEvent + ) => any = ev => {}; + onloadend: ( + this: XMLHttpRequest, + ev: ProgressEvent + ) => any = ev => {}; + onloadstart: ( + this: XMLHttpRequest, + ev: ProgressEvent + ) => any = ev => {}; + onprogress: ( + this: XMLHttpRequest, + ev: ProgressEvent + ) => any = ev => {}; + ontimeout: ( + this: XMLHttpRequest, + ev: ProgressEvent + ) => any = ev => {}; dispatchEvent(event: Event): boolean { throw new Error('Method not implemented.'); } @@ -85,14 +106,14 @@ export class XMLHttpRequestMock implements XMLHttpRequest { } setRequestHeader(header: string, value: string): void { - this.requestHeaders[header] = value; + this.requestHeaders.set(header, value); } setResponseHeader(header: string, value: string): void { - this.responseHeaders[header] = value; + this.responseHeaders.set(header, value); } getResponseHeader(header: string): string | null { - return this.responseHeaders[header]; + return this.responseHeaders.get(header) ?? null; } } diff --git a/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/__tests__/XHRProxy.test.ts b/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/__tests__/XHRProxy.test.ts index a18825771..6599dea08 100644 --- a/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/__tests__/XHRProxy.test.ts +++ b/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/__tests__/XHRProxy.test.ts @@ -62,7 +62,7 @@ function randomInt(max: number): number { const flushPromises = () => new Promise(jest.requireActual('timers').setImmediate); -let xhrProxy; +let xhrProxy: any; const hexToDecimal = (hex: string): string => { return BigInt(hex, 16).toString(10); @@ -76,6 +76,9 @@ beforeEach(() => { xhrProxy = new XHRProxy({ xhrType: XMLHttpRequestMock, resourceReporter: new ResourceReporter([]) + } as { + xhrType: typeof XMLHttpRequest; + resourceReporter: ResourceReporter; }); // we need this because with ms precision between Date.now() calls we can get 0, so we advance @@ -236,17 +239,19 @@ describe('XHRProxy', () => { await flushPromises(); // THEN - const spanId = xhr.requestHeaders[PARENT_ID_HEADER_KEY]; + const spanId = xhr.requestHeaders.get(PARENT_ID_HEADER_KEY); expect(spanId).toBeDefined(); expect(spanId).toMatch(/[1-9].+/); - const traceId = xhr.requestHeaders[TRACE_ID_HEADER_KEY]; + const traceId = xhr.requestHeaders.get(TRACE_ID_HEADER_KEY); expect(traceId).toBeDefined(); expect(traceId).toMatch(/[1-9].+/); expect(traceId !== spanId).toBeTruthy(); - expect(xhr.requestHeaders[SAMPLING_PRIORITY_HEADER_KEY]).toBe('1'); - expect(xhr.requestHeaders[ORIGIN_HEADER_KEY]).toBe(ORIGIN_RUM); + expect(xhr.requestHeaders.get(SAMPLING_PRIORITY_HEADER_KEY)).toBe( + '1' + ); + expect(xhr.requestHeaders.get(ORIGIN_HEADER_KEY)).toBe(ORIGIN_RUM); }); it('does not generate spanId and traceId in request headers when no first party hosts are provided', async () => { @@ -267,8 +272,10 @@ describe('XHRProxy', () => { await flushPromises(); // THEN - expect(xhr.requestHeaders[TRACE_ID_HEADER_KEY]).toBeUndefined(); - expect(xhr.requestHeaders[PARENT_ID_HEADER_KEY]).toBeUndefined(); + expect(xhr.requestHeaders.get(TRACE_ID_HEADER_KEY)).toBeUndefined(); + expect( + xhr.requestHeaders.get(PARENT_ID_HEADER_KEY) + ).toBeUndefined(); }); it('does not generate spanId and traceId in request headers when the url does not match first party hosts', async () => { @@ -298,8 +305,10 @@ describe('XHRProxy', () => { await flushPromises(); // THEN - expect(xhr.requestHeaders[TRACE_ID_HEADER_KEY]).toBeUndefined(); - expect(xhr.requestHeaders[PARENT_ID_HEADER_KEY]).toBeUndefined(); + expect(xhr.requestHeaders.get(TRACE_ID_HEADER_KEY)).toBeUndefined(); + expect( + xhr.requestHeaders.get(PARENT_ID_HEADER_KEY) + ).toBeUndefined(); }); it('does not crash when provided URL is not a valid one', async () => { @@ -325,8 +334,10 @@ describe('XHRProxy', () => { await flushPromises(); // THEN - expect(xhr.requestHeaders[TRACE_ID_HEADER_KEY]).toBeUndefined(); - expect(xhr.requestHeaders[PARENT_ID_HEADER_KEY]).toBeUndefined(); + expect(xhr.requestHeaders.get(TRACE_ID_HEADER_KEY)).toBeUndefined(); + expect( + xhr.requestHeaders.get(PARENT_ID_HEADER_KEY) + ).toBeUndefined(); }); it('generates spanId and traceId with 0 sampling priority in request headers when trace is not sampled', async () => { @@ -352,12 +363,16 @@ describe('XHRProxy', () => { await flushPromises(); // THEN - expect(xhr.requestHeaders[TRACE_ID_HEADER_KEY]).not.toBeUndefined(); expect( - xhr.requestHeaders[PARENT_ID_HEADER_KEY] + xhr.requestHeaders.get(TRACE_ID_HEADER_KEY) ).not.toBeUndefined(); - expect(xhr.requestHeaders[SAMPLING_PRIORITY_HEADER_KEY]).toBe('0'); - expect(xhr.requestHeaders[ORIGIN_HEADER_KEY]).toBe(ORIGIN_RUM); + expect( + xhr.requestHeaders.get(PARENT_ID_HEADER_KEY) + ).not.toBeUndefined(); + expect(xhr.requestHeaders.get(SAMPLING_PRIORITY_HEADER_KEY)).toBe( + '0' + ); + expect(xhr.requestHeaders.get(ORIGIN_HEADER_KEY)).toBe(ORIGIN_RUM); }); it('does not origin as RUM in the request headers when startTracking() + XHR.open() + XHR.send()', async () => { @@ -378,7 +393,7 @@ describe('XHRProxy', () => { await flushPromises(); // THEN - expect(xhr.requestHeaders[ORIGIN_HEADER_KEY]).toBeUndefined(); + expect(xhr.requestHeaders.get(ORIGIN_HEADER_KEY)).toBeUndefined(); }); it('forces the agent to keep the request generated trace when startTracking() + XHR.open() + XHR.send()', async () => { @@ -404,7 +419,9 @@ describe('XHRProxy', () => { await flushPromises(); // THEN - expect(xhr.requestHeaders[SAMPLING_PRIORITY_HEADER_KEY]).toBe('1'); + expect(xhr.requestHeaders.get(SAMPLING_PRIORITY_HEADER_KEY)).toBe( + '1' + ); }); it('forces the agent to discard the request generated trace when startTracking when the request is not traced', async () => { @@ -430,7 +447,9 @@ describe('XHRProxy', () => { await flushPromises(); // THEN - expect(xhr.requestHeaders[SAMPLING_PRIORITY_HEADER_KEY]).toBe('0'); + expect(xhr.requestHeaders.get(SAMPLING_PRIORITY_HEADER_KEY)).toBe( + '0' + ); }); it('adds tracecontext request headers when the host is instrumented with tracecontext and request is sampled', async () => { @@ -460,14 +479,16 @@ describe('XHRProxy', () => { await flushPromises(); // THEN - const contextHeader = xhr.requestHeaders[TRACECONTEXT_HEADER_KEY]; + const contextHeader = xhr.requestHeaders.get( + TRACECONTEXT_HEADER_KEY + ); expect(contextHeader).toMatch( /^00-[0-9a-f]{8}[0]{8}[0-9a-f]{16}-[0-9a-f]{16}-01$/ ); // Parent value of the context header is the 3rd part of it - const parentValue = contextHeader.split('-')[2]; - const stateHeader = xhr.requestHeaders[TRACESTATE_HEADER_KEY]; + const parentValue = contextHeader?.split('-')[2]; + const stateHeader = xhr.requestHeaders.get(TRACESTATE_HEADER_KEY); expect(stateHeader).toBe(`dd=s:1;o:rum;p:${parentValue}`); }); @@ -510,15 +531,19 @@ describe('XHRProxy', () => { /* ================================================================================= * Verify that the trace id in the traceparent header is a 128 bit trace ID (hex). * ================================================================================= */ - const traceparentHeader = - xhr.requestHeaders[TRACECONTEXT_HEADER_KEY]; - const traceparentTraceId = traceparentHeader.split('-')[1]; + const traceparentHeader = xhr.requestHeaders.get( + TRACECONTEXT_HEADER_KEY + ); + const traceparentTraceId = traceparentHeader?.split('-')[1]; expect(traceparentTraceId).toMatch( /^[0-9a-f]{8}[0]{8}[0-9a-f]{16}$/ ); expect( - TracingIdentifierUtils.isWithin128Bits(traceparentTraceId, 16) + TracingIdentifierUtils.isWithin128Bits( + traceparentTraceId as string, + 16 + ) ); /* ========================================================================= @@ -526,18 +551,20 @@ describe('XHRProxy', () => { * ========================================================================= */ // x-datadog-trace-id is a decimal representing the low 64 bits of the 128 bits Trace ID - const xDatadogTraceId = xhr.requestHeaders[TRACE_ID_HEADER_KEY]; + const xDatadogTraceId = xhr.requestHeaders.get(TRACE_ID_HEADER_KEY); - expect(TracingIdentifierUtils.isWithin64Bits(xDatadogTraceId)); + expect( + TracingIdentifierUtils.isWithin64Bits(xDatadogTraceId as string) + ); /* =============================================================== * Verify that the trace id in x-datadog-tags headers is HEX 16. * =============================================================== */ // x-datadog-tags is a HEX 16 contains the high 64 bits of the 128 bits Trace ID - const xDatadogTagsTraceId = xhr.requestHeaders[ - TAGS_HEADER_KEY - ].split('=')[1]; + const xDatadogTagsTraceId = xhr.requestHeaders + ?.get(TAGS_HEADER_KEY) + ?.split('=')[1] as string; expect(xDatadogTagsTraceId).toMatch(/^[a-f0-9]{16}$/); expect( @@ -548,8 +575,8 @@ describe('XHRProxy', () => { * Verify that the trace id in the b3 header is a 128 bit trace ID (hex). * ========================================================================= */ - const b3Header = xhr.requestHeaders[B3_HEADER_KEY]; - const b3TraceId = b3Header.split('-')[0]; + const b3Header = xhr.requestHeaders.get(B3_HEADER_KEY); + const b3TraceId = b3Header?.split('-')[0] as string; expect(b3TraceId).toMatch(/^[0-9a-f]{8}[0]{8}[0-9a-f]{16}$/); expect(TracingIdentifierUtils.isWithin128Bits(b3TraceId, 16)); @@ -558,7 +585,9 @@ describe('XHRProxy', () => { * Verify that the trace id in the X-B3-TraceId header is a 128 bit trace ID (hex). * ================================================================================= */ - const xB3TraceId = xhr.requestHeaders[B3_MULTI_TRACE_ID_HEADER_KEY]; + const xB3TraceId = xhr.requestHeaders.get( + B3_MULTI_TRACE_ID_HEADER_KEY + ) as string; expect(xB3TraceId).toMatch(/^[0-9a-f]{8}[0]{8}[0-9a-f]{16}$/); expect(TracingIdentifierUtils.isWithin128Bits(xB3TraceId, 16)); @@ -601,18 +630,21 @@ describe('XHRProxy', () => { // THEN // x-datadog-trace-id is just the low 64 bits (DECIMAL) - const datadogLowTraceValue = - xhr.requestHeaders[TRACE_ID_HEADER_KEY]; + const datadogLowTraceValue = xhr.requestHeaders.get( + TRACE_ID_HEADER_KEY + ); // We convert the low 64 bits to HEX - const datadogLowTraceValueHex = `${BigInt(datadogLowTraceValue) + const datadogLowTraceValueHex = `${BigInt( + datadogLowTraceValue as string + ) .toString(16) .padStart(16, '0')}`; // The high 64 bits are expressed in x-datadog-tags (HEX) - const datadogHighTraceValueHex = xhr.requestHeaders[ - TAGS_HEADER_KEY - ].split('=')[1]; // High HEX 64 bits + const datadogHighTraceValueHex = xhr.requestHeaders + ?.get(TAGS_HEADER_KEY) + ?.split('=')[1] as string; // High HEX 64 bits // We re-compose the full 128 bit trace-id by joining the strings const datadogTraceValue128BitHex = `${datadogHighTraceValueHex}${datadogLowTraceValueHex}`; @@ -622,18 +654,24 @@ describe('XHRProxy', () => { datadogTraceValue128BitHex ); - const datadogParentValue = xhr.requestHeaders[PARENT_ID_HEADER_KEY]; - const contextHeader = xhr.requestHeaders[TRACECONTEXT_HEADER_KEY]; - const traceContextValue = contextHeader.split('-')[1]; - const parentContextValue = contextHeader.split('-')[2]; - const b3MultiTraceHeader = - xhr.requestHeaders[B3_MULTI_TRACE_ID_HEADER_KEY]; - const b3MultiParentHeader = - xhr.requestHeaders[B3_MULTI_SPAN_ID_HEADER_KEY]; - - const b3Header = xhr.requestHeaders[B3_HEADER_KEY]; - const traceB3Value = b3Header.split('-')[0]; - const parentB3Value = b3Header.split('-')[1]; + const datadogParentValue = xhr.requestHeaders.get( + PARENT_ID_HEADER_KEY + ); + const contextHeader = xhr.requestHeaders.get( + TRACECONTEXT_HEADER_KEY + ); + const traceContextValue = contextHeader?.split('-')[1] as string; + const parentContextValue = contextHeader?.split('-')[2] as string; + const b3MultiTraceHeader = xhr.requestHeaders.get( + B3_MULTI_TRACE_ID_HEADER_KEY + ) as string; + const b3MultiParentHeader = xhr.requestHeaders.get( + B3_MULTI_SPAN_ID_HEADER_KEY + ) as string; + + const b3Header = xhr.requestHeaders.get(B3_HEADER_KEY); + const traceB3Value = b3Header?.split('-')[0] as string; + const parentB3Value = b3Header?.split('-')[1] as string; expect(hexToDecimal(traceContextValue)).toBe( datadogTraceValue128BitDec @@ -676,9 +714,11 @@ describe('XHRProxy', () => { await flushPromises(); // THEN - const traceId = xhr.requestHeaders[B3_MULTI_TRACE_ID_HEADER_KEY]; - const spanId = xhr.requestHeaders[B3_MULTI_SPAN_ID_HEADER_KEY]; - const sampled = xhr.requestHeaders[B3_MULTI_SAMPLED_HEADER_KEY]; + const traceId = xhr.requestHeaders.get( + B3_MULTI_TRACE_ID_HEADER_KEY + ); + const spanId = xhr.requestHeaders.get(B3_MULTI_SPAN_ID_HEADER_KEY); + const sampled = xhr.requestHeaders.get(B3_MULTI_SAMPLED_HEADER_KEY); expect(traceId).toMatch(/^[0-9a-f]{8}[0]{8}[0-9a-f]{16}$/); expect(spanId).toMatch(/^[0-9a-f]{16}$/); expect(sampled).toBe('1'); @@ -711,7 +751,7 @@ describe('XHRProxy', () => { await flushPromises(); // THEN - const headerValue = xhr.requestHeaders[B3_HEADER_KEY]; + const headerValue = xhr.requestHeaders.get(B3_HEADER_KEY); expect(headerValue).toMatch( /^[0-9a-f]{8}[0]{8}[0-9a-f]{16}-[0-9a-f]{16}-1$/ ); @@ -750,23 +790,29 @@ describe('XHRProxy', () => { await flushPromises(); // THEN - expect(xhr.requestHeaders[B3_HEADER_KEY]).not.toBeUndefined(); + expect(xhr.requestHeaders.get(B3_HEADER_KEY)).not.toBeUndefined(); expect( - xhr.requestHeaders[B3_MULTI_TRACE_ID_HEADER_KEY] + xhr.requestHeaders.get(B3_MULTI_TRACE_ID_HEADER_KEY) ).not.toBeUndefined(); expect( - xhr.requestHeaders[B3_MULTI_SPAN_ID_HEADER_KEY] + xhr.requestHeaders.get(B3_MULTI_SPAN_ID_HEADER_KEY) + ).not.toBeUndefined(); + expect(xhr.requestHeaders.get(B3_MULTI_SAMPLED_HEADER_KEY)).toBe( + '1' + ); + expect( + xhr.requestHeaders.get(TRACECONTEXT_HEADER_KEY) ).not.toBeUndefined(); - expect(xhr.requestHeaders[B3_MULTI_SAMPLED_HEADER_KEY]).toBe('1'); expect( - xhr.requestHeaders[TRACECONTEXT_HEADER_KEY] + xhr.requestHeaders.get(TRACE_ID_HEADER_KEY) ).not.toBeUndefined(); - expect(xhr.requestHeaders[TRACE_ID_HEADER_KEY]).not.toBeUndefined(); expect( - xhr.requestHeaders[PARENT_ID_HEADER_KEY] + xhr.requestHeaders.get(PARENT_ID_HEADER_KEY) ).not.toBeUndefined(); - expect(xhr.requestHeaders[SAMPLING_PRIORITY_HEADER_KEY]).toBe('1'); - expect(xhr.requestHeaders[ORIGIN_HEADER_KEY]).toBe(ORIGIN_RUM); + expect(xhr.requestHeaders.get(SAMPLING_PRIORITY_HEADER_KEY)).toBe( + '1' + ); + expect(xhr.requestHeaders.get(ORIGIN_HEADER_KEY)).toBe(ORIGIN_RUM); }); it('adds rum session id to baggage headers when available', async () => { @@ -804,8 +850,10 @@ describe('XHRProxy', () => { await flushPromises(); // THEN - expect(xhr.requestHeaders[BAGGAGE_HEADER_KEY]).not.toBeUndefined(); - expect(xhr.requestHeaders[BAGGAGE_HEADER_KEY]).toBe( + expect( + xhr.requestHeaders.get(BAGGAGE_HEADER_KEY) + ).not.toBeUndefined(); + expect(xhr.requestHeaders.get(BAGGAGE_HEADER_KEY)).toBe( 'session.id=TEST-SESSION-ID' ); }); @@ -845,7 +893,7 @@ describe('XHRProxy', () => { await flushPromises(); // THEN - expect(xhr.requestHeaders[BAGGAGE_HEADER_KEY]).toBeUndefined(); + expect(xhr.requestHeaders.get(BAGGAGE_HEADER_KEY)).toBeUndefined(); }); it('does not add rum session id to baggage headers when propagator type is not datadog or w3c', async () => { @@ -1078,27 +1126,39 @@ describe('XHRProxy', () => { await flushPromises(); // THEN - const timings = - DdNativeRum.stopResource.mock.calls[0][4][ - '_dd.resource_timings' - ]; + const timings = DdNativeRum.stopResource.mock.calls[0][4]; if (Platform.OS === 'ios') { - expect(timings['firstByte']['startTime']).toBeGreaterThan(0); + expect( + timings['_dd.resource_timings.firstByte.startTime'] + ).toBeGreaterThan(0); } else { - expect(timings['firstByte']['startTime']).toBe(0); + expect( + timings['_dd.resource_timings.firstByte.startTime'] + ).toBe(0); } - expect(timings['firstByte']['duration']).toBeGreaterThan(0); + expect( + timings['_dd.resource_timings.firstByte.duration'] + ).toBeGreaterThan(0); - expect(timings['download']['startTime']).toBeGreaterThan(0); - expect(timings['download']['duration']).toBeGreaterThan(0); + expect( + timings['_dd.resource_timings.download.startTime'] + ).toBeGreaterThan(0); + + expect( + timings['_dd.resource_timings.download.duration'] + ).toBeGreaterThan(0); if (Platform.OS === 'ios') { - expect(timings['fetch']['startTime']).toBeGreaterThan(0); + expect( + timings['_dd.resource_timings.fetch.startTime'] + ).toBeGreaterThan(0); } else { - expect(timings['fetch']['startTime']).toBe(0); + expect(timings['_dd.resource_timings.fetch.startTime']).toBe(0); } - expect(timings['fetch']['duration']).toBeGreaterThan(0); + expect( + timings['_dd.resource_timings.fetch.duration'] + ).toBeGreaterThan(0); }); it(`M generate resource timings when startTracking() + XHR.open() + XHR.send() + XHR.abort(), platform=${platform}`, async () => { @@ -1126,27 +1186,38 @@ describe('XHRProxy', () => { await flushPromises(); // THEN - const timings = - DdNativeRum.stopResource.mock.calls[0][4][ - '_dd.resource_timings' - ]; + const timings = DdNativeRum.stopResource.mock.calls[0][4]; if (Platform.OS === 'ios') { - expect(timings['firstByte']['startTime']).toBeGreaterThan(0); + expect( + timings['_dd.resource_timings.firstByte.startTime'] + ).toBeGreaterThan(0); } else { - expect(timings['firstByte']['startTime']).toBe(0); + expect( + timings['_dd.resource_timings.firstByte.startTime'] + ).toBe(0); } - expect(timings['firstByte']['duration']).toBeGreaterThan(0); + expect( + timings['_dd.resource_timings.firstByte.duration'] + ).toBeGreaterThan(0); - expect(timings['download']['startTime']).toBeGreaterThan(0); - expect(timings['download']['duration']).toBeGreaterThan(0); + expect( + timings['_dd.resource_timings.download.startTime'] + ).toBeGreaterThan(0); + expect( + timings['_dd.resource_timings.download.duration'] + ).toBeGreaterThan(0); if (Platform.OS === 'ios') { - expect(timings['fetch']['startTime']).toBeGreaterThan(0); + expect( + timings['_dd.resource_timings.fetch.startTime'] + ).toBeGreaterThan(0); } else { - expect(timings['fetch']['startTime']).toBe(0); + expect(timings['_dd.resource_timings.fetch.startTime']).toBe(0); } - expect(timings['fetch']['duration']).toBeGreaterThan(0); + expect( + timings['_dd.resource_timings.fetch.duration'] + ).toBeGreaterThan(0); }); }); @@ -1183,7 +1254,7 @@ describe('XHRProxy', () => { firstPartyHostsRegexMap: firstPartyHostsRegexMapBuilder([]) }); DdRum.registerResourceEventMapper(event => { - event.context['body'] = JSON.parse( + (event.context as any)['body'] = JSON.parse( event.resourceContext?.response ); return event; @@ -1200,9 +1271,7 @@ describe('XHRProxy', () => { // THEN const attributes = DdNativeRum.stopResource.mock.calls[0][4]; - expect(attributes['body']).toEqual({ - body: 'content' - }); + expect(attributes['body.body']).toEqual('content'); }); }); @@ -1453,13 +1522,13 @@ describe('XHRProxy', () => { expect(attributes['_dd.graphql.variables']).toEqual('{}'); expect( - xhr.requestHeaders[DATADOG_GRAPH_QL_OPERATION_TYPE_HEADER] + xhr.requestHeaders.get(DATADOG_GRAPH_QL_OPERATION_TYPE_HEADER) ).not.toBeDefined(); expect( - xhr.requestHeaders[DATADOG_GRAPH_QL_OPERATION_NAME_HEADER] + xhr.requestHeaders.get(DATADOG_GRAPH_QL_OPERATION_NAME_HEADER) ).not.toBeDefined(); expect( - xhr.requestHeaders[DATADOG_GRAPH_QL_VARIABLES_HEADER] + xhr.requestHeaders.get(DATADOG_GRAPH_QL_VARIABLES_HEADER) ).not.toBeDefined(); }); @@ -1491,13 +1560,13 @@ describe('XHRProxy', () => { expect(attributes['_dd.graphql.variables']).not.toBeDefined(); expect( - xhr.requestHeaders[DATADOG_GRAPH_QL_OPERATION_TYPE_HEADER] + xhr.requestHeaders.get(DATADOG_GRAPH_QL_OPERATION_TYPE_HEADER) ).not.toBeDefined(); expect( - xhr.requestHeaders[DATADOG_GRAPH_QL_OPERATION_NAME_HEADER] + xhr.requestHeaders.get(DATADOG_GRAPH_QL_OPERATION_NAME_HEADER) ).not.toBeDefined(); expect( - xhr.requestHeaders[DATADOG_GRAPH_QL_VARIABLES_HEADER] + xhr.requestHeaders.get(DATADOG_GRAPH_QL_VARIABLES_HEADER) ).not.toBeDefined(); }); @@ -1530,13 +1599,13 @@ describe('XHRProxy', () => { expect(attributes['_dd.graphql.variables']).not.toBeDefined(); expect( - xhr.requestHeaders[DATADOG_GRAPH_QL_OPERATION_TYPE_HEADER] + xhr.requestHeaders.get(DATADOG_GRAPH_QL_OPERATION_TYPE_HEADER) ).not.toBeDefined(); expect( - xhr.requestHeaders[DATADOG_GRAPH_QL_OPERATION_NAME_HEADER] + xhr.requestHeaders.get(DATADOG_GRAPH_QL_OPERATION_NAME_HEADER) ).not.toBeDefined(); expect( - xhr.requestHeaders[DATADOG_GRAPH_QL_VARIABLES_HEADER] + xhr.requestHeaders.get(DATADOG_GRAPH_QL_VARIABLES_HEADER) ).not.toBeDefined(); }); diff --git a/packages/core/src/sdk/DatadogInternalBridge/__tests__/DdSdkNativeBridge.test.tsx b/packages/core/src/sdk/DatadogInternalBridge/__tests__/DdSdkNativeBridge.test.tsx index 9a20162fd..3dff4e7ce 100644 --- a/packages/core/src/sdk/DatadogInternalBridge/__tests__/DdSdkNativeBridge.test.tsx +++ b/packages/core/src/sdk/DatadogInternalBridge/__tests__/DdSdkNativeBridge.test.tsx @@ -72,7 +72,7 @@ describe('DdSdkNativeBridge', () => { afterEach(() => { jest.resetModules(); jest.resetAllMocks(); - delete global.RN$Bridgeless; + delete (global as any).RN$Bridgeless; }); describe('new architecture implementation', () => { diff --git a/packages/core/src/sdk/DatadogProvider/Buffer/__tests__/BoundedBuffer.test.ts b/packages/core/src/sdk/DatadogProvider/Buffer/__tests__/BoundedBuffer.test.ts index 3762da1e5..87fd37798 100644 --- a/packages/core/src/sdk/DatadogProvider/Buffer/__tests__/BoundedBuffer.test.ts +++ b/packages/core/src/sdk/DatadogProvider/Buffer/__tests__/BoundedBuffer.test.ts @@ -5,7 +5,7 @@ */ import { InternalLog } from '../../../../InternalLog'; -import { DdSdk } from '../../../DdSdk'; +import { NativeDdSdk } from '../../../DdSdkInternal'; import { BoundedBuffer } from '../BoundedBuffer'; describe('BoundedBuffer', () => { @@ -126,7 +126,7 @@ describe('BoundedBuffer', () => { await buffer.drain(); expect(callbackWithId).toHaveBeenCalledTimes(1); expect(callbackWithId).toHaveBeenNthCalledWith(1, 'callbackId1'); - expect(DdSdk.telemetryError).toHaveBeenCalledWith( + expect(NativeDdSdk.telemetryError).toHaveBeenCalledWith( 'Could not generate enough random numbers happened 2 times.', '', 'RandomIdGenerationError' @@ -146,7 +146,7 @@ describe('BoundedBuffer', () => { await buffer.drain(); expect(fakeCallback).toHaveBeenCalledTimes(3); - expect(DdSdk.telemetryError).toHaveBeenCalledWith( + expect(NativeDdSdk.telemetryError).toHaveBeenCalledWith( 'Buffer overflow happened 1 times.', '', 'BufferOverflow' @@ -171,7 +171,7 @@ describe('BoundedBuffer', () => { expect(fakeCallback).toHaveBeenCalledTimes(1); expect(callbackReturningId).not.toHaveBeenCalled(); expect(callbackWithId).not.toHaveBeenCalled(); - expect(DdSdk.telemetryError).toHaveBeenCalledWith( + expect(NativeDdSdk.telemetryError).toHaveBeenCalledWith( 'Buffer overflow happened 2 times.', '', 'BufferOverflow' @@ -196,7 +196,7 @@ describe('BoundedBuffer', () => { expect(fakeCallback).toHaveBeenCalledTimes(1); expect(callbackReturningId).toHaveBeenCalledTimes(1); expect(callbackWithId).toHaveBeenCalledTimes(1); - expect(DdSdk.telemetryError).not.toHaveBeenCalled(); + expect(NativeDdSdk.telemetryError).not.toHaveBeenCalled(); }); }); }); diff --git a/packages/core/src/sdk/DatadogProvider/__tests__/initialization.test.tsx b/packages/core/src/sdk/DatadogProvider/__tests__/initialization.test.tsx index 8ace4b731..66a3206b2 100644 --- a/packages/core/src/sdk/DatadogProvider/__tests__/initialization.test.tsx +++ b/packages/core/src/sdk/DatadogProvider/__tests__/initialization.test.tsx @@ -70,6 +70,7 @@ describe('DatadogProvider', () => { }, "appHangThreshold": undefined, "applicationId": "fakeApplicationId", + "attributeEncoders": [], "batchProcessingLevel": "MEDIUM", "batchSize": "MEDIUM", "bundleLogsWithRum": true, diff --git a/packages/core/src/sdk/EventMappers/__tests__/EventMapper.test.ts b/packages/core/src/sdk/EventMappers/__tests__/EventMapper.test.ts index 780b0a831..93333f7de 100644 --- a/packages/core/src/sdk/EventMappers/__tests__/EventMapper.test.ts +++ b/packages/core/src/sdk/EventMappers/__tests__/EventMapper.test.ts @@ -4,14 +4,14 @@ * Copyright 2016-Present Datadog, Inc. */ -import { DdSdk } from '../../DdSdk'; +import { NativeDdSdk } from '../../DdSdkInternal'; import { EventMapper } from '../EventMapper'; describe('EventMapper', () => { it('returns the original log when the event log mapper crashes', () => { const eventMapper = new EventMapper( (event: object) => { - event['badData'] = 'bad data'; + (event as { badData: string })['badData'] = 'bad data'; throw new Error('crashed'); }, (event: object) => event, @@ -26,7 +26,7 @@ describe('EventMapper', () => { ).toEqual({ someData: 'some data' }); - expect(DdSdk.telemetryDebug).toHaveBeenCalledWith( + expect(NativeDdSdk.telemetryDebug).toHaveBeenCalledWith( 'Error while running the event mapper' ); }); diff --git a/packages/core/src/sdk/FileBasedConfiguration/__tests__/FileBasedConfiguration.test.ts b/packages/core/src/sdk/FileBasedConfiguration/__tests__/FileBasedConfiguration.test.ts index 1670e5fe2..2d10d390e 100644 --- a/packages/core/src/sdk/FileBasedConfiguration/__tests__/FileBasedConfiguration.test.ts +++ b/packages/core/src/sdk/FileBasedConfiguration/__tests__/FileBasedConfiguration.test.ts @@ -118,6 +118,7 @@ describe('FileBasedConfiguration', () => { "actionNameAttribute": "action-name-attr", "additionalConfiguration": {}, "applicationId": "fake-app-id", + "attributeEncoders": [], "batchProcessingLevel": "MEDIUM", "batchSize": "MEDIUM", "bundleLogsWithRum": true, @@ -180,6 +181,7 @@ describe('FileBasedConfiguration', () => { "actionNameAttribute": undefined, "additionalConfiguration": {}, "applicationId": "fake-app-id", + "attributeEncoders": [], "batchProcessingLevel": "MEDIUM", "batchSize": "MEDIUM", "bundleLogsWithRum": true, diff --git a/packages/core/src/trace/__tests__/DdTrace.test.ts b/packages/core/src/trace/__tests__/DdTrace.test.ts index a1567afef..ba7a87d26 100644 --- a/packages/core/src/trace/__tests__/DdTrace.test.ts +++ b/packages/core/src/trace/__tests__/DdTrace.test.ts @@ -52,13 +52,13 @@ describe('DdTrace', () => { }); test('uses empty context with error when context is invalid or null', async () => { - const context: any = 123; + const context: any = Symbol('invalid-context'); await DdTrace.startSpan('operation', context); expect(InternalLog.log).toHaveBeenNthCalledWith( - 1, + 2, expect.anything(), - SdkVerbosity.ERROR + SdkVerbosity.WARN ); expect(NativeModules.DdTrace.startSpan).toHaveBeenCalledWith( @@ -104,18 +104,17 @@ describe('DdTrace', () => { }); test('uses empty context with error when context is invalid or null', async () => { - const context: any = 123; + const context: any = Symbol('invalid-context'); await DdTrace.startSpan('operation', context); const spanId = await DdTrace.startSpan('operation', {}); await DdTrace.finishSpan(spanId, context); expect(InternalLog.log).toHaveBeenNthCalledWith( - 1, + 2, expect.anything(), - SdkVerbosity.ERROR + SdkVerbosity.WARN ); - expect(NativeModules.DdTrace.finishSpan).toHaveBeenCalledWith( spanId, {}, diff --git a/packages/core/src/utils/__tests__/argsUtils.test.ts b/packages/core/src/utils/__tests__/argsUtils.test.ts deleted file mode 100644 index 1a2f4eacb..000000000 --- a/packages/core/src/utils/__tests__/argsUtils.test.ts +++ /dev/null @@ -1,66 +0,0 @@ -/* eslint-disable @typescript-eslint/ban-ts-comment */ -/* - * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. - * This product includes software developed at Datadog (https://www.datadoghq.com/). - * Copyright 2016-Present Datadog, Inc. - */ - -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/__tests__/errorUtils.test.ts b/packages/core/src/utils/__tests__/errorUtils.test.ts index b9941d4b4..c89f7a2b7 100644 --- a/packages/core/src/utils/__tests__/errorUtils.test.ts +++ b/packages/core/src/utils/__tests__/errorUtils.test.ts @@ -4,7 +4,7 @@ * Copyright 2016-Present Datadog, Inc. */ -import { getErrorName } from '../errorUtils'; +import { getErrorName } from '../../sdk/AttributesEncoding/errorUtils'; describe('errorUtils', () => { describe('getErrorName', () => { From 9e03a70c8319ffaa9b55127a669c863414302cdb Mon Sep 17 00:00:00 2001 From: Marco Saia Date: Fri, 10 Oct 2025 11:36:37 +0200 Subject: [PATCH 105/410] Minor warning fixes in tests --- .../rum/instrumentation/DdRumErrorTracking.test.tsx | 6 +++--- .../requestProxy/XHRProxy/__tests__/XHRProxy.test.ts | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/core/src/__tests__/rum/instrumentation/DdRumErrorTracking.test.tsx b/packages/core/src/__tests__/rum/instrumentation/DdRumErrorTracking.test.tsx index cfa6b9014..cfc8bfbb8 100644 --- a/packages/core/src/__tests__/rum/instrumentation/DdRumErrorTracking.test.tsx +++ b/packages/core/src/__tests__/rum/instrumentation/DdRumErrorTracking.test.tsx @@ -137,7 +137,7 @@ it('M intercept and send a RUM event W onGlobalError() {Error object}', async () expect.any(Number), '' ); - expect(DdRum.addError.mock.calls[0][2]).toContain( + expect((DdRum.addError as any).mock.calls[0][2]).toContain( '/packages/core/src/__tests__/rum/instrumentation/DdRumErrorTracking.test.tsx' ); expect(baseErrorHandlerCalled).toStrictEqual(true); @@ -174,7 +174,7 @@ it('M intercept and send a RUM event W onGlobalError() {CustomError object}', as expect.any(Number), '' ); - expect(DdRum.addError.mock.calls[0][2]).toContain( + expect((DdRum.addError as any).mock.calls[0][2]).toContain( '/packages/core/src/__tests__/rum/instrumentation/DdRumErrorTracking.test.tsx' ); expect(baseErrorHandlerCalled).toStrictEqual(true); @@ -624,7 +624,7 @@ it('M intercept and send a RUM event W on error() {called from RNErrorHandler}', expect.any(Number), '' ); - expect(DdRum.addError.mock.calls[0][2]).toContain( + expect((DdRum.addError as any).mock.calls[0][2]).toContain( '/packages/core/src/__tests__/rum/instrumentation/DdRumErrorTracking.test.tsx' ); expect(baseErrorHandlerCalled).toStrictEqual(true); diff --git a/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/__tests__/XHRProxy.test.ts b/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/__tests__/XHRProxy.test.ts index 6599dea08..6e944b6c3 100644 --- a/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/__tests__/XHRProxy.test.ts +++ b/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/__tests__/XHRProxy.test.ts @@ -1618,16 +1618,16 @@ describe('XHRProxy', () => { firstPartyHostsRegexMap: firstPartyHostsRegexMapBuilder([]) }); DdRum.registerResourceEventMapper(event => { - if (event.context['_dd.graphql.variables']) { + if ((event.context as any)['_dd.graphql.variables']) { const variables = JSON.parse( - event.context['_dd.graphql.variables'] + (event.context as any)['_dd.graphql.variables'] ); if (variables.password) { variables.password = '***'; } - event.context['_dd.graphql.variables'] = JSON.stringify( - variables - ); + (event.context as any)[ + '_dd.graphql.variables' + ] = JSON.stringify(variables); } return event; From 646e3826fc9e599dc4f8bb066314d9c61e82bc30 Mon Sep 17 00:00:00 2001 From: Marco Saia Date: Mon, 13 Oct 2025 17:50:06 +0200 Subject: [PATCH 106/410] Changed .tsx to .ts + removed left-over comment --- .../{attributesEncoding.tsx => attributesEncoding.ts} | 0 .../{defaultEncoders.tsx => defaultEncoders.ts} | 10 ---------- .../{errorUtils.tsx => errorUtils.ts} | 0 .../sdk/AttributesEncoding/{helpers.tsx => helpers.ts} | 0 .../src/sdk/AttributesEncoding/{types.tsx => types.ts} | 0 .../src/sdk/AttributesEncoding/{utils.tsx => utils.ts} | 0 packages/core/src/sdk/{DdSdk.tsx => DdSdk.ts} | 0 .../src/sdk/{DdSdkInternal.tsx => DdSdkInternal.ts} | 0 8 files changed, 10 deletions(-) rename packages/core/src/sdk/AttributesEncoding/{attributesEncoding.tsx => attributesEncoding.ts} (100%) rename packages/core/src/sdk/AttributesEncoding/{defaultEncoders.tsx => defaultEncoders.ts} (95%) rename packages/core/src/sdk/AttributesEncoding/{errorUtils.tsx => errorUtils.ts} (100%) rename packages/core/src/sdk/AttributesEncoding/{helpers.tsx => helpers.ts} (100%) rename packages/core/src/sdk/AttributesEncoding/{types.tsx => types.ts} (100%) rename packages/core/src/sdk/AttributesEncoding/{utils.tsx => utils.ts} (100%) rename packages/core/src/sdk/{DdSdk.tsx => DdSdk.ts} (100%) rename packages/core/src/sdk/{DdSdkInternal.tsx => DdSdkInternal.ts} (100%) diff --git a/packages/core/src/sdk/AttributesEncoding/attributesEncoding.tsx b/packages/core/src/sdk/AttributesEncoding/attributesEncoding.ts similarity index 100% rename from packages/core/src/sdk/AttributesEncoding/attributesEncoding.tsx rename to packages/core/src/sdk/AttributesEncoding/attributesEncoding.ts diff --git a/packages/core/src/sdk/AttributesEncoding/defaultEncoders.tsx b/packages/core/src/sdk/AttributesEncoding/defaultEncoders.ts similarity index 95% rename from packages/core/src/sdk/AttributesEncoding/defaultEncoders.tsx rename to packages/core/src/sdk/AttributesEncoding/defaultEncoders.ts index fdee006f1..420b629ec 100644 --- a/packages/core/src/sdk/AttributesEncoding/defaultEncoders.tsx +++ b/packages/core/src/sdk/AttributesEncoding/defaultEncoders.ts @@ -60,17 +60,7 @@ export const dateEncoder: AttributeEncoder = { check: (v: unknown): v is Date => v instanceof Date, encode: (d: Date) => String(d) }; -/* - } else if ('componentStack' in error) { - stack = String(error.componentStack); - } else if ( - 'sourceURL' in error && - 'line' in error && - 'column' in error - ) { - -*/ /** * Extended Error Encoder. * Serializes name, message, stack, and cause (ES2022+) for Error objects. diff --git a/packages/core/src/sdk/AttributesEncoding/errorUtils.tsx b/packages/core/src/sdk/AttributesEncoding/errorUtils.ts similarity index 100% rename from packages/core/src/sdk/AttributesEncoding/errorUtils.tsx rename to packages/core/src/sdk/AttributesEncoding/errorUtils.ts diff --git a/packages/core/src/sdk/AttributesEncoding/helpers.tsx b/packages/core/src/sdk/AttributesEncoding/helpers.ts similarity index 100% rename from packages/core/src/sdk/AttributesEncoding/helpers.tsx rename to packages/core/src/sdk/AttributesEncoding/helpers.ts diff --git a/packages/core/src/sdk/AttributesEncoding/types.tsx b/packages/core/src/sdk/AttributesEncoding/types.ts similarity index 100% rename from packages/core/src/sdk/AttributesEncoding/types.tsx rename to packages/core/src/sdk/AttributesEncoding/types.ts diff --git a/packages/core/src/sdk/AttributesEncoding/utils.tsx b/packages/core/src/sdk/AttributesEncoding/utils.ts similarity index 100% rename from packages/core/src/sdk/AttributesEncoding/utils.tsx rename to packages/core/src/sdk/AttributesEncoding/utils.ts diff --git a/packages/core/src/sdk/DdSdk.tsx b/packages/core/src/sdk/DdSdk.ts similarity index 100% rename from packages/core/src/sdk/DdSdk.tsx rename to packages/core/src/sdk/DdSdk.ts diff --git a/packages/core/src/sdk/DdSdkInternal.tsx b/packages/core/src/sdk/DdSdkInternal.ts similarity index 100% rename from packages/core/src/sdk/DdSdkInternal.tsx rename to packages/core/src/sdk/DdSdkInternal.ts From a3c778c80bb402fa7ffbc4f168d8a33e2a79d939 Mon Sep 17 00:00:00 2001 From: Marco Saia Date: Tue, 21 Oct 2025 15:56:24 +0200 Subject: [PATCH 107/410] Limit encoded attributes to 128 --- .../__tests__/attributesEncoding.test.ts | 31 ++++++++++ .../AttributesEncoding/attributesEncoding.ts | 8 +-- .../src/sdk/AttributesEncoding/helpers.ts | 62 ++++++++++++++++--- 3 files changed, 87 insertions(+), 14 deletions(-) diff --git a/packages/core/src/sdk/AttributesEncoding/__tests__/attributesEncoding.test.ts b/packages/core/src/sdk/AttributesEncoding/__tests__/attributesEncoding.test.ts index 9cd2718ff..759aeefd6 100644 --- a/packages/core/src/sdk/AttributesEncoding/__tests__/attributesEncoding.test.ts +++ b/packages/core/src/sdk/AttributesEncoding/__tests__/attributesEncoding.test.ts @@ -263,4 +263,35 @@ describe('encodeAttributes', () => { ]) ); }); + + it('drops attributes after reaching the 128 limit and warns once', () => { + // Prepare 200 simple attributes — max=128 + const input: Record = {}; + for (let i = 0; i < 200; i++) { + input[`key${i}`] = i; + } + + const result = encodeAttributes(input); + + // Check that only 128 attributes remain + expect(Object.keys(result)).toHaveLength(128); + + // Check the first ones are preserved + expect(result).toHaveProperty('key0', 0); + expect(result).toHaveProperty('key127', 127); + + // Check later ones were dropped + expect(result).not.toHaveProperty('key128'); + + // Check that a warning was shown at least once + expect(warn).toHaveBeenCalledWith( + expect.stringContaining('Attribute limit') + ); + + // Check there is only one "limit reached" warning (even if multiple attributes were dropped) + const limitWarnings = (warn as jest.Mock).mock.calls.filter(([msg]) => + msg.includes('Attribute limit') + ); + expect(limitWarnings).toHaveLength(1); + }); }); diff --git a/packages/core/src/sdk/AttributesEncoding/attributesEncoding.ts b/packages/core/src/sdk/AttributesEncoding/attributesEncoding.ts index 0f05d7e11..cd2103f74 100644 --- a/packages/core/src/sdk/AttributesEncoding/attributesEncoding.ts +++ b/packages/core/src/sdk/AttributesEncoding/attributesEncoding.ts @@ -7,7 +7,7 @@ import { DdSdk } from '../DdSdk'; import { builtInEncoders } from './defaultEncoders'; -import { encodeAttributesInPlace } from './helpers'; +import { encodeAttributesInPlace, type EncodeContext } from './helpers'; import type { Encodable } from './types'; import { isPlainObject, warn } from './utils'; @@ -23,14 +23,14 @@ import { isPlainObject, warn } from './utils'; export function encodeAttributes(input: unknown): Record { const result: Record = {}; const allEncoders = [...DdSdk.attributeEncoders, ...builtInEncoders]; - + const context: EncodeContext = { numOfAttributes: 0 }; if (isPlainObject(input)) { for (const [k, v] of Object.entries(input)) { - encodeAttributesInPlace(v, result, [k], allEncoders); + encodeAttributesInPlace(v, result, [k], allEncoders, context); } } else { // Fallback for primitive values passed as root - encodeAttributesInPlace(input, result, ['context'], allEncoders); + encodeAttributesInPlace(input, result, ['context'], allEncoders, context); warn( 'Warning: attributes root should be an object.\n' + 'Received a primitive/array instead, which will be wrapped under the "context" key.' diff --git a/packages/core/src/sdk/AttributesEncoding/helpers.ts b/packages/core/src/sdk/AttributesEncoding/helpers.ts index 3636be9e1..1fc80dece 100644 --- a/packages/core/src/sdk/AttributesEncoding/helpers.ts +++ b/packages/core/src/sdk/AttributesEncoding/helpers.ts @@ -7,6 +7,13 @@ import type { AttributeEncoder, Encodable } from './types'; import { formatPathForLog, isPlainObject, warn } from './utils'; +const MAX_ATTRIBUTES = 128; + +export interface EncodeContext { + numOfAttributes: number; + limitReachedWarned?: boolean; +} + /** * Recursive in-place encoder: flattens values into `out` dictionary. * Never applies "context", that's only for the root. @@ -15,7 +22,8 @@ export function encodeAttributesInPlace( input: unknown, out: Record, path: string[], - encoders: AttributeEncoder[] + encoders: AttributeEncoder[], + context: EncodeContext = { numOfAttributes: 0 } ): void { const value = applyEncoders(input, encoders); @@ -27,7 +35,7 @@ export function encodeAttributesInPlace( typeof value === 'number' || typeof value === 'boolean' ) { - out[path.join('.')] = value; + addEncodedAttribute(out, path, value, context); return; } @@ -49,7 +57,7 @@ export function encodeAttributesInPlace( if (isPlainObject(v)) { const nested: Record = {}; - encodeAttributesInPlace(v, nested, [], encoders); + encodeAttributesInPlace(v, nested, [], encoders, context); return nested; } @@ -66,16 +74,20 @@ export function encodeAttributesInPlace( return undefined; }; - out[path.join('.')] = value - .map(normalize) - .filter(item => item !== undefined); // drop unsupported + addEncodedAttribute( + out, + path, + value.map(normalize).filter(item => item !== undefined), + context + ); + return; } // Plain object if (isPlainObject(value)) { for (const [k, v] of Object.entries(value)) { - encodeAttributesInPlace(v, out, [...path, k], encoders); + encodeAttributesInPlace(v, out, [...path, k], encoders, context); } return; } @@ -94,7 +106,8 @@ export function encodeAttributesInPlace( */ export function sanitizeForJson( value: unknown, - encoders: AttributeEncoder[] + encoders: AttributeEncoder[], + context: EncodeContext = { numOfAttributes: 0 } ): Encodable { const v = applyEncoders(value, encoders); @@ -102,14 +115,14 @@ export function sanitizeForJson( if (isPlainObject(v)) { const out: Record = {}; for (const [k, val] of Object.entries(v)) { - encodeAttributesInPlace(val, out, [k], encoders); + encodeAttributesInPlace(val, out, [k], encoders, context); } return out; } // If array, sanitize items if (Array.isArray(v)) { - return v.map(item => sanitizeForJson(item, encoders)); + return v.map(item => sanitizeForJson(item, encoders, context)); } return v; @@ -132,3 +145,32 @@ export function applyEncoders( // Not matched by any encoder; leave as-is for the visitor to decide return value as Encodable; } + +function addEncodedAttribute( + out: Record, + path: string[], + value: Encodable, + context: EncodeContext +): void { + if (context.numOfAttributes >= MAX_ATTRIBUTES) { + // Only warn once to avoid log spam + if (!context.limitReachedWarned) { + warn( + `Attribute limit of ${MAX_ATTRIBUTES} reached; further attributes will be dropped.` + ); + context.limitReachedWarned = true; + } + + // Optional: warn for specific dropped attribute (if desired) + warn( + `Dropped attribute at '${formatPathForLog( + path + )}' because limit of ${MAX_ATTRIBUTES} attributes was reached. All further attributes will be dropped.` + ); + + return; + } + + out[path.join('.')] = value; + context.numOfAttributes++; +} From fc90d025648ce7eb97516983bb288b16e77efb97 Mon Sep 17 00:00:00 2001 From: Marco Saia Date: Tue, 21 Oct 2025 16:07:32 +0200 Subject: [PATCH 108/410] Add tests to ensure attributes are encoded by copy --- .../__tests__/attributesEncoding.test.ts | 116 ++++++++++++++++++ 1 file changed, 116 insertions(+) diff --git a/packages/core/src/sdk/AttributesEncoding/__tests__/attributesEncoding.test.ts b/packages/core/src/sdk/AttributesEncoding/__tests__/attributesEncoding.test.ts index 759aeefd6..b679ee363 100644 --- a/packages/core/src/sdk/AttributesEncoding/__tests__/attributesEncoding.test.ts +++ b/packages/core/src/sdk/AttributesEncoding/__tests__/attributesEncoding.test.ts @@ -130,6 +130,122 @@ describe('encodeAttributes', () => { expect(result).toEqual({ valid: 'ok' }); }); + it('does not modify original object when dropping values', () => { + const input = { valid: 'ok', bad: () => {} }; + const result = encodeAttributes(input); + expect(result).toEqual({ valid: 'ok' }); + expect(input).toHaveProperty('bad'); + }); + + it('does not modify original object when dropping nested invalid values', () => { + const input = { user: { profile: { bad: () => {}, good: 'ok' } } }; + const userBefore = { ...input.user }; + const profileBefore = { ...input.user.profile }; + + const result = encodeAttributes(input); + + // Encoder should flatten and drop invalid function + expect(result).toEqual({ 'user.profile.good': 'ok' }); + + // Verify that the original objects were not mutated or replaced + expect(input.user).toEqual(userBefore); + expect(input.user.profile).toEqual(profileBefore); + }); + + it('does not modify original array inside object', () => { + const arr = [1, 2, () => {}]; + const input = { data: arr }; + const arrBefore = [...arr]; + + const result = encodeAttributes(input); + expect(result).toEqual({ data: [1, 2] }); // dropped the function + expect(input.data).toEqual(arrBefore); // original array untouched + }); + + it('does not modify original nested arrays of objects', () => { + const objA = { val: 1 }; + const objB = { bad: () => {} }; + const objC = { val: 2 }; + + const input = { + matrix: [[objA, objB], [objC]] + }; + + // capture snapshots + const matrixBefore = input.matrix; + const row0Before = input.matrix[0]; + const row1Before = input.matrix[1]; + const objA_before = { ...objA }; + const objB_before = { ...objB }; + const objC_before = { ...objC }; + + const result = encodeAttributes(input); + + expect(result).toEqual({ + matrix: [ + [{ val: 1 }, {}], // objB sanitized + [{ val: 2 }] + ] + }); + + // check original references untouched + expect(input.matrix).toBe(matrixBefore); // same outer array reference + expect(input.matrix[0]).toBe(row0Before); // same row0 reference + expect(input.matrix[1]).toBe(row1Before); // same row1 reference + expect(objA).toEqual(objA_before); // object A unchanged + expect(objB).toEqual(objB_before); // object B unchanged + expect(objC).toEqual(objC_before); // object C unchanged + }); + + it('does not modify original Map when encoding', () => { + const innerMap = new Map([['x', 1]]); + const outerMap = new Map([['inner', innerMap]]); + const input = { outer: outerMap }; + + const snapshot = new Map(outerMap); + const innerSnapshot = new Map(innerMap); + + const result = encodeAttributes(input); + expect(result.outer).toBeInstanceOf(Array); + expect(input.outer).toBe(outerMap); // same reference + expect(Array.from(input.outer.entries())).toEqual( + Array.from(snapshot.entries()) + ); + expect(Array.from(innerMap.entries())).toEqual( + Array.from(innerSnapshot.entries()) + ); + }); + + it('does not modify original object when attribute limit is reached', () => { + const input: Record = {}; + for (let i = 0; i < 200; i++) { + input[`k${i}`] = `v${i}`; + } + const snapshot = { ...input }; + + const result = encodeAttributes(input); + expect(Object.keys(result)).toHaveLength(128); + expect(input).toEqual(snapshot); // original still has 200 keys + }); + + it('does not modify original when sanitizing arrays of objects', () => { + const obj1 = { ok: true }; + const obj2 = { bad: () => {} }; + const input = [obj1, obj2]; + + // Capture pre-encode snapshots manually + const obj1Before = { ...obj1 }; + const obj2Before = { ...obj2 }; + const arrayBefore = [...input]; + + const result = encodeAttributes(input); + + expect(result).toEqual({ context: [{ ok: true }, {}] }); + expect(input).toEqual(arrayBefore); + expect(input[0]).toEqual(obj1Before); + expect(input[1]).toEqual(obj2Before); + }); + it('handles deeply nested objects', () => { const deep = { level1: { level2: { level3: { value: 42 } } } }; const result = encodeAttributes(deep); From c1145b2365e97bd99dfb7ea78fcab7d6b949455d Mon Sep 17 00:00:00 2001 From: Sergio Barrio Date: Fri, 28 Nov 2025 12:35:19 +0100 Subject: [PATCH 109/410] Missing attributes on FileBasedConfiguration and linting --- .../XHRProxy/__tests__/XHRProxy.test.ts | 15 ++++++----- .../AttributesEncoding/attributesEncoding.ts | 11 ++++++-- packages/core/src/sdk/DdSdkInternal.ts | 26 ++++++++++++++++--- .../__tests__/FileBasedConfiguration.test.ts | 1 + 4 files changed, 41 insertions(+), 12 deletions(-) diff --git a/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/__tests__/XHRProxy.test.ts b/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/__tests__/XHRProxy.test.ts index 6e944b6c3..367d48553 100644 --- a/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/__tests__/XHRProxy.test.ts +++ b/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/__tests__/XHRProxy.test.ts @@ -931,7 +931,7 @@ describe('XHRProxy', () => { await flushPromises(); // THEN - expect(xhr.requestHeaders[BAGGAGE_HEADER_KEY]).toBeUndefined(); + expect(xhr.requestHeaders.get(BAGGAGE_HEADER_KEY)).toBeUndefined(); }); it('rum session id does not overwrite existing baggage headers', async () => { @@ -970,14 +970,17 @@ describe('XHRProxy', () => { await flushPromises(); // THEN - expect(xhr.requestHeaders[BAGGAGE_HEADER_KEY]).not.toBeUndefined(); - expect(xhr.requestHeaders[BAGGAGE_HEADER_KEY]).toContain( + expect( + xhr.requestHeaders.get(BAGGAGE_HEADER_KEY) + ).not.toBeUndefined(); + expect(xhr.requestHeaders.get(BAGGAGE_HEADER_KEY)).toContain( 'existing.key=existing-value' ); - const values = xhr.requestHeaders[BAGGAGE_HEADER_KEY].split( - ',' - ).sort(); + const values = xhr.requestHeaders + .get(BAGGAGE_HEADER_KEY) + ?.split(',') + .sort(); expect(values[0]).toBe('existing.key=existing-value'); expect(values[1]).toBe('session.id=TEST-SESSION-ID'); diff --git a/packages/core/src/sdk/AttributesEncoding/attributesEncoding.ts b/packages/core/src/sdk/AttributesEncoding/attributesEncoding.ts index cd2103f74..aa35d5fca 100644 --- a/packages/core/src/sdk/AttributesEncoding/attributesEncoding.ts +++ b/packages/core/src/sdk/AttributesEncoding/attributesEncoding.ts @@ -7,7 +7,8 @@ import { DdSdk } from '../DdSdk'; import { builtInEncoders } from './defaultEncoders'; -import { encodeAttributesInPlace, type EncodeContext } from './helpers'; +import type { EncodeContext } from './helpers'; +import { encodeAttributesInPlace } from './helpers'; import type { Encodable } from './types'; import { isPlainObject, warn } from './utils'; @@ -30,7 +31,13 @@ export function encodeAttributes(input: unknown): Record { } } else { // Fallback for primitive values passed as root - encodeAttributesInPlace(input, result, ['context'], allEncoders, context); + encodeAttributesInPlace( + input, + result, + ['context'], + allEncoders, + context + ); warn( 'Warning: attributes root should be an object.\n' + 'Received a primitive/array instead, which will be wrapped under the "context" key.' diff --git a/packages/core/src/sdk/DdSdkInternal.ts b/packages/core/src/sdk/DdSdkInternal.ts index 7af33d2e3..78df1217a 100644 --- a/packages/core/src/sdk/DdSdkInternal.ts +++ b/packages/core/src/sdk/DdSdkInternal.ts @@ -39,10 +39,6 @@ export class DdSdkWrapper implements DdNativeSdkType { return NativeDdSdk.getConstants(); } - setAttributes(attributes: object): Promise { - return NativeDdSdk.setAttributes(attributes); - } - setUserInfo(user: object): Promise { return NativeDdSdk.setUserInfo(user); } @@ -55,6 +51,28 @@ export class DdSdkWrapper implements DdNativeSdkType { return NativeDdSdk.addUserExtraInfo(extraInfo); } + addAttribute(key: string, value: object): Promise { + return NativeDdSdk.addAttribute(key, value); + } + removeAttribute(key: string): Promise { + return NativeDdSdk.removeAttribute(key); + } + addAttributes(attributes: object): Promise { + return NativeDdSdk.addAttributes(attributes); + } + removeAttributes(keys: string[]): Promise { + return NativeDdSdk.removeAttributes(keys); + } + setAccountInfo(account: object): Promise { + return NativeDdSdk.setAccountInfo(account); + } + clearAccountInfo(): Promise { + return NativeDdSdk.clearAccountInfo(); + } + addAccountExtraInfo(extraInfo: object): Promise { + return NativeDdSdk.addAccountExtraInfo(extraInfo); + } + setTrackingConsent(trackingConsent: string): Promise { return NativeDdSdk.setTrackingConsent(trackingConsent); } diff --git a/packages/core/src/sdk/FileBasedConfiguration/__tests__/FileBasedConfiguration.test.ts b/packages/core/src/sdk/FileBasedConfiguration/__tests__/FileBasedConfiguration.test.ts index 2d10d390e..e1fe5e511 100644 --- a/packages/core/src/sdk/FileBasedConfiguration/__tests__/FileBasedConfiguration.test.ts +++ b/packages/core/src/sdk/FileBasedConfiguration/__tests__/FileBasedConfiguration.test.ts @@ -27,6 +27,7 @@ describe('FileBasedConfiguration', () => { "actionNameAttribute": "action-name-attr", "additionalConfiguration": {}, "applicationId": "fake-app-id", + "attributeEncoders": [], "batchProcessingLevel": "MEDIUM", "batchSize": "MEDIUM", "bundleLogsWithRum": true, From 02b5bfe2a101658fa0527a9e915b094862a4522b Mon Sep 17 00:00:00 2001 From: Sergio Barrio Date: Fri, 28 Nov 2025 16:25:22 +0100 Subject: [PATCH 110/410] Add NavigationTrackingOptions to React Navigation tracking --- example/app.json | 2 +- example/src/App.tsx | 34 ++++- .../NestedNavigator/ScreenWithLinks.tsx | 4 +- .../DdRumReactNavigationTracking.test.tsx | 134 +++++++++++++++++- packages/react-navigation/src/index.tsx | 14 +- .../DdRumReactNavigationTracking.tsx | 65 +++++++-- 6 files changed, 234 insertions(+), 19 deletions(-) diff --git a/example/app.json b/example/app.json index f0f0c83d7..70b08eb89 100644 --- a/example/app.json +++ b/example/app.json @@ -1,5 +1,5 @@ { "name": "DdSdkReactNativeExample", "displayName": "DD RN Sample", - "navigation": "react-native-navigation" + "navigation": "react-navigation" } diff --git a/example/src/App.tsx b/example/src/App.tsx index c29982a97..831d68fd6 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -12,13 +12,41 @@ import { Route } from "@react-navigation/native"; import { NestedNavigator } from './screens/NestedNavigator/NestedNavigator'; import { getDatadogConfig, onDatadogInitialization } from './ddUtils'; import { TrackingConsent } from '@datadog/mobile-react-native'; +import { NavigationTrackingOptions, ParamsTrackingPredicate, ViewTrackingPredicate } from '@datadog/mobile-react-navigation/src/rum/instrumentation/DdRumReactNavigationTracking'; const Tab = createBottomTabNavigator(); -const viewPredicate: ViewNamePredicate = function customViewNamePredicate(route: Route, trackedName: string) { +// === Navigation Tracking custom predicates +const viewNamePredicate: ViewNamePredicate = function customViewNamePredicate(route: Route, trackedName: string) { return "Custom RN " + trackedName; } +const viewTrackingPredicate: ViewTrackingPredicate = function customViewTrackingPredicate(route: Route) { + if (route.name === "AlertModal") { + return false; + } + + return true; +} + +const paramsTrackingPredicate: ParamsTrackingPredicate = function customParamsTrackingPredicate(route: Route) { + const filteredParams: any = {}; + if (route.params?.creditCardNumber) { + filteredParams["creditCardNumber"] = "XXXX XXXX XXXX XXXX"; + } + + if (route.params?.username) { + filteredParams["username"] = route.params.username; + } + + return filteredParams; +} + +const navigationTrackingOptions: NavigationTrackingOptions = { + viewNamePredicate, + viewTrackingPredicate, + paramsTrackingPredicate, +} // === Datadog Provider Configuration schemes === // 1.- Direct configuration @@ -39,7 +67,9 @@ export default function App() { return ( { - DdRumReactNavigationTracking.startTrackingViews(navigationRef.current, viewPredicate) + DdRumReactNavigationTracking.startTrackingViews( + navigationRef.current, + navigationTrackingOptions) }}> { const {navigate} = useNavigation() return (<> - {props.links.map(link =>