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/526] 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/526] 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/526] 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/526] 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/526] 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/526] 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/526] 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/526] 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/526] 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/526] 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/526] 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/526] 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/526] 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/526] 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/526] 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/526] 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/526] 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/526] 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/526] 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/526] 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/526] 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/526] 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 2be361ca7dba859592dbe991964f43acd61cfbcf Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Mon, 20 Oct 2025 17:47:08 +0300 Subject: [PATCH 023/526] Initial native ios lib setup --- example-new-architecture/App.tsx | 3 + packages/core/ios/Sources/DdFlags.h | 24 ++++++++ packages/core/ios/Sources/DdFlags.mm | 55 +++++++++++++++++++ .../ios/Sources/DdFlagsImplementation.swift | 16 ++++++ packages/core/src/flags/DdFlags.ts | 24 ++++++++ packages/core/src/index.tsx | 2 + packages/core/src/nativeModulesTypes.ts | 6 ++ packages/core/src/specs/NativeDdFlags.ts | 20 +++++++ 8 files changed, 150 insertions(+) create mode 100644 packages/core/ios/Sources/DdFlags.h create mode 100644 packages/core/ios/Sources/DdFlags.mm create mode 100644 packages/core/ios/Sources/DdFlagsImplementation.swift create mode 100644 packages/core/src/flags/DdFlags.ts create mode 100644 packages/core/src/specs/NativeDdFlags.ts diff --git a/example-new-architecture/App.tsx b/example-new-architecture/App.tsx index e830aa345..467e9e837 100644 --- a/example-new-architecture/App.tsx +++ b/example-new-architecture/App.tsx @@ -8,6 +8,7 @@ import { RumActionType, DdLogs, DdTrace, + DdFlags, } from '@datadog/mobile-react-native'; import React from 'react'; import type {PropsWithChildren} from 'react'; @@ -32,6 +33,8 @@ import { import {APPLICATION_ID, CLIENT_TOKEN, ENVIRONMENT} from './ddCredentials'; (async () => { + console.log({constant: await DdFlags.getConstant()}); + const config = new DdSdkReactNativeConfiguration( CLIENT_TOKEN, ENVIRONMENT, diff --git a/packages/core/ios/Sources/DdFlags.h b/packages/core/ios/Sources/DdFlags.h new file mode 100644 index 000000000..d8fdab341 --- /dev/null +++ b/packages/core/ios/Sources/DdFlags.h @@ -0,0 +1,24 @@ +/* + * 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 +@class DdFlagsImplementation; + +#ifdef RCT_NEW_ARCH_ENABLED + +#import +@interface DdFlags: NSObject + +#else + +#import +@interface DdFlags : NSObject + +#endif + +@property (nonatomic, strong) DdFlagsImplementation* ddFlagsImplementation; + +@end diff --git a/packages/core/ios/Sources/DdFlags.mm b/packages/core/ios/Sources/DdFlags.mm new file mode 100644 index 000000000..a8724d525 --- /dev/null +++ b/packages/core/ios/Sources/DdFlags.mm @@ -0,0 +1,55 @@ +/* + * 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 this first to prevent require cycles +#if __has_include("DatadogSDKReactNative-Swift.h") +#import +#else +#import +#endif +#import "DdFlags.h" + + +@implementation DdFlags + +RCT_EXPORT_MODULE() + +// FIXME: This is a temporary method to test whether the native library setup is working. +RCT_REMAP_METHOD(getConstant, withResolve:(RCTPromiseResolveBlock)resolve + withRejecter:(RCTPromiseRejectBlock)reject) +{ + [self getConstant:resolve reject:reject]; +} + +// Thanks to this guard, we won't compile this code when we build for the new architecture. +#ifdef RCT_NEW_ARCH_ENABLED +- (std::shared_ptr)getTurboModule: + (const facebook::react::ObjCTurboModule::InitParams &)params +{ + return std::make_shared(params); +} +#endif + +- (DdFlagsImplementation*)ddFlagsImplementation +{ + if (_ddFlagsImplementation == nil) { + _ddFlagsImplementation = [[DdFlagsImplementation alloc] init]; + } + return _ddFlagsImplementation; +} + ++ (BOOL)requiresMainQueueSetup { + return NO; +} + +- (dispatch_queue_t)methodQueue { + return [RNQueue getSharedQueue]; +} + +- (void)getConstant:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + [self.ddFlagsImplementation getConstant:resolve reject:reject]; +} + +@end diff --git a/packages/core/ios/Sources/DdFlagsImplementation.swift b/packages/core/ios/Sources/DdFlagsImplementation.swift new file mode 100644 index 000000000..6736c8153 --- /dev/null +++ b/packages/core/ios/Sources/DdFlagsImplementation.swift @@ -0,0 +1,16 @@ +/* + * 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 Foundation + +@objc +public class DdFlagsImplementation: NSObject { + @objc + public func getConstant(_ resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) -> Void { + // FIXME: This is a temporary method to test whether the native library setup is working. + resolve(43) + } +} diff --git a/packages/core/src/flags/DdFlags.ts b/packages/core/src/flags/DdFlags.ts new file mode 100644 index 000000000..8c2680329 --- /dev/null +++ b/packages/core/src/flags/DdFlags.ts @@ -0,0 +1,24 @@ +/* + * 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 type { DdNativeFlagsType } from '../nativeModulesTypes'; + +class DdFlagsWrapper { + // eslint-disable-next-line global-require, @typescript-eslint/no-var-requires + private nativeFlags: DdNativeFlagsType = require('../specs/NativeDdFlags') + .default; + + getConstant = (): Promise => { + InternalLog.log('Flags.getConstant()', SdkVerbosity.DEBUG); + + return this.nativeFlags.getConstant(); + }; +} + +const DdFlags = new DdFlagsWrapper(); + +export { DdFlags }; diff --git a/packages/core/src/index.tsx b/packages/core/src/index.tsx index 062fecc90..4d9e9695e 100644 --- a/packages/core/src/index.tsx +++ b/packages/core/src/index.tsx @@ -21,6 +21,7 @@ import { InternalLog } from './InternalLog'; import { ProxyConfiguration, ProxyType } from './ProxyConfiguration'; import { SdkVerbosity } from './SdkVerbosity'; import { TrackingConsent } from './TrackingConsent'; +import { DdFlags } from './flags/DdFlags'; import { DdLogs } from './logs/DdLogs'; import { DdRum } from './rum/DdRum'; import { DdBabelInteractionTracking } from './rum/instrumentation/interactionTracking/DdBabelInteractionTracking'; @@ -53,6 +54,7 @@ export { FileBasedConfiguration, InitializationMode, DdLogs, + DdFlags, DdTrace, DdRum, RumActionType, diff --git a/packages/core/src/nativeModulesTypes.ts b/packages/core/src/nativeModulesTypes.ts index b05fb6e95..4a4ee8e9d 100644 --- a/packages/core/src/nativeModulesTypes.ts +++ b/packages/core/src/nativeModulesTypes.ts @@ -4,6 +4,7 @@ * Copyright 2016-Present Datadog, Inc. */ +import type { Spec as NativeDdFlags } from './specs/NativeDdFlags'; import type { Spec as NativeDdLogs } from './specs/NativeDdLogs'; import type { Spec as NativeDdRum } from './specs/NativeDdRum'; import type { Spec as NativeDdSdk } from './specs/NativeDdSdk'; @@ -24,6 +25,11 @@ export type DdNativeLogsType = NativeDdLogs; */ export type DdNativeTraceType = NativeDdTrace; +/** + * The entry point to use Datadog's Flags feature. + */ +export type DdNativeFlagsType = NativeDdFlags; + /** * A configuration object to initialize Datadog's features. */ diff --git a/packages/core/src/specs/NativeDdFlags.ts b/packages/core/src/specs/NativeDdFlags.ts new file mode 100644 index 000000000..3c06e7c96 --- /dev/null +++ b/packages/core/src/specs/NativeDdFlags.ts @@ -0,0 +1,20 @@ +/* + * 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. + */ + +/* eslint-disable @typescript-eslint/ban-types */ +import type { TurboModule } from 'react-native'; +import { TurboModuleRegistry } from 'react-native'; + +/** + * Do not import this Spec directly, use DdNativeFlagsType instead. + */ +export interface Spec extends TurboModule { + // TODO: This is a temporary method to test whether the native library setup is working. + readonly getConstant: () => Promise; +} + +// eslint-disable-next-line import/no-default-export +export default TurboModuleRegistry.get('DdFlags'); From 7caa72b320940396882b14dfbcea85355bc61f83 Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Mon, 20 Oct 2025 17:53:47 +0300 Subject: [PATCH 024/526] Add DatadogFlags package to the iOS deps --- example-new-architecture/ios/Podfile | 5 + example-new-architecture/ios/Podfile.lock | 120 ++++++++++++-------- packages/core/DatadogSDKReactNative.podspec | 1 + 3 files changed, 79 insertions(+), 47 deletions(-) diff --git a/example-new-architecture/ios/Podfile b/example-new-architecture/ios/Podfile index f2f0fa09f..ead08928f 100644 --- a/example-new-architecture/ios/Podfile +++ b/example-new-architecture/ios/Podfile @@ -19,6 +19,11 @@ end target 'DdSdkReactNativeExample' do pod 'DatadogSDKReactNative', :path => '../../packages/core/DatadogSDKReactNative.podspec', :testspecs => ['Tests'] + # These dependencies are not yet released, so we need to use the branch from the feature/flags branch. + pod 'DatadogRUM', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :branch => 'feature/flags' + pod 'DatadogInternal', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :branch => 'feature/flags' + pod 'DatadogFlags', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :branch => 'feature/flags' + config = use_native_modules! use_react_native!( diff --git a/example-new-architecture/ios/Podfile.lock b/example-new-architecture/ios/Podfile.lock index c0cd90bf6..99ad9a04a 100644 --- a/example-new-architecture/ios/Podfile.lock +++ b/example-new-architecture/ios/Podfile.lock @@ -5,6 +5,8 @@ PODS: - DatadogCrashReporting (3.1.0): - DatadogInternal (= 3.1.0) - PLCrashReporter (~> 1.12.0) + - DatadogFlags (3.1.0): + - DatadogInternal (= 3.1.0) - DatadogInternal (3.1.0) - DatadogLogs (3.1.0): - DatadogInternal (= 3.1.0) @@ -13,6 +15,7 @@ PODS: - DatadogSDKReactNative (2.12.1): - DatadogCore (= 3.1.0) - DatadogCrashReporting (= 3.1.0) + - DatadogFlags (= 3.1.0) - DatadogLogs (= 3.1.0) - DatadogRUM (= 3.1.0) - DatadogTrace (= 3.1.0) @@ -40,6 +43,7 @@ PODS: - DatadogSDKReactNative/Tests (2.12.1): - DatadogCore (= 3.1.0) - DatadogCrashReporting (= 3.1.0) + - DatadogFlags (= 3.1.0) - DatadogLogs (= 3.1.0) - DatadogRUM (= 3.1.0) - DatadogTrace (= 3.1.0) @@ -1634,6 +1638,9 @@ PODS: DEPENDENCIES: - boost (from `../node_modules/react-native/third-party-podspecs/boost.podspec`) + - DatadogFlags (from `https://github.com/DataDog/dd-sdk-ios.git`, branch `feature/flags`) + - DatadogInternal (from `https://github.com/DataDog/dd-sdk-ios.git`, branch `feature/flags`) + - DatadogRUM (from `https://github.com/DataDog/dd-sdk-ios.git`, branch `feature/flags`) - DatadogSDKReactNative (from `../../packages/core/DatadogSDKReactNative.podspec`) - DatadogSDKReactNative/Tests (from `../../packages/core/DatadogSDKReactNative.podspec`) - DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`) @@ -1706,9 +1713,7 @@ SPEC REPOS: https://github.com/CocoaPods/Specs.git: - DatadogCore - DatadogCrashReporting - - DatadogInternal - DatadogLogs - - DatadogRUM - DatadogTrace - DatadogWebViewTracking - OpenTelemetrySwiftApi @@ -1718,6 +1723,15 @@ SPEC REPOS: EXTERNAL SOURCES: boost: :podspec: "../node_modules/react-native/third-party-podspecs/boost.podspec" + DatadogFlags: + :branch: feature/flags + :git: https://github.com/DataDog/dd-sdk-ios.git + DatadogInternal: + :branch: feature/flags + :git: https://github.com/DataDog/dd-sdk-ios.git + DatadogRUM: + :branch: feature/flags + :git: https://github.com/DataDog/dd-sdk-ios.git DatadogSDKReactNative: :path: "../../packages/core/DatadogSDKReactNative.podspec" DoubleConversion: @@ -1848,14 +1862,26 @@ EXTERNAL SOURCES: Yoga: :path: "../node_modules/react-native/ReactCommon/yoga" +CHECKOUT OPTIONS: + DatadogFlags: + :commit: 18ee3ce825a9806c350133179f5e38168d78e0ba + :git: https://github.com/DataDog/dd-sdk-ios.git + DatadogInternal: + :commit: 18ee3ce825a9806c350133179f5e38168d78e0ba + :git: https://github.com/DataDog/dd-sdk-ios.git + DatadogRUM: + :commit: 18ee3ce825a9806c350133179f5e38168d78e0ba + :git: https://github.com/DataDog/dd-sdk-ios.git + SPEC CHECKSUMS: boost: 1dca942403ed9342f98334bf4c3621f011aa7946 DatadogCore: d2f51c7fb4308cf3c25e55e2e7242e5d558ee71d DatadogCrashReporting: f636f1d1c534572c0b0abcdc59df244c884d825d + DatadogFlags: d4237ffb9c06096d1928dbe47aac877739bc6326 DatadogInternal: 7837b2ce3d525d429682532eeda697b181299fdc DatadogLogs: 250894b5a99da5b924a019049c0d0326823cdbd6 DatadogRUM: 0d2a60e1abb8aacfb8827ef84f6d5deb4d5026c8 - DatadogSDKReactNative: 069ea9876220b2d09b0f4b180ce571b1b6ecbb35 + DatadogSDKReactNative: d2a4348b531e604e839ca13026a92728cc2dd5ec DatadogTrace: f59e933074cd285ad7e9f5af991f8fe04b095991 DatadogWebViewTracking: 9bc92b4147aeed47eb1911451f651094aa6dd6c1 DoubleConversion: f16ae600a246532c4020132d54af21d0ddb2a385 @@ -1866,65 +1892,65 @@ SPEC CHECKSUMS: hermes-engine: 9e868dc7be781364296d6ee2f56d0c1a9ef0bb11 OpenTelemetrySwiftApi: aaee576ed961e0c348af78df58b61300e95bd104 PLCrashReporter: db59ef96fa3d25f3650040d02ec2798cffee75f2 - RCT-Folly: ea9d9256ba7f9322ef911169a9f696e5857b9e17 + RCT-Folly: 7b4f73a92ad9571b9dbdb05bb30fad927fa971e1 RCTDeprecation: ebe712bb05077934b16c6bf25228bdec34b64f83 RCTRequired: ca91e5dd26b64f577b528044c962baf171c6b716 RCTTypeSafety: e7678bd60850ca5a41df9b8dc7154638cb66871f React: 4641770499c39f45d4e7cde1eba30e081f9d8a3d React-callinvoker: 4bef67b5c7f3f68db5929ab6a4d44b8a002998ea - React-Core: a68cea3e762814e60ecc3fa521c7f14c36c99245 - React-CoreModules: d81b1eaf8066add66299bab9d23c9f00c9484c7c - React-cxxreact: 984f8b1feeca37181d4e95301fcd6f5f6501c6ab + React-Core: 0a06707a0b34982efc4a556aff5dae4b22863455 + React-CoreModules: 907334e94314189c2e5eed4877f3efe7b26d85b0 + React-cxxreact: 3a1d5e8f4faa5e09be26614e9c8bbcae8d11b73d React-debug: 817160c07dc8d24d020fbd1eac7b3558ffc08964 - React-defaultsnativemodule: 18a684542f82ce1897552a1c4b847be414c9566e - React-domnativemodule: 90bdd4ec3ab38c47cfc3461c1e9283a8507d613f - React-Fabric: f6dade7007533daeb785ba5925039d83f343be4b - React-FabricComponents: b0655cc3e1b5ae12a4a1119aa7d8308f0ad33520 - React-FabricImage: 9b157c4c01ac2bf433f834f0e1e5fe234113a576 + React-defaultsnativemodule: 814830ccbc3fb08d67d0190e63b179ee4098c67b + React-domnativemodule: 270acf94bd0960b026bc3bfb327e703665d27fb4 + React-Fabric: 64586dc191fc1c170372a638b8e722e4f1d0a09b + React-FabricComponents: b0ebd032387468ea700574c581b139f57a7497fb + React-FabricImage: 81f0e0794caf25ad1224fa406d288fbc1986607f React-featureflags: f2792b067a351d86fdc7bec23db3b9a2f2c8d26c - 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-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-nativeconfig: 8efdb1ef1e9158c77098a93085438f7e7b463678 - React-NativeModulesApple: cebca2e5320a3d66e123cade23bd90a167ffce5e - React-perflogger: 72e653eb3aba9122f9e57cf012d22d2486f33358 - React-performancetimeline: cd6a9374a72001165995d2ab632f672df04076dc + React-NativeModulesApple: 958d4f6c5c2ace4c0f427cf7ef82e28ae6538a22 + React-perflogger: 9b4f13c0afe56bc7b4a0e93ec74b1150421ee22d + React-performancetimeline: 359db1cb889aa0282fafc5838331b0987c4915a9 React-RCTActionSheet: aacf2375084dea6e7c221f4a727e579f732ff342 - 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-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-rendererconsistency: b4917053ecbaa91469c67a4319701c9dc0d40be6 - React-rendererdebug: aa181c36dd6cf5b35511d1ed875d6638fd38f0ec + React-rendererdebug: 81becbc8852b38d9b1b68672aa504556481330d5 React-rncore: 120d21715c9b4ba8f798bffe986cb769b988dd74 - React-RuntimeApple: d033becbbd1eba6f9f6e3af6f1893030ce203edd - React-RuntimeCore: 38af280bb678e66ba000a3c3d42920b2a138eebb + React-RuntimeApple: 52ed0e9e84a7c2607a901149fb13599a3c057655 + React-RuntimeCore: ca6189d2e53d86db826e2673fe8af6571b8be157 React-runtimeexecutor: 877596f82f5632d073e121cba2d2084b76a76899 - React-RuntimeHermes: 37aad735ff21ca6de2d8450a96de1afe9f86c385 - React-runtimescheduler: 8ec34cc885281a34696ea16c4fd86892d631f38d + React-RuntimeHermes: 3b752dc5d8a1661c9d1687391d6d96acfa385549 + React-runtimescheduler: 8321bb09175ace2a4f0b3e3834637eb85bf42ebe React-timing: 331cbf9f2668c67faddfd2e46bb7f41cbd9320b9 - React-utils: ed818f19ab445000d6b5c4efa9d462449326cc9f - ReactCodegen: f853a20cc9125c5521c8766b4b49375fec20648b - ReactCommon: 300d8d9c5cb1a6cd79a67cf5d8f91e4d477195f9 + React-utils: 54df9ada708578c8ad40d92895d6fed03e0e8a9e + ReactCodegen: 21a52ccddc6479448fc91903a437dd23ddc7366c + ReactCommon: bfd3600989d79bc3acbe7704161b171a1480b9fd SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 Yoga: feb4910aba9742cfedc059e2b2902e22ffe9954a -PODFILE CHECKSUM: d9d720c99b6fffec4dd489d565a544a358a52b83 +PODFILE CHECKSUM: a25d6a4a713f5e05063f261c976c29ec22e26585 COCOAPODS: 1.16.2 diff --git a/packages/core/DatadogSDKReactNative.podspec b/packages/core/DatadogSDKReactNative.podspec index 3f5026f5a..b49ded1bd 100644 --- a/packages/core/DatadogSDKReactNative.podspec +++ b/packages/core/DatadogSDKReactNative.podspec @@ -24,6 +24,7 @@ Pod::Spec.new do |s| s.dependency 'DatadogTrace', '3.1.0' s.dependency 'DatadogRUM', '3.1.0' s.dependency 'DatadogCrashReporting', '3.1.0' + s.dependency 'DatadogFlags', '3.1.0' # DatadogWebViewTracking is not available for tvOS s.ios.dependency 'DatadogWebViewTracking', '3.1.0' From 10209fa3edbece5f16e5eef774ef654ca1d193a5 Mon Sep 17 00:00:00 2001 From: Sergio Barrio Date: Fri, 10 Oct 2025 14:52:21 +0200 Subject: [PATCH 025/526] 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 026/526] 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 027/526] 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 028/526] 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 029/526] 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 030/526] 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 031/526] 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 032/526] 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 033/526] 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 034/526] 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 035/526] 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 036/526] 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 037/526] 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 038/526] 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 039/526] 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 040/526] 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 041/526] 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 042/526] 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 043/526] 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 044/526] 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 045/526] 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 046/526] 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 047/526] 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 048/526] 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 049/526] 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 050/526] 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 051/526] 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 dbac6210d101a6a00df35e08e070b66170a058c2 Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Wed, 29 Oct 2025 21:07:51 +0200 Subject: [PATCH 052/526] Working flag assignment evaluation --- example-new-architecture/App.tsx | 16 +++- example-new-architecture/ios/Podfile | 12 ++- example-new-architecture/ios/Podfile.lock | 52 +++++++++---- .../core/ios/Sources/DatadogSDKReactNative.h | 2 + packages/core/ios/Sources/DdFlags.mm | 29 ++++++-- .../ios/Sources/DdFlagsImplementation.swift | 73 ++++++++++++++++++- .../Sources/DdSdkNativeInitialization.swift | 12 +++ .../src/DdSdkReactNativeConfiguration.tsx | 2 + packages/core/src/flags/DatadogFlags.ts | 31 ++++++++ packages/core/src/flags/DdFlags.ts | 24 ------ packages/core/src/flags/FlagsClient.ts | 52 +++++++++++++ packages/core/src/flags/types.ts | 25 +++++++ packages/core/src/index.tsx | 4 +- packages/core/src/specs/NativeDdFlags.ts | 18 ++++- 14 files changed, 294 insertions(+), 58 deletions(-) create mode 100644 packages/core/src/flags/DatadogFlags.ts delete mode 100644 packages/core/src/flags/DdFlags.ts create mode 100644 packages/core/src/flags/FlagsClient.ts create mode 100644 packages/core/src/flags/types.ts diff --git a/example-new-architecture/App.tsx b/example-new-architecture/App.tsx index 467e9e837..67c4ae020 100644 --- a/example-new-architecture/App.tsx +++ b/example-new-architecture/App.tsx @@ -8,7 +8,7 @@ import { RumActionType, DdLogs, DdTrace, - DdFlags, + DatadogFlags, } from '@datadog/mobile-react-native'; import React from 'react'; import type {PropsWithChildren} from 'react'; @@ -33,8 +33,6 @@ import { import {APPLICATION_ID, CLIENT_TOKEN, ENVIRONMENT} from './ddCredentials'; (async () => { - console.log({constant: await DdFlags.getConstant()}); - const config = new DdSdkReactNativeConfiguration( CLIENT_TOKEN, ENVIRONMENT, @@ -57,6 +55,18 @@ import {APPLICATION_ID, CLIENT_TOKEN, ENVIRONMENT} from './ddCredentials'; await DdLogs.info('info log'); const spanId = await DdTrace.startSpan('test span'); await DdTrace.finishSpan(spanId); + + const flagsClient = DatadogFlags.getClient(); + + await flagsClient.setEvaluationContext({ + targetingKey: 'test-user-1', + attributes: { + country: 'US', + }, + }); + + // Feature flag page: https://app.datadoghq.com/feature-flags/c715d5a6-939e-486e-be12-6f96cb36a018?environmentId=d114cd9a-79ed-4c56-bcf3-bcac9293653b + console.log({booleanValue: await flagsClient.getBooleanValue('dp-test-flag', false)}) })(); type SectionProps = PropsWithChildren<{ diff --git a/example-new-architecture/ios/Podfile b/example-new-architecture/ios/Podfile index ead08928f..b77b116d7 100644 --- a/example-new-architecture/ios/Podfile +++ b/example-new-architecture/ios/Podfile @@ -19,10 +19,14 @@ end target 'DdSdkReactNativeExample' do pod 'DatadogSDKReactNative', :path => '../../packages/core/DatadogSDKReactNative.podspec', :testspecs => ['Tests'] - # These dependencies are not yet released, so we need to use the branch from the feature/flags branch. - pod 'DatadogRUM', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :branch => 'feature/flags' - pod 'DatadogInternal', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :branch => 'feature/flags' - pod 'DatadogFlags', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :branch => 'feature/flags' + # Flags don't seem to be released yet so we pull them from the iOS SDK develop branch. + pod 'DatadogInternal', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :branch => 'develop' + pod 'DatadogCore', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :branch => 'develop' + pod 'DatadogLogs', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :branch => 'develop' + pod 'DatadogTrace', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :branch => 'develop' + pod 'DatadogRUM', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :branch => 'develop' + pod 'DatadogCrashReporting', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :branch => 'develop' + pod 'DatadogFlags', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :branch => 'develop' config = use_native_modules! diff --git a/example-new-architecture/ios/Podfile.lock b/example-new-architecture/ios/Podfile.lock index 99ad9a04a..26890694a 100644 --- a/example-new-architecture/ios/Podfile.lock +++ b/example-new-architecture/ios/Podfile.lock @@ -1638,11 +1638,15 @@ PODS: DEPENDENCIES: - boost (from `../node_modules/react-native/third-party-podspecs/boost.podspec`) - - DatadogFlags (from `https://github.com/DataDog/dd-sdk-ios.git`, branch `feature/flags`) - - DatadogInternal (from `https://github.com/DataDog/dd-sdk-ios.git`, branch `feature/flags`) - - DatadogRUM (from `https://github.com/DataDog/dd-sdk-ios.git`, branch `feature/flags`) + - DatadogCore (from `https://github.com/DataDog/dd-sdk-ios.git`, branch `develop`) + - DatadogCrashReporting (from `https://github.com/DataDog/dd-sdk-ios.git`, branch `develop`) + - DatadogFlags (from `https://github.com/DataDog/dd-sdk-ios.git`, branch `develop`) + - DatadogInternal (from `https://github.com/DataDog/dd-sdk-ios.git`, branch `develop`) + - DatadogLogs (from `https://github.com/DataDog/dd-sdk-ios.git`, branch `develop`) + - DatadogRUM (from `https://github.com/DataDog/dd-sdk-ios.git`, branch `develop`) - DatadogSDKReactNative (from `../../packages/core/DatadogSDKReactNative.podspec`) - DatadogSDKReactNative/Tests (from `../../packages/core/DatadogSDKReactNative.podspec`) + - DatadogTrace (from `https://github.com/DataDog/dd-sdk-ios.git`, branch `develop`) - DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`) - fast_float (from `../node_modules/react-native/third-party-podspecs/fast_float.podspec`) - FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`) @@ -1711,10 +1715,6 @@ DEPENDENCIES: SPEC REPOS: https://github.com/CocoaPods/Specs.git: - - DatadogCore - - DatadogCrashReporting - - DatadogLogs - - DatadogTrace - DatadogWebViewTracking - OpenTelemetrySwiftApi - PLCrashReporter @@ -1723,17 +1723,29 @@ SPEC REPOS: EXTERNAL SOURCES: boost: :podspec: "../node_modules/react-native/third-party-podspecs/boost.podspec" + DatadogCore: + :branch: develop + :git: https://github.com/DataDog/dd-sdk-ios.git + DatadogCrashReporting: + :branch: develop + :git: https://github.com/DataDog/dd-sdk-ios.git DatadogFlags: - :branch: feature/flags + :branch: develop :git: https://github.com/DataDog/dd-sdk-ios.git DatadogInternal: - :branch: feature/flags + :branch: develop + :git: https://github.com/DataDog/dd-sdk-ios.git + DatadogLogs: + :branch: develop :git: https://github.com/DataDog/dd-sdk-ios.git DatadogRUM: - :branch: feature/flags + :branch: develop :git: https://github.com/DataDog/dd-sdk-ios.git DatadogSDKReactNative: :path: "../../packages/core/DatadogSDKReactNative.podspec" + DatadogTrace: + :branch: develop + :git: https://github.com/DataDog/dd-sdk-ios.git DoubleConversion: :podspec: "../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec" fast_float: @@ -1863,14 +1875,26 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/ReactCommon/yoga" CHECKOUT OPTIONS: + DatadogCore: + :commit: 00bb35ce9eb9b50b37cf725f29b0a96da9fa46f1 + :git: https://github.com/DataDog/dd-sdk-ios.git + DatadogCrashReporting: + :commit: 00bb35ce9eb9b50b37cf725f29b0a96da9fa46f1 + :git: https://github.com/DataDog/dd-sdk-ios.git DatadogFlags: - :commit: 18ee3ce825a9806c350133179f5e38168d78e0ba + :commit: 00bb35ce9eb9b50b37cf725f29b0a96da9fa46f1 :git: https://github.com/DataDog/dd-sdk-ios.git DatadogInternal: - :commit: 18ee3ce825a9806c350133179f5e38168d78e0ba + :commit: 00bb35ce9eb9b50b37cf725f29b0a96da9fa46f1 + :git: https://github.com/DataDog/dd-sdk-ios.git + DatadogLogs: + :commit: 00bb35ce9eb9b50b37cf725f29b0a96da9fa46f1 :git: https://github.com/DataDog/dd-sdk-ios.git DatadogRUM: - :commit: 18ee3ce825a9806c350133179f5e38168d78e0ba + :commit: 00bb35ce9eb9b50b37cf725f29b0a96da9fa46f1 + :git: https://github.com/DataDog/dd-sdk-ios.git + DatadogTrace: + :commit: 00bb35ce9eb9b50b37cf725f29b0a96da9fa46f1 :git: https://github.com/DataDog/dd-sdk-ios.git SPEC CHECKSUMS: @@ -1951,6 +1975,6 @@ SPEC CHECKSUMS: SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 Yoga: feb4910aba9742cfedc059e2b2902e22ffe9954a -PODFILE CHECKSUM: a25d6a4a713f5e05063f261c976c29ec22e26585 +PODFILE CHECKSUM: 0371740c3ef15f157e2090328ec6d4963ce43cd5 COCOAPODS: 1.16.2 diff --git a/packages/core/ios/Sources/DatadogSDKReactNative.h b/packages/core/ios/Sources/DatadogSDKReactNative.h index bb724670c..0144fd508 100644 --- a/packages/core/ios/Sources/DatadogSDKReactNative.h +++ b/packages/core/ios/Sources/DatadogSDKReactNative.h @@ -6,3 +6,5 @@ // This file is imported in the auto-generated DatadogSDKReactNative-Swift.h header file. // Deleting it could result in iOS builds failing. + +#import \ No newline at end of file diff --git a/packages/core/ios/Sources/DdFlags.mm b/packages/core/ios/Sources/DdFlags.mm index a8724d525..bc5104a92 100644 --- a/packages/core/ios/Sources/DdFlags.mm +++ b/packages/core/ios/Sources/DdFlags.mm @@ -16,11 +16,24 @@ @implementation DdFlags RCT_EXPORT_MODULE() -// FIXME: This is a temporary method to test whether the native library setup is working. -RCT_REMAP_METHOD(getConstant, withResolve:(RCTPromiseResolveBlock)resolve - withRejecter:(RCTPromiseRejectBlock)reject) +RCT_REMAP_METHOD(setEvaluationContext, + withClientName:(NSString *)clientName + withTargetingKey:(NSString *)targetingKey + withAttributes:(NSDictionary *)attributes + withResolve:(RCTPromiseResolveBlock)resolve + withReject:(RCTPromiseRejectBlock)reject) { - [self getConstant:resolve reject:reject]; + [self setEvaluationContext:clientName targetingKey:targetingKey attributes:attributes resolve:resolve reject:reject]; +} + +RCT_REMAP_METHOD(getBooleanValue, + withClientName:(NSString *)clientName + withKey:(NSString *)key + withDefaultValue:(BOOL)defaultValue + withResolve:(RCTPromiseResolveBlock)resolve + withReject:(RCTPromiseRejectBlock)reject) +{ + [self getBooleanValue:clientName key:key defaultValue:defaultValue resolve:resolve reject:reject]; } // Thanks to this guard, we won't compile this code when we build for the new architecture. @@ -48,8 +61,12 @@ - (dispatch_queue_t)methodQueue { return [RNQueue getSharedQueue]; } -- (void)getConstant:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { - [self.ddFlagsImplementation getConstant:resolve reject:reject]; +- (void)setEvaluationContext:(NSString *)clientName targetingKey:(NSString *)targetingKey attributes:(NSDictionary *)attributes resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + [self.ddFlagsImplementation setEvaluationContext:clientName targetingKey:targetingKey attributes:attributes resolve:resolve reject:reject]; +} + +- (void)getBooleanValue:(NSString *)clientName key:(NSString *)key defaultValue:(BOOL)defaultValue resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + [self.ddFlagsImplementation getBooleanValue:clientName key:key defaultValue:defaultValue resolve:resolve reject:reject]; } @end diff --git a/packages/core/ios/Sources/DdFlagsImplementation.swift b/packages/core/ios/Sources/DdFlagsImplementation.swift index 6736c8153..da4f5a914 100644 --- a/packages/core/ios/Sources/DdFlagsImplementation.swift +++ b/packages/core/ios/Sources/DdFlagsImplementation.swift @@ -5,12 +5,79 @@ */ import Foundation +import DatadogFlags @objc public class DdFlagsImplementation: NSObject { + // Store a registry of client providers by name + // Use providers instead of direct clients to ensure lazy initialization + private var clientProviders: [String: () -> FlagsClientProtocol] = [:] + + private func getClient(name: String) -> FlagsClientProtocol { + if let provider = clientProviders[name] { + return provider() + } + + let client = FlagsClient.create(name: name) + + clientProviders[name] = { FlagsClient.shared(named: name) } + + return client + } + + private func parseAttributes(attributes: NSDictionary) -> [String: AnyValue] { + func asAnyValue(value: Any) -> AnyValue { + switch value { + case let s as String: return .string(s) + case let b as Bool: return .bool(b) + case let i as Int: return .int(i) + case let d as Double: return .double(d) + // FIXME: Do we even support nested evaluation contexts? + case let dict as NSDictionary: return .dictionary(parseAttributes(attributes: dict)) + case let arr as NSArray: return .array(arr.compactMap(asAnyValue)) + case is NSNull: return .null + default: return .null + } + } + + var result: [String: AnyValue] = [:] + for (key, value) in attributes { + guard let stringKey = key as? String else { + continue + } + result[stringKey] = asAnyValue(value: value) + } + return result + } + @objc - public func getConstant(_ resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) -> Void { - // FIXME: This is a temporary method to test whether the native library setup is working. - resolve(43) + public func setEvaluationContext(_ clientName: String, targetingKey: String, attributes: NSDictionary, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { + let client = getClient(name: clientName) + + let evaluationContext = FlagsEvaluationContext(targetingKey: targetingKey, attributes: parseAttributes(attributes: attributes)) + + client.setEvaluationContext(evaluationContext) { result in + switch result { + case .success: + resolve(nil) + case .failure(let error): + reject(error.localizedDescription, "", error) + } + } + } + + @objc + public func getBooleanValue( + _ clientName: String, + key: String, + defaultValue: Bool, + resolve: RCTPromiseResolveBlock, + reject: RCTPromiseRejectBlock + ) { + let client = getClient(name: clientName) + + let value = client.getBooleanValue(key: key, defaultValue: defaultValue) + + resolve(value) } } diff --git a/packages/core/ios/Sources/DdSdkNativeInitialization.swift b/packages/core/ios/Sources/DdSdkNativeInitialization.swift index 7b0f1be19..4341fdb4f 100644 --- a/packages/core/ios/Sources/DdSdkNativeInitialization.swift +++ b/packages/core/ios/Sources/DdSdkNativeInitialization.swift @@ -6,6 +6,7 @@ import Foundation import DatadogCore +import DatadogFlags import DatadogRUM import DatadogLogs import DatadogTrace @@ -87,6 +88,9 @@ public class DdSdkNativeInitialization: NSObject { let traceConfig = buildTraceConfiguration(configuration: sdkConfiguration) Trace.enable(with: traceConfig) + let flagsConfig = buildFlagsConfiguration(configuration: sdkConfiguration) + Flags.enable(with: flagsConfig) + if sdkConfiguration.nativeCrashReportEnabled ?? false { CrashReporting.enable() } @@ -215,6 +219,14 @@ public class DdSdkNativeInitialization: NSObject { return Trace.Configuration(customEndpoint: customTraceEndpointURL) } + func buildFlagsConfiguration(configuration: DdSdkConfiguration) -> Flags.Configuration { + // TODO: Handle flags configuration. + + return Flags.Configuration( + gracefulModeEnabled: true + ) + } + func setVerbosityLevel(configuration: DdSdkConfiguration) { switch configuration.verbosity?.lowercased { case "debug": diff --git a/packages/core/src/DdSdkReactNativeConfiguration.tsx b/packages/core/src/DdSdkReactNativeConfiguration.tsx index 44debb2d2..425cd7cd1 100644 --- a/packages/core/src/DdSdkReactNativeConfiguration.tsx +++ b/packages/core/src/DdSdkReactNativeConfiguration.tsx @@ -358,6 +358,8 @@ export class DdSdkReactNativeConfiguration { public customEndpoints: CustomEndpoints = DEFAULTS.getCustomEndpoints(); + // TODO: Handle flags configuration. + constructor( readonly clientToken: string, readonly env: string, diff --git a/packages/core/src/flags/DatadogFlags.ts b/packages/core/src/flags/DatadogFlags.ts new file mode 100644 index 000000000..e97b2974c --- /dev/null +++ b/packages/core/src/flags/DatadogFlags.ts @@ -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'; + +import { FlagsClient } from './FlagsClient'; +import type { DatadogFlagsConfiguration } from './types'; + +class DatadogFlagsWrapper { + getClient = (clientName: string = 'default'): FlagsClient => { + return new FlagsClient(clientName); + }; + + enable = async ( + _configuration: DatadogFlagsConfiguration + ): Promise => { + InternalLog.log( + 'No-op DatadogFlags.enable() called. Flags are initialized globally by default for now.', + SdkVerbosity.DEBUG + ); + + return Promise.resolve(); + }; +} + +const DatadogFlags = new DatadogFlagsWrapper(); + +export { DatadogFlags }; diff --git a/packages/core/src/flags/DdFlags.ts b/packages/core/src/flags/DdFlags.ts deleted file mode 100644 index 8c2680329..000000000 --- a/packages/core/src/flags/DdFlags.ts +++ /dev/null @@ -1,24 +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 { InternalLog } from '../InternalLog'; -import { SdkVerbosity } from '../SdkVerbosity'; -import type { DdNativeFlagsType } from '../nativeModulesTypes'; - -class DdFlagsWrapper { - // eslint-disable-next-line global-require, @typescript-eslint/no-var-requires - private nativeFlags: DdNativeFlagsType = require('../specs/NativeDdFlags') - .default; - - getConstant = (): Promise => { - InternalLog.log('Flags.getConstant()', SdkVerbosity.DEBUG); - - return this.nativeFlags.getConstant(); - }; -} - -const DdFlags = new DdFlagsWrapper(); - -export { DdFlags }; diff --git a/packages/core/src/flags/FlagsClient.ts b/packages/core/src/flags/FlagsClient.ts new file mode 100644 index 000000000..b6f5ea275 --- /dev/null +++ b/packages/core/src/flags/FlagsClient.ts @@ -0,0 +1,52 @@ +/* + * 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 type { DdNativeFlagsType } from '../nativeModulesTypes'; + +import type { EvaluationContext } from './types'; + +export class FlagsClient { + // eslint-disable-next-line global-require, @typescript-eslint/no-var-requires + private nativeFlags: DdNativeFlagsType = require('../specs/NativeDdFlags') + .default; + + private clientName: string; + + constructor(clientName: string = 'default') { + this.clientName = clientName; + } + + setEvaluationContext = async ( + context: EvaluationContext + ): Promise => { + const { targetingKey, attributes } = context; + + await this.nativeFlags.setEvaluationContext( + this.clientName, + targetingKey, + attributes + ); + }; + + getBooleanValue = async ( + key: string, + defaultValue: boolean + ): Promise => { + InternalLog.log( + `Flags.getBooleanValue(${key}, ${defaultValue})`, + SdkVerbosity.DEBUG + ); + + const value = await this.nativeFlags.getBooleanValue( + this.clientName, + key, + defaultValue + ); + + return value; + }; +} diff --git a/packages/core/src/flags/types.ts b/packages/core/src/flags/types.ts new file mode 100644 index 000000000..2aa5effb4 --- /dev/null +++ b/packages/core/src/flags/types.ts @@ -0,0 +1,25 @@ +/* + * 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. + */ + +/** + * Evaluation context for flags. + */ +export interface EvaluationContext { + targetingKey: string; + attributes: Record; +} + +/** + * Configuration settings for flags. + */ +export interface DatadogFlagsConfiguration { + gracefulModeEnabled?: boolean; + customFlagsEndpoint?: string; + customFlagsHeaders?: Record; + customExposureEndpoint?: string; + trackExposures?: boolean; + rumIntegrationEnabled?: boolean; +} diff --git a/packages/core/src/index.tsx b/packages/core/src/index.tsx index 4d9e9695e..b95c2c10a 100644 --- a/packages/core/src/index.tsx +++ b/packages/core/src/index.tsx @@ -21,7 +21,7 @@ import { InternalLog } from './InternalLog'; import { ProxyConfiguration, ProxyType } from './ProxyConfiguration'; import { SdkVerbosity } from './SdkVerbosity'; import { TrackingConsent } from './TrackingConsent'; -import { DdFlags } from './flags/DdFlags'; +import { DatadogFlags } from './flags/DatadogFlags'; import { DdLogs } from './logs/DdLogs'; import { DdRum } from './rum/DdRum'; import { DdBabelInteractionTracking } from './rum/instrumentation/interactionTracking/DdBabelInteractionTracking'; @@ -54,7 +54,7 @@ export { FileBasedConfiguration, InitializationMode, DdLogs, - DdFlags, + DatadogFlags, DdTrace, DdRum, RumActionType, diff --git a/packages/core/src/specs/NativeDdFlags.ts b/packages/core/src/specs/NativeDdFlags.ts index 3c06e7c96..130bb8e86 100644 --- a/packages/core/src/specs/NativeDdFlags.ts +++ b/packages/core/src/specs/NativeDdFlags.ts @@ -12,8 +12,22 @@ import { TurboModuleRegistry } from 'react-native'; * Do not import this Spec directly, use DdNativeFlagsType instead. */ export interface Spec extends TurboModule { - // TODO: This is a temporary method to test whether the native library setup is working. - readonly getConstant: () => Promise; + // TODO: Flags and all other features are initialized globally for now. We want to change this in the future. + // readonly enable: ( + // configuration: DatadogFlagsConfiguration + // ) => Promise; + + readonly setEvaluationContext: ( + clientName: string, + targetingKey: string, + attributes: { [key: string]: unknown } + ) => Promise; + + readonly getBooleanValue: ( + clientName: string, + key: string, + defaultValue: boolean + ) => Promise; } // eslint-disable-next-line import/no-default-export From 58d40aef8b242e80bbe5a243511bc256e18bc744 Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Tue, 4 Nov 2025 19:39:48 +0200 Subject: [PATCH 053/526] Pass configuration from RN to Swift SDK --- example-new-architecture/App.tsx | 3 ++ .../core/ios/Sources/DdSdkConfiguration.swift | 6 ++- .../Sources/DdSdkNativeInitialization.swift | 13 ++---- .../ios/Sources/RNDdSdkConfiguration.swift | 42 +++++++++++++++++-- packages/core/src/DdSdkReactNative.tsx | 3 +- .../src/DdSdkReactNativeConfiguration.tsx | 3 +- packages/core/src/flags/types.ts | 1 + packages/core/src/types.tsx | 4 +- 8 files changed, 58 insertions(+), 17 deletions(-) diff --git a/example-new-architecture/App.tsx b/example-new-architecture/App.tsx index 67c4ae020..badd7700d 100644 --- a/example-new-architecture/App.tsx +++ b/example-new-architecture/App.tsx @@ -46,6 +46,9 @@ import {APPLICATION_ID, CLIENT_TOKEN, ENVIRONMENT} from './ddCredentials'; config.telemetrySampleRate = 100; config.uploadFrequency = UploadFrequency.FREQUENT; config.batchSize = BatchSize.SMALL; + config.flagsConfiguration = { + enabled: true, + }; await DdSdkReactNative.initialize(config); await DdRum.startView('main', 'Main'); setTimeout(async () => { diff --git a/packages/core/ios/Sources/DdSdkConfiguration.swift b/packages/core/ios/Sources/DdSdkConfiguration.swift index 7a76cf39d..823783483 100644 --- a/packages/core/ios/Sources/DdSdkConfiguration.swift +++ b/packages/core/ios/Sources/DdSdkConfiguration.swift @@ -6,6 +6,7 @@ import Foundation import DatadogCore +import DatadogFlags import DatadogInternal import DatadogRUM @@ -76,6 +77,7 @@ public class DdSdkConfiguration: NSObject { public var trackWatchdogTerminations: Bool public var batchProcessingLevel: Datadog.Configuration.BatchProcessingLevel public var initialResourceThreshold: Double? = nil + public var configurationForFlags: Flags.Configuration? = nil public init( clientToken: String, @@ -108,7 +110,8 @@ public class DdSdkConfiguration: NSObject { appHangThreshold: Double?, trackWatchdogTerminations: Bool, batchProcessingLevel: Datadog.Configuration.BatchProcessingLevel, - initialResourceThreshold: Double? + initialResourceThreshold: Double?, + configurationForFlags: Flags.Configuration? ) { self.clientToken = clientToken self.env = env @@ -141,6 +144,7 @@ public class DdSdkConfiguration: NSObject { self.trackWatchdogTerminations = trackWatchdogTerminations self.batchProcessingLevel = batchProcessingLevel self.initialResourceThreshold = initialResourceThreshold + self.configurationForFlags = configurationForFlags } } diff --git a/packages/core/ios/Sources/DdSdkNativeInitialization.swift b/packages/core/ios/Sources/DdSdkNativeInitialization.swift index 4341fdb4f..943b40d36 100644 --- a/packages/core/ios/Sources/DdSdkNativeInitialization.swift +++ b/packages/core/ios/Sources/DdSdkNativeInitialization.swift @@ -88,8 +88,9 @@ public class DdSdkNativeInitialization: NSObject { let traceConfig = buildTraceConfiguration(configuration: sdkConfiguration) Trace.enable(with: traceConfig) - let flagsConfig = buildFlagsConfiguration(configuration: sdkConfiguration) - Flags.enable(with: flagsConfig) + if let configurationForFlags = sdkConfiguration.configurationForFlags { + Flags.enable(with: configurationForFlags) + } if sdkConfiguration.nativeCrashReportEnabled ?? false { CrashReporting.enable() @@ -219,14 +220,6 @@ public class DdSdkNativeInitialization: NSObject { return Trace.Configuration(customEndpoint: customTraceEndpointURL) } - func buildFlagsConfiguration(configuration: DdSdkConfiguration) -> Flags.Configuration { - // TODO: Handle flags configuration. - - return Flags.Configuration( - gracefulModeEnabled: true - ) - } - func setVerbosityLevel(configuration: DdSdkConfiguration) { switch configuration.verbosity?.lowercased { case "debug": diff --git a/packages/core/ios/Sources/RNDdSdkConfiguration.swift b/packages/core/ios/Sources/RNDdSdkConfiguration.swift index 66437c481..123b0e535 100644 --- a/packages/core/ios/Sources/RNDdSdkConfiguration.swift +++ b/packages/core/ios/Sources/RNDdSdkConfiguration.swift @@ -5,6 +5,7 @@ */ import DatadogCore +import DatadogFlags import DatadogRUM import DatadogInternal import Foundation @@ -43,6 +44,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 configurationForFlags = object(forKey: "configurationForFlags") as? NSDictionary return DdSdkConfiguration( clientToken: (clientToken != nil) ? clientToken! : String(), @@ -75,7 +77,8 @@ extension NSDictionary { appHangThreshold: appHangThreshold, trackWatchdogTerminations: trackWatchdogTerminations ?? DefaultConfiguration.trackWatchdogTerminations, batchProcessingLevel: batchProcessingLevel.asBatchProcessingLevel(), - initialResourceThreshold: initialResourceThreshold + initialResourceThreshold: initialResourceThreshold, + configurationForFlags: configurationForFlags?.asConfigurationForFlags() ) } @@ -96,7 +99,38 @@ extension NSDictionary { reactNativeVersion: reactNativeVersion ) } - + + func asConfigurationForFlags() -> Flags.Configuration? { + let enabled = object(forKey: "enabled") as! Bool + + if !enabled { + return nil + } + + let gracefulModeEnabled = object(forKey: "gracefulModeEnabled") as? Bool + let customFlagsHeaders = object(forKey: "customFlagsHeaders") as? [String: String] + let trackExposures = object(forKey: "trackExposures") as? Bool + let rumIntegrationEnabled = object(forKey: "rumIntegrationEnabled") as? Bool + + var customFlagsEndpointURL: URL? = nil + if let customFlagsEndpoint = object(forKey: "customFlagsEndpoint") as? String { + customFlagsEndpointURL = URL(string: "\(customFlagsEndpoint)/precompute-assignments" as String) + } + var customExposureEndpointURL: URL? = nil + if let customExposureEndpoint = object(forKey: "customExposureEndpoint") as? String { + customExposureEndpointURL = URL(string: "\(customExposureEndpoint)/api/v2/exposures" as String) + } + + return Flags.Configuration( + gracefulModeEnabled: gracefulModeEnabled ?? true, + customFlagsEndpoint: customFlagsEndpointURL, + customFlagsHeaders: customFlagsHeaders, + customExposureEndpoint: customExposureEndpointURL, + trackExposures: trackExposures ?? true, + rumIntegrationEnabled: rumIntegrationEnabled ?? true + ) + } + func asCustomEndpoints() -> CustomEndpoints { let rum = object(forKey: "rum") as? NSString let logs = object(forKey: "logs") as? NSString @@ -244,6 +278,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 configurationForFlags = configuration["configurationForFlags"] as? NSDictionary return DdSdkConfiguration( clientToken: clientToken ?? String(), @@ -279,7 +314,8 @@ extension Dictionary where Key == String, Value == AnyObject { appHangThreshold: appHangThreshold, trackWatchdogTerminations: trackWatchdogTerminations ?? DefaultConfiguration.trackWatchdogTerminations, batchProcessingLevel: batchProcessingLevel.asBatchProcessingLevel(), - initialResourceThreshold: initialResourceThreshold + initialResourceThreshold: initialResourceThreshold, + configurationForFlags: configurationForFlags?.asConfigurationForFlags() ) } } diff --git a/packages/core/src/DdSdkReactNative.tsx b/packages/core/src/DdSdkReactNative.tsx index 1838542df..7dd9aecd7 100644 --- a/packages/core/src/DdSdkReactNative.tsx +++ b/packages/core/src/DdSdkReactNative.tsx @@ -356,7 +356,8 @@ export class DdSdkReactNative { configuration.resourceTracingSamplingRate, configuration.trackWatchdogTerminations, configuration.batchProcessingLevel, - configuration.initialResourceThreshold + configuration.initialResourceThreshold, + configuration.flagsConfiguration ); }; diff --git a/packages/core/src/DdSdkReactNativeConfiguration.tsx b/packages/core/src/DdSdkReactNativeConfiguration.tsx index 425cd7cd1..01db282c8 100644 --- a/packages/core/src/DdSdkReactNativeConfiguration.tsx +++ b/packages/core/src/DdSdkReactNativeConfiguration.tsx @@ -7,6 +7,7 @@ import type { ProxyConfiguration } from './ProxyConfiguration'; import type { SdkVerbosity } from './SdkVerbosity'; import { TrackingConsent } from './TrackingConsent'; +import type { DatadogFlagsConfiguration } from './flags/types'; import type { ActionEventMapper } from './rum/eventMappers/actionEventMapper'; import type { ErrorEventMapper } from './rum/eventMappers/errorEventMapper'; import type { ResourceEventMapper } from './rum/eventMappers/resourceEventMapper'; @@ -358,7 +359,7 @@ export class DdSdkReactNativeConfiguration { public customEndpoints: CustomEndpoints = DEFAULTS.getCustomEndpoints(); - // TODO: Handle flags configuration. + public flagsConfiguration?: DatadogFlagsConfiguration; constructor( readonly clientToken: string, diff --git a/packages/core/src/flags/types.ts b/packages/core/src/flags/types.ts index 2aa5effb4..e7ef4a081 100644 --- a/packages/core/src/flags/types.ts +++ b/packages/core/src/flags/types.ts @@ -16,6 +16,7 @@ export interface EvaluationContext { * Configuration settings for flags. */ export interface DatadogFlagsConfiguration { + enabled: boolean; gracefulModeEnabled?: boolean; customFlagsEndpoint?: string; customFlagsHeaders?: Record; diff --git a/packages/core/src/types.tsx b/packages/core/src/types.tsx index bad19d429..5a0289e21 100644 --- a/packages/core/src/types.tsx +++ b/packages/core/src/types.tsx @@ -5,6 +5,7 @@ */ import type { BatchProcessingLevel } from './DdSdkReactNativeConfiguration'; +import type { DatadogFlagsConfiguration } from './flags/types'; declare global { // eslint-disable-next-line no-var, vars-on-top @@ -69,7 +70,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 configurationForFlags: DatadogFlagsConfiguration | undefined ) {} } From 86cf5ab141edae8664d3e31657d196ad1e976cc4 Mon Sep 17 00:00:00 2001 From: Carlos Nogueira Date: Wed, 5 Nov 2025 15:58:05 +0000 Subject: [PATCH 054/526] 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 ab48ffe7f10a47a677c1f6e0207a3c3e45f22b81 Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Thu, 13 Nov 2025 16:29:59 +0200 Subject: [PATCH 055/526] Pin Datadog* ios packages to a specific commit --- example-new-architecture/ios/Podfile | 14 +++---- example-new-architecture/ios/Podfile.lock | 46 +++++++++++------------ 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/example-new-architecture/ios/Podfile b/example-new-architecture/ios/Podfile index b77b116d7..3de68ea46 100644 --- a/example-new-architecture/ios/Podfile +++ b/example-new-architecture/ios/Podfile @@ -20,13 +20,13 @@ target 'DdSdkReactNativeExample' do pod 'DatadogSDKReactNative', :path => '../../packages/core/DatadogSDKReactNative.podspec', :testspecs => ['Tests'] # Flags don't seem to be released yet so we pull them from the iOS SDK develop branch. - pod 'DatadogInternal', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :branch => 'develop' - pod 'DatadogCore', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :branch => 'develop' - pod 'DatadogLogs', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :branch => 'develop' - pod 'DatadogTrace', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :branch => 'develop' - pod 'DatadogRUM', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :branch => 'develop' - pod 'DatadogCrashReporting', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :branch => 'develop' - pod 'DatadogFlags', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :branch => 'develop' + pod 'DatadogInternal', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :commit => '00bb35ce9' + pod 'DatadogCore', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :commit => '00bb35ce9' + pod 'DatadogLogs', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :commit => '00bb35ce9' + pod 'DatadogTrace', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :commit => '00bb35ce9' + pod 'DatadogRUM', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :commit => '00bb35ce9' + pod 'DatadogCrashReporting', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :commit => '00bb35ce9' + pod 'DatadogFlags', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :commit => '00bb35ce9' config = use_native_modules! diff --git a/example-new-architecture/ios/Podfile.lock b/example-new-architecture/ios/Podfile.lock index ce0b1f972..0de14a56a 100644 --- a/example-new-architecture/ios/Podfile.lock +++ b/example-new-architecture/ios/Podfile.lock @@ -1638,15 +1638,15 @@ PODS: DEPENDENCIES: - boost (from `../node_modules/react-native/third-party-podspecs/boost.podspec`) - - DatadogCore (from `https://github.com/DataDog/dd-sdk-ios.git`, branch `develop`) - - DatadogCrashReporting (from `https://github.com/DataDog/dd-sdk-ios.git`, branch `develop`) - - DatadogFlags (from `https://github.com/DataDog/dd-sdk-ios.git`, branch `develop`) - - DatadogInternal (from `https://github.com/DataDog/dd-sdk-ios.git`, branch `develop`) - - DatadogLogs (from `https://github.com/DataDog/dd-sdk-ios.git`, branch `develop`) - - DatadogRUM (from `https://github.com/DataDog/dd-sdk-ios.git`, branch `develop`) + - DatadogCore (from `https://github.com/DataDog/dd-sdk-ios.git`, commit `00bb35ce9`) + - DatadogCrashReporting (from `https://github.com/DataDog/dd-sdk-ios.git`, commit `00bb35ce9`) + - DatadogFlags (from `https://github.com/DataDog/dd-sdk-ios.git`, commit `00bb35ce9`) + - DatadogInternal (from `https://github.com/DataDog/dd-sdk-ios.git`, commit `00bb35ce9`) + - DatadogLogs (from `https://github.com/DataDog/dd-sdk-ios.git`, commit `00bb35ce9`) + - DatadogRUM (from `https://github.com/DataDog/dd-sdk-ios.git`, commit `00bb35ce9`) - DatadogSDKReactNative (from `../../packages/core/DatadogSDKReactNative.podspec`) - DatadogSDKReactNative/Tests (from `../../packages/core/DatadogSDKReactNative.podspec`) - - DatadogTrace (from `https://github.com/DataDog/dd-sdk-ios.git`, branch `develop`) + - DatadogTrace (from `https://github.com/DataDog/dd-sdk-ios.git`, commit `00bb35ce9`) - DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`) - fast_float (from `../node_modules/react-native/third-party-podspecs/fast_float.podspec`) - FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`) @@ -1724,27 +1724,27 @@ EXTERNAL SOURCES: boost: :podspec: "../node_modules/react-native/third-party-podspecs/boost.podspec" DatadogCore: - :branch: develop + :commit: 00bb35ce9 :git: https://github.com/DataDog/dd-sdk-ios.git DatadogCrashReporting: - :branch: develop + :commit: 00bb35ce9 :git: https://github.com/DataDog/dd-sdk-ios.git DatadogFlags: - :branch: develop + :commit: 00bb35ce9 :git: https://github.com/DataDog/dd-sdk-ios.git DatadogInternal: - :branch: develop + :commit: 00bb35ce9 :git: https://github.com/DataDog/dd-sdk-ios.git DatadogLogs: - :branch: develop + :commit: 00bb35ce9 :git: https://github.com/DataDog/dd-sdk-ios.git DatadogRUM: - :branch: develop + :commit: 00bb35ce9 :git: https://github.com/DataDog/dd-sdk-ios.git DatadogSDKReactNative: :path: "../../packages/core/DatadogSDKReactNative.podspec" DatadogTrace: - :branch: develop + :commit: 00bb35ce9 :git: https://github.com/DataDog/dd-sdk-ios.git DoubleConversion: :podspec: "../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec" @@ -1876,25 +1876,25 @@ EXTERNAL SOURCES: CHECKOUT OPTIONS: DatadogCore: - :commit: 00bb35ce9eb9b50b37cf725f29b0a96da9fa46f1 + :commit: 00bb35ce9 :git: https://github.com/DataDog/dd-sdk-ios.git DatadogCrashReporting: - :commit: 00bb35ce9eb9b50b37cf725f29b0a96da9fa46f1 + :commit: 00bb35ce9 :git: https://github.com/DataDog/dd-sdk-ios.git DatadogFlags: - :commit: 00bb35ce9eb9b50b37cf725f29b0a96da9fa46f1 + :commit: 00bb35ce9 :git: https://github.com/DataDog/dd-sdk-ios.git DatadogInternal: - :commit: 00bb35ce9eb9b50b37cf725f29b0a96da9fa46f1 + :commit: 00bb35ce9 :git: https://github.com/DataDog/dd-sdk-ios.git DatadogLogs: - :commit: 00bb35ce9eb9b50b37cf725f29b0a96da9fa46f1 + :commit: 00bb35ce9 :git: https://github.com/DataDog/dd-sdk-ios.git DatadogRUM: - :commit: 00bb35ce9eb9b50b37cf725f29b0a96da9fa46f1 + :commit: 00bb35ce9 :git: https://github.com/DataDog/dd-sdk-ios.git DatadogTrace: - :commit: 00bb35ce9eb9b50b37cf725f29b0a96da9fa46f1 + :commit: 00bb35ce9 :git: https://github.com/DataDog/dd-sdk-ios.git SPEC CHECKSUMS: @@ -1905,7 +1905,7 @@ SPEC CHECKSUMS: DatadogInternal: 7837b2ce3d525d429682532eeda697b181299fdc DatadogLogs: 250894b5a99da5b924a019049c0d0326823cdbd6 DatadogRUM: 0d2a60e1abb8aacfb8827ef84f6d5deb4d5026c8 - DatadogSDKReactNative: 2f11191b56e18680f633bfb125ab1832b327d9b4 + DatadogSDKReactNative: e74b171da3b103bf9b2fd372f480fa71c230830d DatadogTrace: f59e933074cd285ad7e9f5af991f8fe04b095991 DatadogWebViewTracking: 9bc92b4147aeed47eb1911451f651094aa6dd6c1 DoubleConversion: f16ae600a246532c4020132d54af21d0ddb2a385 @@ -1975,6 +1975,6 @@ SPEC CHECKSUMS: SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 Yoga: feb4910aba9742cfedc059e2b2902e22ffe9954a -PODFILE CHECKSUM: 0371740c3ef15f157e2090328ec6d4963ce43cd5 +PODFILE CHECKSUM: f3ab84af7c758d164059ea0f336689464122369d COCOAPODS: 1.16.2 From 2fd23d1f64b930b6dcbd80683359ff2d713e05ef Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Thu, 13 Nov 2025 20:30:12 +0200 Subject: [PATCH 056/526] Update deps to 3.2.0 --- example-new-architecture/ios/Podfile | 15 +-- example-new-architecture/ios/Podfile.lock | 128 ++++++++++---------- packages/core/DatadogSDKReactNative.podspec | 14 +-- 3 files changed, 82 insertions(+), 75 deletions(-) diff --git a/example-new-architecture/ios/Podfile b/example-new-architecture/ios/Podfile index 3de68ea46..6a063cedd 100644 --- a/example-new-architecture/ios/Podfile +++ b/example-new-architecture/ios/Podfile @@ -20,13 +20,14 @@ target 'DdSdkReactNativeExample' do pod 'DatadogSDKReactNative', :path => '../../packages/core/DatadogSDKReactNative.podspec', :testspecs => ['Tests'] # Flags don't seem to be released yet so we pull them from the iOS SDK develop branch. - pod 'DatadogInternal', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :commit => '00bb35ce9' - pod 'DatadogCore', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :commit => '00bb35ce9' - pod 'DatadogLogs', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :commit => '00bb35ce9' - pod 'DatadogTrace', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :commit => '00bb35ce9' - pod 'DatadogRUM', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :commit => '00bb35ce9' - pod 'DatadogCrashReporting', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :commit => '00bb35ce9' - pod 'DatadogFlags', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :commit => '00bb35ce9' + pod 'DatadogInternal', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :commit => '2dfe2d0ff' + pod 'DatadogCore', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :commit => '2dfe2d0ff' + pod 'DatadogLogs', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :commit => '2dfe2d0ff' + pod 'DatadogTrace', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :commit => '2dfe2d0ff' + pod 'DatadogRUM', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :commit => '2dfe2d0ff' + pod 'DatadogCrashReporting', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :commit => '2dfe2d0ff' + pod 'DatadogFlags', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :commit => '2dfe2d0ff' + pod 'DatadogWebViewTracking', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :commit => '2dfe2d0ff' config = use_native_modules! diff --git a/example-new-architecture/ios/Podfile.lock b/example-new-architecture/ios/Podfile.lock index 0de14a56a..93fa5e8f1 100644 --- a/example-new-architecture/ios/Podfile.lock +++ b/example-new-architecture/ios/Podfile.lock @@ -1,25 +1,25 @@ PODS: - boost (1.84.0) - - DatadogCore (3.1.0): - - DatadogInternal (= 3.1.0) - - DatadogCrashReporting (3.1.0): - - DatadogInternal (= 3.1.0) + - DatadogCore (3.2.0): + - DatadogInternal (= 3.2.0) + - DatadogCrashReporting (3.2.0): + - DatadogInternal (= 3.2.0) - PLCrashReporter (~> 1.12.0) - - DatadogFlags (3.1.0): - - DatadogInternal (= 3.1.0) - - DatadogInternal (3.1.0) - - DatadogLogs (3.1.0): - - DatadogInternal (= 3.1.0) - - DatadogRUM (3.1.0): - - DatadogInternal (= 3.1.0) + - DatadogFlags (3.2.0): + - DatadogInternal (= 3.2.0) + - DatadogInternal (3.2.0) + - DatadogLogs (3.2.0): + - DatadogInternal (= 3.2.0) + - DatadogRUM (3.2.0): + - DatadogInternal (= 3.2.0) - DatadogSDKReactNative (2.13.0): - - DatadogCore (= 3.1.0) - - DatadogCrashReporting (= 3.1.0) - - DatadogFlags (= 3.1.0) - - DatadogLogs (= 3.1.0) - - DatadogRUM (= 3.1.0) - - DatadogTrace (= 3.1.0) - - DatadogWebViewTracking (= 3.1.0) + - DatadogCore (= 3.2.0) + - DatadogCrashReporting (= 3.2.0) + - DatadogFlags (= 3.2.0) + - DatadogLogs (= 3.2.0) + - DatadogRUM (= 3.2.0) + - DatadogTrace (= 3.2.0) + - DatadogWebViewTracking (= 3.2.0) - DoubleConversion - glog - hermes-engine @@ -41,13 +41,13 @@ PODS: - ReactCommon/turbomodule/core - Yoga - DatadogSDKReactNative/Tests (2.13.0): - - DatadogCore (= 3.1.0) - - DatadogCrashReporting (= 3.1.0) - - DatadogFlags (= 3.1.0) - - DatadogLogs (= 3.1.0) - - DatadogRUM (= 3.1.0) - - DatadogTrace (= 3.1.0) - - DatadogWebViewTracking (= 3.1.0) + - DatadogCore (= 3.2.0) + - DatadogCrashReporting (= 3.2.0) + - DatadogFlags (= 3.2.0) + - DatadogLogs (= 3.2.0) + - DatadogRUM (= 3.2.0) + - DatadogTrace (= 3.2.0) + - DatadogWebViewTracking (= 3.2.0) - DoubleConversion - glog - hermes-engine @@ -68,11 +68,11 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - DatadogTrace (3.1.0): - - DatadogInternal (= 3.1.0) + - DatadogTrace (3.2.0): + - DatadogInternal (= 3.2.0) - OpenTelemetrySwiftApi (= 1.13.1) - - DatadogWebViewTracking (3.1.0): - - DatadogInternal (= 3.1.0) + - DatadogWebViewTracking (3.2.0): + - DatadogInternal (= 3.2.0) - DoubleConversion (1.1.6) - fast_float (6.1.4) - FBLazyVector (0.76.9) @@ -1638,15 +1638,16 @@ PODS: DEPENDENCIES: - boost (from `../node_modules/react-native/third-party-podspecs/boost.podspec`) - - DatadogCore (from `https://github.com/DataDog/dd-sdk-ios.git`, commit `00bb35ce9`) - - DatadogCrashReporting (from `https://github.com/DataDog/dd-sdk-ios.git`, commit `00bb35ce9`) - - DatadogFlags (from `https://github.com/DataDog/dd-sdk-ios.git`, commit `00bb35ce9`) - - DatadogInternal (from `https://github.com/DataDog/dd-sdk-ios.git`, commit `00bb35ce9`) - - DatadogLogs (from `https://github.com/DataDog/dd-sdk-ios.git`, commit `00bb35ce9`) - - DatadogRUM (from `https://github.com/DataDog/dd-sdk-ios.git`, commit `00bb35ce9`) + - DatadogCore (from `https://github.com/DataDog/dd-sdk-ios.git`, commit `2dfe2d0ff`) + - DatadogCrashReporting (from `https://github.com/DataDog/dd-sdk-ios.git`, commit `2dfe2d0ff`) + - DatadogFlags (from `https://github.com/DataDog/dd-sdk-ios.git`, commit `2dfe2d0ff`) + - DatadogInternal (from `https://github.com/DataDog/dd-sdk-ios.git`, commit `2dfe2d0ff`) + - DatadogLogs (from `https://github.com/DataDog/dd-sdk-ios.git`, commit `2dfe2d0ff`) + - DatadogRUM (from `https://github.com/DataDog/dd-sdk-ios.git`, commit `2dfe2d0ff`) - DatadogSDKReactNative (from `../../packages/core/DatadogSDKReactNative.podspec`) - DatadogSDKReactNative/Tests (from `../../packages/core/DatadogSDKReactNative.podspec`) - - DatadogTrace (from `https://github.com/DataDog/dd-sdk-ios.git`, commit `00bb35ce9`) + - DatadogTrace (from `https://github.com/DataDog/dd-sdk-ios.git`, commit `2dfe2d0ff`) + - DatadogWebViewTracking (from `https://github.com/DataDog/dd-sdk-ios.git`, commit `2dfe2d0ff`) - DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`) - fast_float (from `../node_modules/react-native/third-party-podspecs/fast_float.podspec`) - FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`) @@ -1715,7 +1716,6 @@ DEPENDENCIES: SPEC REPOS: https://github.com/CocoaPods/Specs.git: - - DatadogWebViewTracking - OpenTelemetrySwiftApi - PLCrashReporter - SocketRocket @@ -1724,27 +1724,30 @@ EXTERNAL SOURCES: boost: :podspec: "../node_modules/react-native/third-party-podspecs/boost.podspec" DatadogCore: - :commit: 00bb35ce9 + :commit: 2dfe2d0ff :git: https://github.com/DataDog/dd-sdk-ios.git DatadogCrashReporting: - :commit: 00bb35ce9 + :commit: 2dfe2d0ff :git: https://github.com/DataDog/dd-sdk-ios.git DatadogFlags: - :commit: 00bb35ce9 + :commit: 2dfe2d0ff :git: https://github.com/DataDog/dd-sdk-ios.git DatadogInternal: - :commit: 00bb35ce9 + :commit: 2dfe2d0ff :git: https://github.com/DataDog/dd-sdk-ios.git DatadogLogs: - :commit: 00bb35ce9 + :commit: 2dfe2d0ff :git: https://github.com/DataDog/dd-sdk-ios.git DatadogRUM: - :commit: 00bb35ce9 + :commit: 2dfe2d0ff :git: https://github.com/DataDog/dd-sdk-ios.git DatadogSDKReactNative: :path: "../../packages/core/DatadogSDKReactNative.podspec" DatadogTrace: - :commit: 00bb35ce9 + :commit: 2dfe2d0ff + :git: https://github.com/DataDog/dd-sdk-ios.git + DatadogWebViewTracking: + :commit: 2dfe2d0ff :git: https://github.com/DataDog/dd-sdk-ios.git DoubleConversion: :podspec: "../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec" @@ -1876,38 +1879,41 @@ EXTERNAL SOURCES: CHECKOUT OPTIONS: DatadogCore: - :commit: 00bb35ce9 + :commit: 2dfe2d0ff :git: https://github.com/DataDog/dd-sdk-ios.git DatadogCrashReporting: - :commit: 00bb35ce9 + :commit: 2dfe2d0ff :git: https://github.com/DataDog/dd-sdk-ios.git DatadogFlags: - :commit: 00bb35ce9 + :commit: 2dfe2d0ff :git: https://github.com/DataDog/dd-sdk-ios.git DatadogInternal: - :commit: 00bb35ce9 + :commit: 2dfe2d0ff :git: https://github.com/DataDog/dd-sdk-ios.git DatadogLogs: - :commit: 00bb35ce9 + :commit: 2dfe2d0ff :git: https://github.com/DataDog/dd-sdk-ios.git DatadogRUM: - :commit: 00bb35ce9 + :commit: 2dfe2d0ff :git: https://github.com/DataDog/dd-sdk-ios.git DatadogTrace: - :commit: 00bb35ce9 + :commit: 2dfe2d0ff + :git: https://github.com/DataDog/dd-sdk-ios.git + DatadogWebViewTracking: + :commit: 2dfe2d0ff :git: https://github.com/DataDog/dd-sdk-ios.git SPEC CHECKSUMS: boost: 1dca942403ed9342f98334bf4c3621f011aa7946 - DatadogCore: d2f51c7fb4308cf3c25e55e2e7242e5d558ee71d - DatadogCrashReporting: f636f1d1c534572c0b0abcdc59df244c884d825d - DatadogFlags: d4237ffb9c06096d1928dbe47aac877739bc6326 - DatadogInternal: 7837b2ce3d525d429682532eeda697b181299fdc - DatadogLogs: 250894b5a99da5b924a019049c0d0326823cdbd6 - DatadogRUM: 0d2a60e1abb8aacfb8827ef84f6d5deb4d5026c8 - DatadogSDKReactNative: e74b171da3b103bf9b2fd372f480fa71c230830d - DatadogTrace: f59e933074cd285ad7e9f5af991f8fe04b095991 - DatadogWebViewTracking: 9bc92b4147aeed47eb1911451f651094aa6dd6c1 + DatadogCore: 8f360d91ec79c8799e753ec1abe3169911ee50fa + DatadogCrashReporting: 0a392c47eaf0294df7e04c9ae113e97612af63b3 + DatadogFlags: 2a06a1258e78686246e5383ef90ee71f53dc6bff + DatadogInternal: 291b5ad0142280a651ab196ebd30dcd1d37cf6ff + DatadogLogs: 3ce559b785c8013911be4777845e46202046e618 + DatadogRUM: f26899d603f1797b4a41e00b5ee1aa0f6c972ef2 + DatadogSDKReactNative: 8b0b92cb7ec31e34c97ec6671908662d9203f0ca + DatadogTrace: 95e5fb69876ce3ab223dd8ef3036605c89db3cdf + DatadogWebViewTracking: 6b2d5e5ab1e85481e458f9d0130294078549558b DoubleConversion: f16ae600a246532c4020132d54af21d0ddb2a385 fast_float: 06eeec4fe712a76acc9376682e4808b05ce978b6 FBLazyVector: 7605ea4810e0e10ae4815292433c09bf4324ba45 @@ -1975,6 +1981,6 @@ SPEC CHECKSUMS: SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 Yoga: feb4910aba9742cfedc059e2b2902e22ffe9954a -PODFILE CHECKSUM: f3ab84af7c758d164059ea0f336689464122369d +PODFILE CHECKSUM: bda441670d020698768356464e7ec5c2b0573608 COCOAPODS: 1.16.2 diff --git a/packages/core/DatadogSDKReactNative.podspec b/packages/core/DatadogSDKReactNative.podspec index ccdda06c4..2a65176b0 100644 --- a/packages/core/DatadogSDKReactNative.podspec +++ b/packages/core/DatadogSDKReactNative.podspec @@ -19,15 +19,15 @@ 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 'DatadogFlags', '3.1.0' + s.dependency 'DatadogCore', '3.2.0' + s.dependency 'DatadogLogs', '3.2.0' + s.dependency 'DatadogTrace', '3.2.0' + s.dependency 'DatadogRUM', '3.2.0' + s.dependency 'DatadogCrashReporting', '3.2.0' + s.dependency 'DatadogFlags', '3.2.0' # DatadogWebViewTracking is not available for tvOS - s.ios.dependency 'DatadogWebViewTracking', '3.1.0' + s.ios.dependency 'DatadogWebViewTracking', '3.2.0' s.test_spec 'Tests' do |test_spec| test_spec.source_files = 'ios/Tests/**/*.{swift,json}' From cb038d08bc35ac0868ae1264ba4239e2186f5549 Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Thu, 13 Nov 2025 22:27:12 +0200 Subject: [PATCH 057/526] Support other feature flag types --- example-new-architecture/App.tsx | 34 +++++++- packages/core/ios/Sources/DdFlags.mm | 43 +++++++++- .../ios/Sources/DdFlagsImplementation.swift | 81 +++++++++++++++---- packages/core/src/flags/FlagsClient.ts | 42 ++++++++-- packages/core/src/specs/NativeDdFlags.ts | 18 +++++ 5 files changed, 193 insertions(+), 25 deletions(-) diff --git a/example-new-architecture/App.tsx b/example-new-architecture/App.tsx index badd7700d..4e64b7c91 100644 --- a/example-new-architecture/App.tsx +++ b/example-new-architecture/App.tsx @@ -68,8 +68,6 @@ import {APPLICATION_ID, CLIENT_TOKEN, ENVIRONMENT} from './ddCredentials'; }, }); - // Feature flag page: https://app.datadoghq.com/feature-flags/c715d5a6-939e-486e-be12-6f96cb36a018?environmentId=d114cd9a-79ed-4c56-bcf3-bcac9293653b - console.log({booleanValue: await flagsClient.getBooleanValue('dp-test-flag', false)}) })(); type SectionProps = PropsWithChildren<{ @@ -103,6 +101,35 @@ function Section({children, title}: SectionProps): React.JSX.Element { } function App(): React.JSX.Element { + const [flagValues, setFlagValues] = React.useState>({}); + + React.useEffect(() => { + (async () => { + const flagsClient = DatadogFlags.getClient(); + + const [booleanValue, stringValue, jsonValue, integerValue, numberValue] = await Promise.all([ + flagsClient.getBooleanValue('rn-sdk-test-boolean-flag', false), // https://app.datadoghq.com/feature-flags/046d0e70-626d-41e1-8314-3f009fb79b7a?environmentId=d114cd9a-79ed-4c56-bcf3-bcac9293653b + flagsClient.getStringValue('rn-sdk-test-string-flag', 'default-value'), // https://app.datadoghq.com/feature-flags/80756d8f-a375-437a-a023-b490c91cd506?environmentId=d114cd9a-79ed-4c56-bcf3-bcac9293653b + flagsClient.getObjectValue('rn-sdk-test-json-flag', {default: 'value'}), // https://app.datadoghq.com/feature-flags/bcf75cd6-96d8-4182-8871-0b66ad76127a?environmentId=d114cd9a-79ed-4c56-bcf3-bcac9293653b + flagsClient.getNumberValue('rn-sdk-test-integer-flag', 0), // https://app.datadoghq.com/feature-flags/5cd5a154-65ef-4c15-b539-e68c93eaa7f1?environmentId=d114cd9a-79ed-4c56-bcf3-bcac9293653b + flagsClient.getNumberValue('rn-sdk-test-number-flag', 0.7), // https://app.datadoghq.com/feature-flags/62b3129a-f9fa-49c0-b8a2-1a772b183bf7?environmentId=d114cd9a-79ed-4c56-bcf3-bcac9293653b + ]); + + const newValues = { + boolean: booleanValue, + json: jsonValue, + integer: integerValue, + string: stringValue, + number: numberValue, + }; + + console.log({newValues}); + + setFlagValues(newValues); + })().catch(console.error); + }, []); + + const isDarkMode = useColorScheme() === 'dark'; const backgroundStyle = { @@ -123,6 +150,9 @@ function App(): React.JSX.Element { style={{ backgroundColor: isDarkMode ? Colors.black : Colors.white, }}> + + {JSON.stringify(flagValues, (key, value) => value === undefined ? '' : value, 2)} +
Edit App.tsx to change this screen and then come back to see your edits. diff --git a/packages/core/ios/Sources/DdFlags.mm b/packages/core/ios/Sources/DdFlags.mm index bc5104a92..39086bee5 100644 --- a/packages/core/ios/Sources/DdFlags.mm +++ b/packages/core/ios/Sources/DdFlags.mm @@ -27,7 +27,7 @@ @implementation DdFlags } RCT_REMAP_METHOD(getBooleanValue, - withClientName:(NSString *)clientName + getBooleanValueWithClientName:(NSString *)clientName withKey:(NSString *)key withDefaultValue:(BOOL)defaultValue withResolve:(RCTPromiseResolveBlock)resolve @@ -36,6 +36,36 @@ @implementation DdFlags [self getBooleanValue:clientName key:key defaultValue:defaultValue resolve:resolve reject:reject]; } +RCT_REMAP_METHOD(getStringValue, + getStringValueWithClientName:(NSString *)clientName + withKey:(NSString *)key + withDefaultValue:(NSString *)defaultValue + withResolve:(RCTPromiseResolveBlock)resolve + withReject:(RCTPromiseRejectBlock)reject) +{ + [self getStringValue:clientName key:key defaultValue:defaultValue resolve:resolve reject:reject]; +} + +RCT_REMAP_METHOD(getNumberValue, + getNumberValueWithClientName:(NSString *)clientName + withKey:(NSString *)key + withDefaultValue:(double)defaultValue + withResolve:(RCTPromiseResolveBlock)resolve + withReject:(RCTPromiseRejectBlock)reject) +{ + [self getNumberValue:clientName key:key defaultValue:defaultValue resolve:resolve reject:reject]; +} + +RCT_REMAP_METHOD(getObjectValue, + getObjectValueWithClientName:(NSString *)clientName + withKey:(NSString *)key + withDefaultValue:(NSDictionary *)defaultValue + withResolve:(RCTPromiseResolveBlock)resolve + withReject:(RCTPromiseRejectBlock)reject) +{ + [self getObjectValue:clientName key:key defaultValue:defaultValue resolve:resolve reject:reject]; +} + // Thanks to this guard, we won't compile this code when we build for the new architecture. #ifdef RCT_NEW_ARCH_ENABLED - (std::shared_ptr)getTurboModule: @@ -69,4 +99,15 @@ - (void)getBooleanValue:(NSString *)clientName key:(NSString *)key defaultValue: [self.ddFlagsImplementation getBooleanValue:clientName key:key defaultValue:defaultValue resolve:resolve reject:reject]; } +- (void)getStringValue:(NSString *)clientName key:(NSString *)key defaultValue:(NSString *)defaultValue resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + [self.ddFlagsImplementation getStringValue:clientName key:key defaultValue:defaultValue resolve:resolve reject:reject]; +} + +- (void)getNumberValue:(NSString *)clientName key:(NSString *)key defaultValue:(double)defaultValue resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + [self.ddFlagsImplementation getNumberValue:clientName key:key defaultValue:defaultValue resolve:resolve reject:reject]; +} + +- (void)getObjectValue:(NSString *)clientName key:(NSString *)key defaultValue:(NSDictionary *)defaultValue resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + [self.ddFlagsImplementation getObjectValue:clientName key:key defaultValue:defaultValue resolve:resolve reject:reject]; +} @end diff --git a/packages/core/ios/Sources/DdFlagsImplementation.swift b/packages/core/ios/Sources/DdFlagsImplementation.swift index da4f5a914..c76ac3793 100644 --- a/packages/core/ios/Sources/DdFlagsImplementation.swift +++ b/packages/core/ios/Sources/DdFlagsImplementation.swift @@ -25,27 +25,35 @@ public class DdFlagsImplementation: NSObject { return client } - private func parseAttributes(attributes: NSDictionary) -> [String: AnyValue] { - func asAnyValue(value: Any) -> AnyValue { - switch value { - case let s as String: return .string(s) - case let b as Bool: return .bool(b) - case let i as Int: return .int(i) - case let d as Double: return .double(d) - // FIXME: Do we even support nested evaluation contexts? - case let dict as NSDictionary: return .dictionary(parseAttributes(attributes: dict)) - case let arr as NSArray: return .array(arr.compactMap(asAnyValue)) - case is NSNull: return .null - default: return .null - } + private func asAnyValue(_ value: Any) -> AnyValue { + if value is NSNull { + return .null } - + + if let value = value as? String { + return .string(value) + } else if let value = value as? Bool { + return .bool(value) + } else if let value = value as? Int { + return .int(value) + } else if let value = value as? Double { + return .double(value) + } else if let value = value as? NSDictionary { + return .dictionary(parseAttributes(attributes: value)) + } else if let value = value as? NSArray { + return .array(value.compactMap(asAnyValue)) + } else { + return .null + } + } + + private func parseAttributes(attributes: NSDictionary) -> [String: AnyValue] { var result: [String: AnyValue] = [:] for (key, value) in attributes { guard let stringKey = key as? String else { continue } - result[stringKey] = asAnyValue(value: value) + result[stringKey] = asAnyValue(value) } return result } @@ -75,9 +83,50 @@ public class DdFlagsImplementation: NSObject { reject: RCTPromiseRejectBlock ) { let client = getClient(name: clientName) - let value = client.getBooleanValue(key: key, defaultValue: defaultValue) + resolve(value) + } + + @objc + public func getStringValue( + _ clientName: String, + key: String, + defaultValue: String, + resolve: RCTPromiseResolveBlock, + reject: RCTPromiseRejectBlock + ) { + let client = getClient(name: clientName) + let value = client.getStringValue(key: key, defaultValue: defaultValue) + resolve(value) + } + + @objc + public func getNumberValue( + _ clientName: String, + key: String, + defaultValue: Double, + resolve: RCTPromiseResolveBlock, + reject: RCTPromiseRejectBlock + ) { + let client = getClient(name: clientName) + // TODO: Handle Integer flag values... + let value = client.getDoubleValue(key: key, defaultValue: defaultValue) + resolve(value) + } + + @objc + public func getObjectValue( + _ clientName: String, + key: String, + defaultValue: NSDictionary, + resolve: RCTPromiseResolveBlock, + reject: RCTPromiseRejectBlock + ) { + let client = getClient(name: clientName) + let val = asAnyValue(defaultValue) + let value = client.getObjectValue(key: key, defaultValue: val) + // TODO: Convert to Dictionary. resolve(value) } } diff --git a/packages/core/src/flags/FlagsClient.ts b/packages/core/src/flags/FlagsClient.ts index b6f5ea275..0bbcb9948 100644 --- a/packages/core/src/flags/FlagsClient.ts +++ b/packages/core/src/flags/FlagsClient.ts @@ -3,8 +3,7 @@ * 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 type { DdNativeFlagsType } from '../nativeModulesTypes'; import type { EvaluationContext } from './types'; @@ -36,17 +35,48 @@ export class FlagsClient { key: string, defaultValue: boolean ): Promise => { - InternalLog.log( - `Flags.getBooleanValue(${key}, ${defaultValue})`, - SdkVerbosity.DEBUG + const value = await this.nativeFlags.getBooleanValue( + this.clientName, + key, + defaultValue ); + return value; + }; - const value = await this.nativeFlags.getBooleanValue( + getStringValue = async ( + key: string, + defaultValue: string + ): Promise => { + const value = await this.nativeFlags.getStringValue( + this.clientName, + key, + defaultValue + ); + return value; + }; + + getNumberValue = async ( + key: string, + defaultValue: number + ): Promise => { + const value = await this.nativeFlags.getNumberValue( this.clientName, key, defaultValue ); + return value; + }; + getObjectValue = async ( + key: string, + defaultValue: { [key: string]: unknown } + ): Promise<{ [key: string]: unknown }> => { + // FIXME: This is broken at the moment due to issues with JSON parsing on native iOS SDK side. + const value = await this.nativeFlags.getObjectValue( + this.clientName, + key, + defaultValue + ); return value; }; } diff --git a/packages/core/src/specs/NativeDdFlags.ts b/packages/core/src/specs/NativeDdFlags.ts index 130bb8e86..5074191ae 100644 --- a/packages/core/src/specs/NativeDdFlags.ts +++ b/packages/core/src/specs/NativeDdFlags.ts @@ -28,6 +28,24 @@ export interface Spec extends TurboModule { key: string, defaultValue: boolean ) => Promise; + + readonly getStringValue: ( + clientName: string, + key: string, + defaultValue: string + ) => Promise; + + readonly getNumberValue: ( + clientName: string, + key: string, + defaultValue: number + ) => Promise; + + readonly getObjectValue: ( + clientName: string, + key: string, + defaultValue: { [key: string]: unknown } + ) => Promise<{ [key: string]: unknown }>; } // eslint-disable-next-line import/no-default-export From 9d51b6d1053cd1e39c5899baa9b5ef99f4813483 Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Fri, 14 Nov 2025 18:30:41 +0200 Subject: [PATCH 058/526] Add a get*Details method --- example-new-architecture/App.tsx | 2 +- packages/core/ios/Sources/DdFlags.mm | 14 ++ .../ios/Sources/DdFlagsImplementation.swift | 135 ++++++++++++++---- packages/core/src/flags/DatadogFlags.ts | 1 + packages/core/src/flags/FlagsClient.ts | 14 +- packages/core/src/flags/types.ts | 29 ++-- packages/core/src/specs/NativeDdFlags.ts | 8 ++ 7 files changed, 165 insertions(+), 38 deletions(-) diff --git a/example-new-architecture/App.tsx b/example-new-architecture/App.tsx index 4e64b7c91..84d7ed687 100644 --- a/example-new-architecture/App.tsx +++ b/example-new-architecture/App.tsx @@ -108,7 +108,7 @@ function App(): React.JSX.Element { const flagsClient = DatadogFlags.getClient(); const [booleanValue, stringValue, jsonValue, integerValue, numberValue] = await Promise.all([ - flagsClient.getBooleanValue('rn-sdk-test-boolean-flag', false), // https://app.datadoghq.com/feature-flags/046d0e70-626d-41e1-8314-3f009fb79b7a?environmentId=d114cd9a-79ed-4c56-bcf3-bcac9293653b + flagsClient.getBooleanDetails('rn-sdk-test-boolean-flag', false), // https://app.datadoghq.com/feature-flags/046d0e70-626d-41e1-8314-3f009fb79b7a?environmentId=d114cd9a-79ed-4c56-bcf3-bcac9293653b flagsClient.getStringValue('rn-sdk-test-string-flag', 'default-value'), // https://app.datadoghq.com/feature-flags/80756d8f-a375-437a-a023-b490c91cd506?environmentId=d114cd9a-79ed-4c56-bcf3-bcac9293653b flagsClient.getObjectValue('rn-sdk-test-json-flag', {default: 'value'}), // https://app.datadoghq.com/feature-flags/bcf75cd6-96d8-4182-8871-0b66ad76127a?environmentId=d114cd9a-79ed-4c56-bcf3-bcac9293653b flagsClient.getNumberValue('rn-sdk-test-integer-flag', 0), // https://app.datadoghq.com/feature-flags/5cd5a154-65ef-4c15-b539-e68c93eaa7f1?environmentId=d114cd9a-79ed-4c56-bcf3-bcac9293653b diff --git a/packages/core/ios/Sources/DdFlags.mm b/packages/core/ios/Sources/DdFlags.mm index 39086bee5..d7c294f0b 100644 --- a/packages/core/ios/Sources/DdFlags.mm +++ b/packages/core/ios/Sources/DdFlags.mm @@ -36,6 +36,16 @@ @implementation DdFlags [self getBooleanValue:clientName key:key defaultValue:defaultValue resolve:resolve reject:reject]; } +RCT_REMAP_METHOD(getBooleanDetails, + getBooleanDetailsWithClientName:(NSString *)clientName + withKey:(NSString *)key + withDefaultValue:(BOOL)defaultValue + withResolve:(RCTPromiseResolveBlock)resolve + withReject:(RCTPromiseRejectBlock)reject) +{ + [self getBooleanDetails:clientName key:key defaultValue:defaultValue resolve:resolve reject:reject]; +} + RCT_REMAP_METHOD(getStringValue, getStringValueWithClientName:(NSString *)clientName withKey:(NSString *)key @@ -99,6 +109,10 @@ - (void)getBooleanValue:(NSString *)clientName key:(NSString *)key defaultValue: [self.ddFlagsImplementation getBooleanValue:clientName key:key defaultValue:defaultValue resolve:resolve reject:reject]; } +- (void)getBooleanDetails:(NSString *)clientName key:(NSString *)key defaultValue:(BOOL)defaultValue resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + [self.ddFlagsImplementation getBooleanDetails:clientName key:key defaultValue:defaultValue resolve:resolve reject:reject]; +} + - (void)getStringValue:(NSString *)clientName key:(NSString *)key defaultValue:(NSString *)defaultValue resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { [self.ddFlagsImplementation getStringValue:clientName key:key defaultValue:defaultValue resolve:resolve reject:reject]; } diff --git a/packages/core/ios/Sources/DdFlagsImplementation.swift b/packages/core/ios/Sources/DdFlagsImplementation.swift index c76ac3793..8e632967a 100644 --- a/packages/core/ios/Sources/DdFlagsImplementation.swift +++ b/packages/core/ios/Sources/DdFlagsImplementation.swift @@ -25,35 +25,13 @@ public class DdFlagsImplementation: NSObject { return client } - private func asAnyValue(_ value: Any) -> AnyValue { - if value is NSNull { - return .null - } - - if let value = value as? String { - return .string(value) - } else if let value = value as? Bool { - return .bool(value) - } else if let value = value as? Int { - return .int(value) - } else if let value = value as? Double { - return .double(value) - } else if let value = value as? NSDictionary { - return .dictionary(parseAttributes(attributes: value)) - } else if let value = value as? NSArray { - return .array(value.compactMap(asAnyValue)) - } else { - return .null - } - } - private func parseAttributes(attributes: NSDictionary) -> [String: AnyValue] { var result: [String: AnyValue] = [:] for (key, value) in attributes { guard let stringKey = key as? String else { continue } - result[stringKey] = asAnyValue(value) + result[stringKey] = AnyValue.wrap(value) } return result } @@ -62,7 +40,8 @@ public class DdFlagsImplementation: NSObject { public func setEvaluationContext(_ clientName: String, targetingKey: String, attributes: NSDictionary, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { let client = getClient(name: clientName) - let evaluationContext = FlagsEvaluationContext(targetingKey: targetingKey, attributes: parseAttributes(attributes: attributes)) + let parsedAttributes = parseAttributes(attributes: attributes) + let evaluationContext = FlagsEvaluationContext(targetingKey: targetingKey, attributes: parsedAttributes) client.setEvaluationContext(evaluationContext) { result in switch result { @@ -87,6 +66,20 @@ public class DdFlagsImplementation: NSObject { resolve(value) } + @objc + public func getBooleanDetails( + _ clientName: String, + key: String, + defaultValue: Bool, + resolve: RCTPromiseResolveBlock, + reject: RCTPromiseRejectBlock + ) { + let client = getClient(name: clientName) + let details = client.getBooleanDetails(key: key, defaultValue: defaultValue) + let serializedDetails = details.toSerializedDictionary() + resolve(serializedDetails) + } + @objc public func getStringValue( _ clientName: String, @@ -123,10 +116,96 @@ public class DdFlagsImplementation: NSObject { reject: RCTPromiseRejectBlock ) { let client = getClient(name: clientName) - - let val = asAnyValue(defaultValue) - let value = client.getObjectValue(key: key, defaultValue: val) - // TODO: Convert to Dictionary. + let value = client.getObjectValue(key: key, defaultValue: AnyValue.wrap(defaultValue)) resolve(value) } } + +extension AnyValue { + static func wrap(_ value: Any) -> AnyValue { + if value is NSNull { + return .null + } + + if let value = value as? String { + return .string(value) + } else if let value = value as? Bool { + return .bool(value) + } else if let value = value as? Int { + return .int(value) + } else if let value = value as? Double { + return .double(value) + } else if let value = value as? [String: Any] { + return .dictionary(value.mapValues(AnyValue.wrap)) + } else if let value = value as? [Any] { + return .array(value.map(AnyValue.wrap)) + } else { + return .null + } + } + + func unwrap() -> Any { + switch self { + case .string(let value): + return value + case .bool(let value): + return value + case .int(let value): + return value + case .double(let value): + return value + case .dictionary(let dict): + return dict.mapValues { $0.unwrap() } + case .array(let array): + return array.map { $0.unwrap() } + case .null: + return NSNull() + } + } +} + +extension FlagDetails { + func toSerializedDictionary() -> [String: Any?] { + let dict: [String: Any?] = [ + "key": key, + "value": getSerializedValue(), + "variant": variant as Any?, + "reason": reason as Any?, + "error": getSerializedError() + ] + + return dict + } + + private func getSerializedValue() -> Any { + if let boolValue = value as? Bool { + return boolValue + } else if let stringValue = value as? String { + return stringValue + } else if let intValue = value as? Int { + return intValue + } else if let doubleValue = value as? Double { + return doubleValue + } else if let anyValue = value as? AnyValue { + return anyValue.unwrap() + } + + // Fallback for unexpected types. + return NSNull() + } + + private func getSerializedError() -> String? { + guard let error = error else { + return nil + } + + switch error { + case .providerNotReady: + return "PROVIDER_NOT_READY" + case .flagNotFound: + return "FLAG_NOT_FOUND" + case .typeMismatch: + return "TYPE_MISMATCH" + } + } +} diff --git a/packages/core/src/flags/DatadogFlags.ts b/packages/core/src/flags/DatadogFlags.ts index e97b2974c..e1f18bae9 100644 --- a/packages/core/src/flags/DatadogFlags.ts +++ b/packages/core/src/flags/DatadogFlags.ts @@ -3,6 +3,7 @@ * This product includes software developed at Datadog (https://www.datadoghq.com/). * Copyright 2016-Present Datadog, Inc. */ + import { InternalLog } from '../InternalLog'; import { SdkVerbosity } from '../SdkVerbosity'; diff --git a/packages/core/src/flags/FlagsClient.ts b/packages/core/src/flags/FlagsClient.ts index 0bbcb9948..b6264638e 100644 --- a/packages/core/src/flags/FlagsClient.ts +++ b/packages/core/src/flags/FlagsClient.ts @@ -6,7 +6,7 @@ import type { DdNativeFlagsType } from '../nativeModulesTypes'; -import type { EvaluationContext } from './types'; +import type { EvaluationContext, FlagDetails } from './types'; export class FlagsClient { // eslint-disable-next-line global-require, @typescript-eslint/no-var-requires @@ -43,6 +43,18 @@ export class FlagsClient { return value; }; + getBooleanDetails = async ( + key: string, + defaultValue: boolean + ): Promise> => { + const details = await this.nativeFlags.getBooleanDetails( + this.clientName, + key, + defaultValue + ); + return details; + }; + getStringValue = async ( key: string, defaultValue: string diff --git a/packages/core/src/flags/types.ts b/packages/core/src/flags/types.ts index e7ef4a081..0f652ecf1 100644 --- a/packages/core/src/flags/types.ts +++ b/packages/core/src/flags/types.ts @@ -4,14 +4,6 @@ * Copyright 2016-Present Datadog, Inc. */ -/** - * Evaluation context for flags. - */ -export interface EvaluationContext { - targetingKey: string; - attributes: Record; -} - /** * Configuration settings for flags. */ @@ -24,3 +16,24 @@ export interface DatadogFlagsConfiguration { trackExposures?: boolean; rumIntegrationEnabled?: boolean; } + +/** + * Evaluation context for flags. + */ +export interface EvaluationContext { + targetingKey: string; + attributes: Record; +} + +export type FlagEvaluationError = + | 'PROVIDER_NOT_READY' + | 'FLAG_NOT_FOUND' + | 'TYPE_MISMATCH'; + +export interface FlagDetails { + key: string; + value: T; + variant: string | null; + reason: string | null; + error: FlagEvaluationError | null; +} diff --git a/packages/core/src/specs/NativeDdFlags.ts b/packages/core/src/specs/NativeDdFlags.ts index 5074191ae..fa05e8c68 100644 --- a/packages/core/src/specs/NativeDdFlags.ts +++ b/packages/core/src/specs/NativeDdFlags.ts @@ -8,6 +8,8 @@ import type { TurboModule } from 'react-native'; import { TurboModuleRegistry } from 'react-native'; +import type { FlagDetails } from '../flags/types'; + /** * Do not import this Spec directly, use DdNativeFlagsType instead. */ @@ -46,6 +48,12 @@ export interface Spec extends TurboModule { key: string, defaultValue: { [key: string]: unknown } ) => Promise<{ [key: string]: unknown }>; + + readonly getBooleanDetails: ( + clientName: string, + key: string, + defaultValue: boolean + ) => Promise>; } // eslint-disable-next-line import/no-default-export From a1740655ea1161e338f3f87570f3d94a27b9fdc6 Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Fri, 14 Nov 2025 19:25:43 +0200 Subject: [PATCH 059/526] Switch to only passing details methods from Swift --- example-new-architecture/App.tsx | 8 +-- packages/core/ios/Sources/DdFlags.mm | 44 ++++-------- .../ios/Sources/DdFlagsImplementation.swift | 36 ++++------ packages/core/src/flags/FlagsClient.ts | 68 ++++++++++++------- packages/core/src/specs/NativeDdFlags.ts | 22 +++--- 5 files changed, 84 insertions(+), 94 deletions(-) diff --git a/example-new-architecture/App.tsx b/example-new-architecture/App.tsx index 84d7ed687..42be78f5e 100644 --- a/example-new-architecture/App.tsx +++ b/example-new-architecture/App.tsx @@ -109,10 +109,10 @@ function App(): React.JSX.Element { const [booleanValue, stringValue, jsonValue, integerValue, numberValue] = await Promise.all([ flagsClient.getBooleanDetails('rn-sdk-test-boolean-flag', false), // https://app.datadoghq.com/feature-flags/046d0e70-626d-41e1-8314-3f009fb79b7a?environmentId=d114cd9a-79ed-4c56-bcf3-bcac9293653b - flagsClient.getStringValue('rn-sdk-test-string-flag', 'default-value'), // https://app.datadoghq.com/feature-flags/80756d8f-a375-437a-a023-b490c91cd506?environmentId=d114cd9a-79ed-4c56-bcf3-bcac9293653b - flagsClient.getObjectValue('rn-sdk-test-json-flag', {default: 'value'}), // https://app.datadoghq.com/feature-flags/bcf75cd6-96d8-4182-8871-0b66ad76127a?environmentId=d114cd9a-79ed-4c56-bcf3-bcac9293653b - flagsClient.getNumberValue('rn-sdk-test-integer-flag', 0), // https://app.datadoghq.com/feature-flags/5cd5a154-65ef-4c15-b539-e68c93eaa7f1?environmentId=d114cd9a-79ed-4c56-bcf3-bcac9293653b - flagsClient.getNumberValue('rn-sdk-test-number-flag', 0.7), // https://app.datadoghq.com/feature-flags/62b3129a-f9fa-49c0-b8a2-1a772b183bf7?environmentId=d114cd9a-79ed-4c56-bcf3-bcac9293653b + flagsClient.getStringDetails('rn-sdk-test-string-flag', 'default-value'), // https://app.datadoghq.com/feature-flags/80756d8f-a375-437a-a023-b490c91cd506?environmentId=d114cd9a-79ed-4c56-bcf3-bcac9293653b + flagsClient.getObjectDetails('rn-sdk-test-json-flag', {default: 'value'}), // https://app.datadoghq.com/feature-flags/bcf75cd6-96d8-4182-8871-0b66ad76127a?environmentId=d114cd9a-79ed-4c56-bcf3-bcac9293653b + flagsClient.getNumberDetails('rn-sdk-test-integer-flag', 0), // https://app.datadoghq.com/feature-flags/5cd5a154-65ef-4c15-b539-e68c93eaa7f1?environmentId=d114cd9a-79ed-4c56-bcf3-bcac9293653b + flagsClient.getNumberDetails('rn-sdk-test-number-flag', 0.7), // https://app.datadoghq.com/feature-flags/62b3129a-f9fa-49c0-b8a2-1a772b183bf7?environmentId=d114cd9a-79ed-4c56-bcf3-bcac9293653b ]); const newValues = { diff --git a/packages/core/ios/Sources/DdFlags.mm b/packages/core/ios/Sources/DdFlags.mm index d7c294f0b..59939d45b 100644 --- a/packages/core/ios/Sources/DdFlags.mm +++ b/packages/core/ios/Sources/DdFlags.mm @@ -26,16 +26,6 @@ @implementation DdFlags [self setEvaluationContext:clientName targetingKey:targetingKey attributes:attributes resolve:resolve reject:reject]; } -RCT_REMAP_METHOD(getBooleanValue, - getBooleanValueWithClientName:(NSString *)clientName - withKey:(NSString *)key - withDefaultValue:(BOOL)defaultValue - withResolve:(RCTPromiseResolveBlock)resolve - withReject:(RCTPromiseRejectBlock)reject) -{ - [self getBooleanValue:clientName key:key defaultValue:defaultValue resolve:resolve reject:reject]; -} - RCT_REMAP_METHOD(getBooleanDetails, getBooleanDetailsWithClientName:(NSString *)clientName withKey:(NSString *)key @@ -46,34 +36,34 @@ @implementation DdFlags [self getBooleanDetails:clientName key:key defaultValue:defaultValue resolve:resolve reject:reject]; } -RCT_REMAP_METHOD(getStringValue, - getStringValueWithClientName:(NSString *)clientName +RCT_REMAP_METHOD(getStringDetails, + getStringDetailsWithClientName:(NSString *)clientName withKey:(NSString *)key withDefaultValue:(NSString *)defaultValue withResolve:(RCTPromiseResolveBlock)resolve withReject:(RCTPromiseRejectBlock)reject) { - [self getStringValue:clientName key:key defaultValue:defaultValue resolve:resolve reject:reject]; + [self getStringDetails:clientName key:key defaultValue:defaultValue resolve:resolve reject:reject]; } -RCT_REMAP_METHOD(getNumberValue, - getNumberValueWithClientName:(NSString *)clientName +RCT_REMAP_METHOD(getNumberDetails, + getNumberDetailsWithClientName:(NSString *)clientName withKey:(NSString *)key withDefaultValue:(double)defaultValue withResolve:(RCTPromiseResolveBlock)resolve withReject:(RCTPromiseRejectBlock)reject) { - [self getNumberValue:clientName key:key defaultValue:defaultValue resolve:resolve reject:reject]; + [self getNumberDetails:clientName key:key defaultValue:defaultValue resolve:resolve reject:reject]; } -RCT_REMAP_METHOD(getObjectValue, - getObjectValueWithClientName:(NSString *)clientName +RCT_REMAP_METHOD(getObjectDetails, + getObjectDetailsWithClientName:(NSString *)clientName withKey:(NSString *)key withDefaultValue:(NSDictionary *)defaultValue withResolve:(RCTPromiseResolveBlock)resolve withReject:(RCTPromiseRejectBlock)reject) { - [self getObjectValue:clientName key:key defaultValue:defaultValue resolve:resolve reject:reject]; + [self getObjectDetails:clientName key:key defaultValue:defaultValue resolve:resolve reject:reject]; } // Thanks to this guard, we won't compile this code when we build for the new architecture. @@ -105,23 +95,19 @@ - (void)setEvaluationContext:(NSString *)clientName targetingKey:(NSString *)tar [self.ddFlagsImplementation setEvaluationContext:clientName targetingKey:targetingKey attributes:attributes resolve:resolve reject:reject]; } -- (void)getBooleanValue:(NSString *)clientName key:(NSString *)key defaultValue:(BOOL)defaultValue resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { - [self.ddFlagsImplementation getBooleanValue:clientName key:key defaultValue:defaultValue resolve:resolve reject:reject]; -} - - (void)getBooleanDetails:(NSString *)clientName key:(NSString *)key defaultValue:(BOOL)defaultValue resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { [self.ddFlagsImplementation getBooleanDetails:clientName key:key defaultValue:defaultValue resolve:resolve reject:reject]; } -- (void)getStringValue:(NSString *)clientName key:(NSString *)key defaultValue:(NSString *)defaultValue resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { - [self.ddFlagsImplementation getStringValue:clientName key:key defaultValue:defaultValue resolve:resolve reject:reject]; +- (void)getStringDetails:(NSString *)clientName key:(NSString *)key defaultValue:(NSString *)defaultValue resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + [self.ddFlagsImplementation getStringDetails:clientName key:key defaultValue:defaultValue resolve:resolve reject:reject]; } -- (void)getNumberValue:(NSString *)clientName key:(NSString *)key defaultValue:(double)defaultValue resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { - [self.ddFlagsImplementation getNumberValue:clientName key:key defaultValue:defaultValue resolve:resolve reject:reject]; +- (void)getNumberDetails:(NSString *)clientName key:(NSString *)key defaultValue:(double)defaultValue resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + [self.ddFlagsImplementation getNumberDetails:clientName key:key defaultValue:defaultValue resolve:resolve reject:reject]; } -- (void)getObjectValue:(NSString *)clientName key:(NSString *)key defaultValue:(NSDictionary *)defaultValue resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { - [self.ddFlagsImplementation getObjectValue:clientName key:key defaultValue:defaultValue resolve:resolve reject:reject]; +- (void)getObjectDetails:(NSString *)clientName key:(NSString *)key defaultValue:(NSDictionary *)defaultValue resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + [self.ddFlagsImplementation getObjectDetails:clientName key:key defaultValue:defaultValue resolve:resolve reject:reject]; } @end diff --git a/packages/core/ios/Sources/DdFlagsImplementation.swift b/packages/core/ios/Sources/DdFlagsImplementation.swift index 8e632967a..9e3e41058 100644 --- a/packages/core/ios/Sources/DdFlagsImplementation.swift +++ b/packages/core/ios/Sources/DdFlagsImplementation.swift @@ -53,19 +53,6 @@ public class DdFlagsImplementation: NSObject { } } - @objc - public func getBooleanValue( - _ clientName: String, - key: String, - defaultValue: Bool, - resolve: RCTPromiseResolveBlock, - reject: RCTPromiseRejectBlock - ) { - let client = getClient(name: clientName) - let value = client.getBooleanValue(key: key, defaultValue: defaultValue) - resolve(value) - } - @objc public func getBooleanDetails( _ clientName: String, @@ -81,7 +68,7 @@ public class DdFlagsImplementation: NSObject { } @objc - public func getStringValue( + public func getStringDetails( _ clientName: String, key: String, defaultValue: String, @@ -89,12 +76,13 @@ public class DdFlagsImplementation: NSObject { reject: RCTPromiseRejectBlock ) { let client = getClient(name: clientName) - let value = client.getStringValue(key: key, defaultValue: defaultValue) - resolve(value) + let details = client.getStringDetails(key: key, defaultValue: defaultValue) + let serializedDetails = details.toSerializedDictionary() + resolve(serializedDetails) } @objc - public func getNumberValue( + public func getNumberDetails( _ clientName: String, key: String, defaultValue: Double, @@ -103,21 +91,23 @@ public class DdFlagsImplementation: NSObject { ) { let client = getClient(name: clientName) // TODO: Handle Integer flag values... - let value = client.getDoubleValue(key: key, defaultValue: defaultValue) - resolve(value) + let details = client.getDoubleDetails(key: key, defaultValue: defaultValue) + let serializedDetails = details.toSerializedDictionary() + resolve(serializedDetails) } @objc - public func getObjectValue( + public func getObjectDetails( _ clientName: String, key: String, - defaultValue: NSDictionary, + defaultValue: [String: Any], resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock ) { let client = getClient(name: clientName) - let value = client.getObjectValue(key: key, defaultValue: AnyValue.wrap(defaultValue)) - resolve(value) + let details = client.getObjectDetails(key: key, defaultValue: AnyValue.wrap(defaultValue)) + let serializedDetails = details.toSerializedDictionary() + resolve(serializedDetails) } } diff --git a/packages/core/src/flags/FlagsClient.ts b/packages/core/src/flags/FlagsClient.ts index b6264638e..502bb3a69 100644 --- a/packages/core/src/flags/FlagsClient.ts +++ b/packages/core/src/flags/FlagsClient.ts @@ -31,23 +31,23 @@ export class FlagsClient { ); }; - getBooleanValue = async ( + getBooleanDetails = async ( key: string, defaultValue: boolean - ): Promise => { - const value = await this.nativeFlags.getBooleanValue( + ): Promise> => { + const details = await this.nativeFlags.getBooleanDetails( this.clientName, key, defaultValue ); - return value; + return details; }; - getBooleanDetails = async ( + getStringDetails = async ( key: string, - defaultValue: boolean - ): Promise> => { - const details = await this.nativeFlags.getBooleanDetails( + defaultValue: string + ): Promise> => { + const details = await this.nativeFlags.getStringDetails( this.clientName, key, defaultValue @@ -55,28 +55,52 @@ export class FlagsClient { return details; }; - getStringValue = async ( + getNumberDetails = async ( key: string, - defaultValue: string - ): Promise => { - const value = await this.nativeFlags.getStringValue( + defaultValue: number + ): Promise> => { + const details = await this.nativeFlags.getNumberDetails( this.clientName, key, defaultValue ); - return value; + return details; }; - getNumberValue = async ( + getObjectDetails = async ( key: string, - defaultValue: number - ): Promise => { - const value = await this.nativeFlags.getNumberValue( + defaultValue: { [key: string]: unknown } + ): Promise> => { + const details = await this.nativeFlags.getObjectDetails( this.clientName, key, defaultValue ); - return value; + return details; + }; + + getBooleanValue = async ( + key: string, + defaultValue: boolean + ): Promise => { + const details = await this.getBooleanDetails(key, defaultValue); + return details.value; + }; + + getStringValue = async ( + key: string, + defaultValue: string + ): Promise => { + const details = await this.getStringDetails(key, defaultValue); + return details.value; + }; + + getNumberValue = async ( + key: string, + defaultValue: number + ): Promise => { + const details = await this.getNumberDetails(key, defaultValue); + return details.value; }; getObjectValue = async ( @@ -84,11 +108,7 @@ export class FlagsClient { defaultValue: { [key: string]: unknown } ): Promise<{ [key: string]: unknown }> => { // FIXME: This is broken at the moment due to issues with JSON parsing on native iOS SDK side. - const value = await this.nativeFlags.getObjectValue( - this.clientName, - key, - defaultValue - ); - return value; + const details = await this.getObjectDetails(key, defaultValue); + return details.value; }; } diff --git a/packages/core/src/specs/NativeDdFlags.ts b/packages/core/src/specs/NativeDdFlags.ts index fa05e8c68..52461c660 100644 --- a/packages/core/src/specs/NativeDdFlags.ts +++ b/packages/core/src/specs/NativeDdFlags.ts @@ -25,35 +25,29 @@ export interface Spec extends TurboModule { attributes: { [key: string]: unknown } ) => Promise; - readonly getBooleanValue: ( + readonly getBooleanDetails: ( clientName: string, key: string, defaultValue: boolean - ) => Promise; + ) => Promise>; - readonly getStringValue: ( + readonly getStringDetails: ( clientName: string, key: string, defaultValue: string - ) => Promise; + ) => Promise>; - readonly getNumberValue: ( + readonly getNumberDetails: ( clientName: string, key: string, defaultValue: number - ) => Promise; + ) => Promise>; - readonly getObjectValue: ( + readonly getObjectDetails: ( clientName: string, key: string, defaultValue: { [key: string]: unknown } - ) => Promise<{ [key: string]: unknown }>; - - readonly getBooleanDetails: ( - clientName: string, - key: string, - defaultValue: boolean - ) => Promise>; + ) => Promise>; } // eslint-disable-next-line import/no-default-export From 9882b316dd90f1b69da171900c40386beb99af3b Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Fri, 14 Nov 2025 19:27:49 +0200 Subject: [PATCH 060/526] Properly handle Integer flag type --- .../ios/Sources/DdFlagsImplementation.swift | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/packages/core/ios/Sources/DdFlagsImplementation.swift b/packages/core/ios/Sources/DdFlagsImplementation.swift index 9e3e41058..0c70d7d1f 100644 --- a/packages/core/ios/Sources/DdFlagsImplementation.swift +++ b/packages/core/ios/Sources/DdFlagsImplementation.swift @@ -90,10 +90,23 @@ public class DdFlagsImplementation: NSObject { reject: RCTPromiseRejectBlock ) { let client = getClient(name: clientName) - // TODO: Handle Integer flag values... - let details = client.getDoubleDetails(key: key, defaultValue: defaultValue) - let serializedDetails = details.toSerializedDictionary() - resolve(serializedDetails) + + let doubleDetails = client.getDoubleDetails(key: key, defaultValue: defaultValue) + + // Try to retrieve this flag as Integer, not a Number flag type. + if doubleDetails.error == .typeMismatch { + if let safeInt = Int(exactly: defaultValue) { + let intDetails = client.getIntegerDetails(key: key, defaultValue: safeInt) + + // If resolved correctly, return Integer details. + if intDetails.error == nil { + resolve(intDetails.toSerializedDictionary()) + return + } + } + } + + resolve(doubleDetails.toSerializedDictionary()) } @objc From 11dd28789b7d1a004591ae4982cbf3e8eb4c1f51 Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Fri, 14 Nov 2025 19:52:35 +0200 Subject: [PATCH 061/526] Add default value type validation --- packages/core/src/flags/FlagsClient.ts | 40 ++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/packages/core/src/flags/FlagsClient.ts b/packages/core/src/flags/FlagsClient.ts index 502bb3a69..3e1606a63 100644 --- a/packages/core/src/flags/FlagsClient.ts +++ b/packages/core/src/flags/FlagsClient.ts @@ -35,6 +35,16 @@ export class FlagsClient { key: string, defaultValue: boolean ): Promise> => { + if (typeof defaultValue !== 'boolean') { + return { + key, + value: defaultValue, + variant: null, + reason: null, + error: 'TYPE_MISMATCH' + }; + } + const details = await this.nativeFlags.getBooleanDetails( this.clientName, key, @@ -47,6 +57,16 @@ export class FlagsClient { key: string, defaultValue: string ): Promise> => { + if (typeof defaultValue !== 'string') { + return { + key, + value: defaultValue, + variant: null, + reason: null, + error: 'TYPE_MISMATCH' + }; + } + const details = await this.nativeFlags.getStringDetails( this.clientName, key, @@ -59,6 +79,16 @@ export class FlagsClient { key: string, defaultValue: number ): Promise> => { + if (typeof defaultValue !== 'number') { + return { + key, + value: defaultValue, + variant: null, + reason: null, + error: 'TYPE_MISMATCH' + }; + } + const details = await this.nativeFlags.getNumberDetails( this.clientName, key, @@ -71,6 +101,16 @@ export class FlagsClient { key: string, defaultValue: { [key: string]: unknown } ): Promise> => { + if (typeof defaultValue !== 'object') { + return { + key, + value: defaultValue, + variant: null, + reason: null, + error: 'TYPE_MISMATCH' + }; + } + const details = await this.nativeFlags.getObjectDetails( this.clientName, key, From 591282602cf5276433a655c0d629da9ea8a0618f Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Fri, 14 Nov 2025 20:36:34 +0200 Subject: [PATCH 062/526] Add JSDoc documentation to public stuff --- packages/core/src/flags/DatadogFlags.ts | 15 ++- packages/core/src/flags/types.ts | 170 +++++++++++++++++++++++- 2 files changed, 178 insertions(+), 7 deletions(-) diff --git a/packages/core/src/flags/DatadogFlags.ts b/packages/core/src/flags/DatadogFlags.ts index e1f18bae9..e5e65fb5c 100644 --- a/packages/core/src/flags/DatadogFlags.ts +++ b/packages/core/src/flags/DatadogFlags.ts @@ -6,12 +6,16 @@ import { InternalLog } from '../InternalLog'; import { SdkVerbosity } from '../SdkVerbosity'; +import { getGlobalInstance } from '../utils/singletonUtils'; import { FlagsClient } from './FlagsClient'; -import type { DatadogFlagsConfiguration } from './types'; +import type { DatadogFlagsType, DatadogFlagsConfiguration } from './types'; -class DatadogFlagsWrapper { +const FLAGS_MODULE = 'com.datadog.reactnative.flags'; + +class DatadogFlagsWrapper implements DatadogFlagsType { getClient = (clientName: string = 'default'): FlagsClient => { + // TODO: Do we have to track whether .enabled() was called before .getClient() could be called? return new FlagsClient(clientName); }; @@ -27,6 +31,7 @@ class DatadogFlagsWrapper { }; } -const DatadogFlags = new DatadogFlagsWrapper(); - -export { DatadogFlags }; +export const DatadogFlags: DatadogFlagsType = getGlobalInstance( + FLAGS_MODULE, + () => new DatadogFlagsWrapper() +); diff --git a/packages/core/src/flags/types.ts b/packages/core/src/flags/types.ts index 0f652ecf1..e5964d5b5 100644 --- a/packages/core/src/flags/types.ts +++ b/packages/core/src/flags/types.ts @@ -4,36 +4,202 @@ * Copyright 2016-Present Datadog, Inc. */ +import type { FlagsClient } from './FlagsClient'; + +export type DatadogFlagsType = { + /** + * Returns a `FlagsClient` instance for further feature flag evaluation. + * + * If client name is not provided, the `'default'` client is returned. + */ + getClient: (clientName?: string) => FlagsClient; + /** + * Enables the Datadog Flags feature. + * + * TODO: This method is no-op for now, as flags are initialized globally by default. + */ + enable: (configuration: DatadogFlagsConfiguration) => Promise; +}; + /** - * Configuration settings for flags. + * Configuration options for the Datadog Flags feature. + * + * Use this type to customize the behavior of feature flag evaluation, including custom endpoints, + * exposure tracking, and error handling modes. */ export interface DatadogFlagsConfiguration { + /** + * Controls whether the feature flag evaluation feature is enabled. + */ enabled: boolean; + /** + * Controls error handling behavior for `FlagsClient` API misuse. + * + * This setting determines how the SDK responds to incorrect usage, such as: + * - Creating a `FlagsClient` before calling `Flags.enable()` + * + * Error handling is selected based on the build configuration and this setting: + * - Release builds use safe defaults with SDK level-based logging + * - Debug builds with `gracefulModeEnabled = true` log warnings to console instead of crashing + * - Debug builds with `gracefulModeEnabled = false` crashes with fatal errors for fail-fast development + * + * Recommended usage: + * - Set to `false` in development, test, and QA builds for immediate error detection + * - Set to `true` (default) in dogfooding and staging environments for visible warnings without crashes + * - Production builds always handle errors gracefully regardless of this setting + * + * @default true + */ gracefulModeEnabled?: boolean; + /** + * Custom server URL for retrieving flag assignments. + * + * If not set, the SDK uses the default Datadog Flags endpoint for the configured site. + * + * @default undefined + */ customFlagsEndpoint?: string; + /** + * Additional HTTP headers to attach to requests made to `customFlagsEndpoint`. + * + * Useful for authentication or routing when using your own Flags service. Ignored when using the default Datadog endpoint. + * + * @default undefined + */ customFlagsHeaders?: Record; + /** + * Custom server URL for sending Flags exposure data. + * + * If not set, the SDK uses the default Datadog Flags exposure endpoint. + * + * @default undefined + */ customExposureEndpoint?: string; + /** + * Enables exposure logging via the dedicated exposures intake endpoint. + * + * When enabled, flag evaluation events are sent to the exposures endpoint for analytics and monitoring. + * + * @default true + */ trackExposures?: boolean; + /** + * Enables the RUM integration. + * + * When enabled, flag evaluation events are sent to RUM for correlation with user sessions. + * + * @default true + */ rumIntegrationEnabled?: boolean; } /** - * Evaluation context for flags. + * Context information used for feature flag targeting and evaluation. + * + * The evaluation context contains user or session information that determines which flag + * variations are returned. This typically includes a unique identifier (targeting key) and + * optional custom attributes for more granular targeting. + * + * You can create an evaluation context and set it on the client before evaluating flags: + * + * ```ts + * const context: EvaluationContext = { + * targetingKey: "user-123", + * attributes: { + * "email": "user@example.com", + * "plan": "premium", + * "age": 25, + * "beta_tester": true + * } + * }; + * + * await client.setEvaluationContext(context); + * ``` */ export interface EvaluationContext { + /** + * The unique identifier used for targeting this user or session. + * + * This is typically a user ID, session ID, or device ID. The targeting key is used + * by the feature flag service to determine which variation to serve. + */ targetingKey: string; + + /** + * Custom attributes for more granular targeting. + * + * Attributes can include user properties, session data, or any other contextual information + * needed for flag evaluation rules. + */ attributes: Record; } +/** + * An error tha occurs during feature flag evaluation. + * + * Indicates why a flag evaluation may have failed or returned a default value. + */ export type FlagEvaluationError = | 'PROVIDER_NOT_READY' | 'FLAG_NOT_FOUND' | 'TYPE_MISMATCH'; +/** + * Detailed information about a feature flag evaluation. + * + * `FlagDetails` contains both the evaluated flag value and metadata about the evaluation, + * including the variant served, evaluation reason, and any errors that occurred. + * + * Use this type when you need access to evaluation metadata beyond just the flag value: + * + * ```ts + * const details = await flagsClient.getBooleanDetails('new-feature', false); + * + * if (details.value) { + * // Feature is enabled + * console.log(`Using variant: ${details.variant ?? 'default'}`); + * } + * + * if (details.error) { + * console.log(`Evaluation error: ${details.error}`); + * } + * ``` + */ export interface FlagDetails { + /** + * The feature flag key that was evaluated. + */ key: string; + /** + * The evaluated flag value. + * + * This is either the flag's assigned value or the default value if evaluation failed. + */ value: T; + /** + * The variant key for the evaluated flag. + * + * Variants identify which version of the flag was served. Returns `null` if the flag + * was not found or if the default value was used. + * + * ```ts + * const details = await flagsClient.getBooleanDetails('new-feature', false); + * console.log(`Served variant: ${details.variant ?? 'default'}`); + * ``` + */ variant: string | null; + /** + * The reason why this evaluation result was returned. + * + * Provides context about how the flag was evaluated, such as "TARGETING_MATCH" or "DEFAULT". + * Returns `null` if the flag was not found. + */ reason: string | null; + /** + * The error that occurred during evaluation, if any. + * + * Returns `null` if evaluation succeeded. Check this property to determine if the returned + * value is from a successful evaluation or a fallback to the default value. + */ error: FlagEvaluationError | null; } From 257f683f9aeaecc7a1b428330d4ea4738677b295 Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Mon, 17 Nov 2025 14:58:37 +0200 Subject: [PATCH 063/526] Hard set `gracefulModeEnabled` to true, add a warning for misconfigured DatadogFlags --- example-new-architecture/App.tsx | 21 ++++++++----------- .../ios/Sources/DdFlagsImplementation.swift | 12 +++++++---- .../ios/Sources/RNDdSdkConfiguration.swift | 6 ++++-- packages/core/src/DdSdkReactNative.tsx | 8 +++++++ packages/core/src/flags/DatadogFlags.ts | 18 +++++++++++----- packages/core/src/flags/types.ts | 19 ----------------- 6 files changed, 42 insertions(+), 42 deletions(-) diff --git a/example-new-architecture/App.tsx b/example-new-architecture/App.tsx index 42be78f5e..f8c91d870 100644 --- a/example-new-architecture/App.tsx +++ b/example-new-architecture/App.tsx @@ -50,6 +50,7 @@ import {APPLICATION_ID, CLIENT_TOKEN, ENVIRONMENT} from './ddCredentials'; enabled: true, }; await DdSdkReactNative.initialize(config); + await DatadogFlags.enable(config.flagsConfiguration); await DdRum.startView('main', 'Main'); setTimeout(async () => { await DdRum.addTiming('one_second'); @@ -58,16 +59,6 @@ import {APPLICATION_ID, CLIENT_TOKEN, ENVIRONMENT} from './ddCredentials'; await DdLogs.info('info log'); const spanId = await DdTrace.startSpan('test span'); await DdTrace.finishSpan(spanId); - - const flagsClient = DatadogFlags.getClient(); - - await flagsClient.setEvaluationContext({ - targetingKey: 'test-user-1', - attributes: { - country: 'US', - }, - }); - })(); type SectionProps = PropsWithChildren<{ @@ -107,6 +98,14 @@ function App(): React.JSX.Element { (async () => { const flagsClient = DatadogFlags.getClient(); + // Set flag evaluation context. + await flagsClient.setEvaluationContext({ + targetingKey: 'test-user-1', + attributes: { + country: 'US', + }, + }); + const [booleanValue, stringValue, jsonValue, integerValue, numberValue] = await Promise.all([ flagsClient.getBooleanDetails('rn-sdk-test-boolean-flag', false), // https://app.datadoghq.com/feature-flags/046d0e70-626d-41e1-8314-3f009fb79b7a?environmentId=d114cd9a-79ed-4c56-bcf3-bcac9293653b flagsClient.getStringDetails('rn-sdk-test-string-flag', 'default-value'), // https://app.datadoghq.com/feature-flags/80756d8f-a375-437a-a023-b490c91cd506?environmentId=d114cd9a-79ed-4c56-bcf3-bcac9293653b @@ -123,8 +122,6 @@ function App(): React.JSX.Element { number: numberValue, }; - console.log({newValues}); - setFlagValues(newValues); })().catch(console.error); }, []); diff --git a/packages/core/ios/Sources/DdFlagsImplementation.swift b/packages/core/ios/Sources/DdFlagsImplementation.swift index 0c70d7d1f..7c943d0e0 100644 --- a/packages/core/ios/Sources/DdFlagsImplementation.swift +++ b/packages/core/ios/Sources/DdFlagsImplementation.swift @@ -5,23 +5,27 @@ */ import Foundation +import DatadogInternal import DatadogFlags @objc public class DdFlagsImplementation: NSObject { - // Store a registry of client providers by name - // Use providers instead of direct clients to ensure lazy initialization private var clientProviders: [String: () -> FlagsClientProtocol] = [:] + /// Retrieve a `FlagsClient` instance in a non-interruptive way for usage in methods bridged to React Native. + /// + /// We create a simple registry of client providers by client name holding closures for retrieving a client since client references are kept internally in the flagging SDK. + /// This is motivated by the fact that it is impossible to create a bridged synchronous `FlagsClient` creation; thus, we create a client instance dynamically on-demand. + /// + /// - Important: Due to specifics of React Native hot reloading, this registry is destroyed upon JS bundle refresh. This leads to`FlagsClient.create` being called several times during development process for the same client. + /// This should not be a problem because `gracefulModeEnabled` is hard set to `true` for the RN SDK. private func getClient(name: String) -> FlagsClientProtocol { if let provider = clientProviders[name] { return provider() } let client = FlagsClient.create(name: name) - clientProviders[name] = { FlagsClient.shared(named: name) } - return client } diff --git a/packages/core/ios/Sources/RNDdSdkConfiguration.swift b/packages/core/ios/Sources/RNDdSdkConfiguration.swift index cfdb4d89a..ff0a46807 100644 --- a/packages/core/ios/Sources/RNDdSdkConfiguration.swift +++ b/packages/core/ios/Sources/RNDdSdkConfiguration.swift @@ -109,7 +109,9 @@ extension NSDictionary { return nil } - let gracefulModeEnabled = object(forKey: "gracefulModeEnabled") as? Bool + // Hard set `gracefulModeEnabled` to `true` because this misconfiguration is handled on JS side. + let gracefulModeEnabled = true + let customFlagsHeaders = object(forKey: "customFlagsHeaders") as? [String: String] let trackExposures = object(forKey: "trackExposures") as? Bool let rumIntegrationEnabled = object(forKey: "rumIntegrationEnabled") as? Bool @@ -124,7 +126,7 @@ extension NSDictionary { } return Flags.Configuration( - gracefulModeEnabled: gracefulModeEnabled ?? true, + gracefulModeEnabled: gracefulModeEnabled, customFlagsEndpoint: customFlagsEndpointURL, customFlagsHeaders: customFlagsHeaders, customExposureEndpoint: customExposureEndpointURL, diff --git a/packages/core/src/DdSdkReactNative.tsx b/packages/core/src/DdSdkReactNative.tsx index b9907f93d..b7305d67a 100644 --- a/packages/core/src/DdSdkReactNative.tsx +++ b/packages/core/src/DdSdkReactNative.tsx @@ -353,6 +353,14 @@ export class DdSdkReactNative { ] = `${reactNativeVersion}`; } + // Hard set `gracefulModeEnabled` to `true` because crashing an app on misconfiguration + // is not the usual workflow for React Native. + if (configuration.flagsConfiguration) { + Object.assign(configuration.flagsConfiguration, { + gracefulModeEnabled: true + }); + } + return new DdSdkConfiguration( configuration.clientToken, configuration.env, diff --git a/packages/core/src/flags/DatadogFlags.ts b/packages/core/src/flags/DatadogFlags.ts index e5e65fb5c..8e8d2e98e 100644 --- a/packages/core/src/flags/DatadogFlags.ts +++ b/packages/core/src/flags/DatadogFlags.ts @@ -14,18 +14,26 @@ import type { DatadogFlagsType, DatadogFlagsConfiguration } from './types'; const FLAGS_MODULE = 'com.datadog.reactnative.flags'; class DatadogFlagsWrapper implements DatadogFlagsType { + private _isEnabled = false; + getClient = (clientName: string = 'default'): FlagsClient => { - // TODO: Do we have to track whether .enabled() was called before .getClient() could be called? + if (__DEV__) { + if (!this._isEnabled) { + InternalLog.log( + 'DatadogFlags.getClient() called before DatadogFlags have been initialized. Flag evaluations will resolve to default values.', + SdkVerbosity.ERROR + ); + } + } + return new FlagsClient(clientName); }; enable = async ( _configuration: DatadogFlagsConfiguration ): Promise => { - InternalLog.log( - 'No-op DatadogFlags.enable() called. Flags are initialized globally by default for now.', - SdkVerbosity.DEBUG - ); + // Feature Flags are initialized globally by default for now. + this._isEnabled = _configuration.enabled; return Promise.resolve(); }; diff --git a/packages/core/src/flags/types.ts b/packages/core/src/flags/types.ts index e5964d5b5..6ecb0880f 100644 --- a/packages/core/src/flags/types.ts +++ b/packages/core/src/flags/types.ts @@ -32,25 +32,6 @@ export interface DatadogFlagsConfiguration { * Controls whether the feature flag evaluation feature is enabled. */ enabled: boolean; - /** - * Controls error handling behavior for `FlagsClient` API misuse. - * - * This setting determines how the SDK responds to incorrect usage, such as: - * - Creating a `FlagsClient` before calling `Flags.enable()` - * - * Error handling is selected based on the build configuration and this setting: - * - Release builds use safe defaults with SDK level-based logging - * - Debug builds with `gracefulModeEnabled = true` log warnings to console instead of crashing - * - Debug builds with `gracefulModeEnabled = false` crashes with fatal errors for fail-fast development - * - * Recommended usage: - * - Set to `false` in development, test, and QA builds for immediate error detection - * - Set to `true` (default) in dogfooding and staging environments for visible warnings without crashes - * - Production builds always handle errors gracefully regardless of this setting - * - * @default true - */ - gracefulModeEnabled?: boolean; /** * Custom server URL for retrieving flag assignments. * From 67c8284bfc4c97b5240a31d6704a17f71acd4c82 Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Mon, 17 Nov 2025 17:03:43 +0200 Subject: [PATCH 064/526] Remove stale FIXME --- packages/core/src/flags/FlagsClient.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/core/src/flags/FlagsClient.ts b/packages/core/src/flags/FlagsClient.ts index 3e1606a63..b07c9b731 100644 --- a/packages/core/src/flags/FlagsClient.ts +++ b/packages/core/src/flags/FlagsClient.ts @@ -147,7 +147,6 @@ export class FlagsClient { key: string, defaultValue: { [key: string]: unknown } ): Promise<{ [key: string]: unknown }> => { - // FIXME: This is broken at the moment due to issues with JSON parsing on native iOS SDK side. const details = await this.getObjectDetails(key, defaultValue); return details.value; }; From 70484e6d6249ca86f2b784838f9c941dbb398d18 Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Mon, 17 Nov 2025 19:42:19 +0200 Subject: [PATCH 065/526] Minor improvements to error handling --- example-new-architecture/App.tsx | 2 +- .../ios/Sources/DdFlagsImplementation.swift | 13 ++++++++++- packages/core/src/flags/DatadogFlags.ts | 16 ++++++------- packages/core/src/flags/FlagsClient.ts | 23 ++++++++++++++----- 4 files changed, 37 insertions(+), 17 deletions(-) diff --git a/example-new-architecture/App.tsx b/example-new-architecture/App.tsx index f8c91d870..454026708 100644 --- a/example-new-architecture/App.tsx +++ b/example-new-architecture/App.tsx @@ -123,7 +123,7 @@ function App(): React.JSX.Element { }; setFlagValues(newValues); - })().catch(console.error); + })().catch(error => console.error(error.message)); }, []); diff --git a/packages/core/ios/Sources/DdFlagsImplementation.swift b/packages/core/ios/Sources/DdFlagsImplementation.swift index 7c943d0e0..f0eab15a3 100644 --- a/packages/core/ios/Sources/DdFlagsImplementation.swift +++ b/packages/core/ios/Sources/DdFlagsImplementation.swift @@ -52,7 +52,18 @@ public class DdFlagsImplementation: NSObject { case .success: resolve(nil) case .failure(let error): - reject(error.localizedDescription, "", error) + var errorCode: String + switch (error) { + case .clientNotInitialized: + errorCode = "CLIENT_NOT_INITIALIZED" + case .invalidConfiguration: + errorCode = "INVALID_CONFIGURATION" + case .invalidResponse: + errorCode = "INVALID_RESPONSE" + case .networkError: + errorCode = "NETWORK_ERROR" + } + reject(nil, errorCode, error) } } } diff --git a/packages/core/src/flags/DatadogFlags.ts b/packages/core/src/flags/DatadogFlags.ts index 8e8d2e98e..195881112 100644 --- a/packages/core/src/flags/DatadogFlags.ts +++ b/packages/core/src/flags/DatadogFlags.ts @@ -14,16 +14,14 @@ import type { DatadogFlagsType, DatadogFlagsConfiguration } from './types'; const FLAGS_MODULE = 'com.datadog.reactnative.flags'; class DatadogFlagsWrapper implements DatadogFlagsType { - private _isEnabled = false; + private isFeatureEnabled = false; getClient = (clientName: string = 'default'): FlagsClient => { - if (__DEV__) { - if (!this._isEnabled) { - InternalLog.log( - 'DatadogFlags.getClient() called before DatadogFlags have been initialized. Flag evaluations will resolve to default values.', - SdkVerbosity.ERROR - ); - } + if (!this.isFeatureEnabled) { + InternalLog.log( + 'DatadogFlags.getClient() called before DatadogFlags have been initialized. Flag evaluations will resolve to default values.', + SdkVerbosity.ERROR + ); } return new FlagsClient(clientName); @@ -33,7 +31,7 @@ class DatadogFlagsWrapper implements DatadogFlagsType { _configuration: DatadogFlagsConfiguration ): Promise => { // Feature Flags are initialized globally by default for now. - this._isEnabled = _configuration.enabled; + this.isFeatureEnabled = _configuration.enabled; return Promise.resolve(); }; diff --git a/packages/core/src/flags/FlagsClient.ts b/packages/core/src/flags/FlagsClient.ts index b07c9b731..e9ccf00f4 100644 --- a/packages/core/src/flags/FlagsClient.ts +++ b/packages/core/src/flags/FlagsClient.ts @@ -4,6 +4,8 @@ * Copyright 2016-Present Datadog, Inc. */ +import { InternalLog } from '../InternalLog'; +import { SdkVerbosity } from '../SdkVerbosity'; import type { DdNativeFlagsType } from '../nativeModulesTypes'; import type { EvaluationContext, FlagDetails } from './types'; @@ -24,11 +26,20 @@ export class FlagsClient { ): Promise => { const { targetingKey, attributes } = context; - await this.nativeFlags.setEvaluationContext( - this.clientName, - targetingKey, - attributes - ); + try { + await this.nativeFlags.setEvaluationContext( + this.clientName, + targetingKey, + attributes + ); + } catch (error) { + if (error instanceof Error) { + InternalLog.log( + `Error setting flag evaluation context: ${error.message}`, + SdkVerbosity.ERROR + ); + } + } }; getBooleanDetails = async ( @@ -101,7 +112,7 @@ export class FlagsClient { key: string, defaultValue: { [key: string]: unknown } ): Promise> => { - if (typeof defaultValue !== 'object') { + if (typeof defaultValue !== 'object' || defaultValue === null) { return { key, value: defaultValue, From c7fea30cd20b277577149b3446eb20697f93cb73 Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Mon, 17 Nov 2025 20:27:49 +0200 Subject: [PATCH 066/526] Revert Datadog deps to 3.1.0, add them to old arch app --- example-new-architecture/ios/Podfile | 18 +- example-new-architecture/ios/Podfile.lock | 126 +++--- example/ios/Podfile | 10 + example/ios/Podfile.lock | 411 +++++++++++++++++--- packages/core/DatadogSDKReactNative.podspec | 14 +- 5 files changed, 439 insertions(+), 140 deletions(-) diff --git a/example-new-architecture/ios/Podfile b/example-new-architecture/ios/Podfile index 6a063cedd..5810da564 100644 --- a/example-new-architecture/ios/Podfile +++ b/example-new-architecture/ios/Podfile @@ -19,15 +19,15 @@ end target 'DdSdkReactNativeExample' do pod 'DatadogSDKReactNative', :path => '../../packages/core/DatadogSDKReactNative.podspec', :testspecs => ['Tests'] - # Flags don't seem to be released yet so we pull them from the iOS SDK develop branch. - pod 'DatadogInternal', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :commit => '2dfe2d0ff' - pod 'DatadogCore', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :commit => '2dfe2d0ff' - pod 'DatadogLogs', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :commit => '2dfe2d0ff' - pod 'DatadogTrace', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :commit => '2dfe2d0ff' - pod 'DatadogRUM', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :commit => '2dfe2d0ff' - pod 'DatadogCrashReporting', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :commit => '2dfe2d0ff' - pod 'DatadogFlags', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :commit => '2dfe2d0ff' - pod 'DatadogWebViewTracking', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :commit => '2dfe2d0ff' + # Pin Datadog* dependencies to a specific reference until they are updated in feature/v3. + pod 'DatadogInternal', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :branch => 'feature/flags' + pod 'DatadogCore', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :branch => 'feature/flags' + pod 'DatadogLogs', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :branch => 'feature/flags' + pod 'DatadogTrace', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :branch => 'feature/flags' + pod 'DatadogRUM', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :branch => 'feature/flags' + pod 'DatadogCrashReporting', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :branch => 'feature/flags' + pod 'DatadogFlags', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :branch => 'feature/flags' + pod 'DatadogWebViewTracking', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :branch => 'feature/flags' config = use_native_modules! diff --git a/example-new-architecture/ios/Podfile.lock b/example-new-architecture/ios/Podfile.lock index 93fa5e8f1..4a49e5325 100644 --- a/example-new-architecture/ios/Podfile.lock +++ b/example-new-architecture/ios/Podfile.lock @@ -1,25 +1,25 @@ PODS: - boost (1.84.0) - - DatadogCore (3.2.0): - - DatadogInternal (= 3.2.0) - - DatadogCrashReporting (3.2.0): - - DatadogInternal (= 3.2.0) + - DatadogCore (3.1.0): + - DatadogInternal (= 3.1.0) + - DatadogCrashReporting (3.1.0): + - DatadogInternal (= 3.1.0) - PLCrashReporter (~> 1.12.0) - - DatadogFlags (3.2.0): - - DatadogInternal (= 3.2.0) - - DatadogInternal (3.2.0) - - DatadogLogs (3.2.0): - - DatadogInternal (= 3.2.0) - - DatadogRUM (3.2.0): - - DatadogInternal (= 3.2.0) + - DatadogFlags (3.1.0): + - DatadogInternal (= 3.1.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.2.0) - - DatadogCrashReporting (= 3.2.0) - - DatadogFlags (= 3.2.0) - - DatadogLogs (= 3.2.0) - - DatadogRUM (= 3.2.0) - - DatadogTrace (= 3.2.0) - - DatadogWebViewTracking (= 3.2.0) + - DatadogCore (= 3.1.0) + - DatadogCrashReporting (= 3.1.0) + - DatadogFlags (= 3.1.0) + - DatadogLogs (= 3.1.0) + - DatadogRUM (= 3.1.0) + - DatadogTrace (= 3.1.0) + - DatadogWebViewTracking (= 3.1.0) - DoubleConversion - glog - hermes-engine @@ -41,13 +41,13 @@ PODS: - ReactCommon/turbomodule/core - Yoga - DatadogSDKReactNative/Tests (2.13.0): - - DatadogCore (= 3.2.0) - - DatadogCrashReporting (= 3.2.0) - - DatadogFlags (= 3.2.0) - - DatadogLogs (= 3.2.0) - - DatadogRUM (= 3.2.0) - - DatadogTrace (= 3.2.0) - - DatadogWebViewTracking (= 3.2.0) + - DatadogCore (= 3.1.0) + - DatadogCrashReporting (= 3.1.0) + - DatadogFlags (= 3.1.0) + - DatadogLogs (= 3.1.0) + - DatadogRUM (= 3.1.0) + - DatadogTrace (= 3.1.0) + - DatadogWebViewTracking (= 3.1.0) - DoubleConversion - glog - hermes-engine @@ -68,11 +68,11 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - DatadogTrace (3.2.0): - - DatadogInternal (= 3.2.0) + - DatadogTrace (3.1.0): + - DatadogInternal (= 3.1.0) - OpenTelemetrySwiftApi (= 1.13.1) - - DatadogWebViewTracking (3.2.0): - - DatadogInternal (= 3.2.0) + - DatadogWebViewTracking (3.1.0): + - DatadogInternal (= 3.1.0) - DoubleConversion (1.1.6) - fast_float (6.1.4) - FBLazyVector (0.76.9) @@ -1638,16 +1638,16 @@ PODS: DEPENDENCIES: - boost (from `../node_modules/react-native/third-party-podspecs/boost.podspec`) - - DatadogCore (from `https://github.com/DataDog/dd-sdk-ios.git`, commit `2dfe2d0ff`) - - DatadogCrashReporting (from `https://github.com/DataDog/dd-sdk-ios.git`, commit `2dfe2d0ff`) - - DatadogFlags (from `https://github.com/DataDog/dd-sdk-ios.git`, commit `2dfe2d0ff`) - - DatadogInternal (from `https://github.com/DataDog/dd-sdk-ios.git`, commit `2dfe2d0ff`) - - DatadogLogs (from `https://github.com/DataDog/dd-sdk-ios.git`, commit `2dfe2d0ff`) - - DatadogRUM (from `https://github.com/DataDog/dd-sdk-ios.git`, commit `2dfe2d0ff`) + - DatadogCore (from `https://github.com/DataDog/dd-sdk-ios.git`, branch `feature/flags`) + - DatadogCrashReporting (from `https://github.com/DataDog/dd-sdk-ios.git`, branch `feature/flags`) + - DatadogFlags (from `https://github.com/DataDog/dd-sdk-ios.git`, branch `feature/flags`) + - DatadogInternal (from `https://github.com/DataDog/dd-sdk-ios.git`, branch `feature/flags`) + - DatadogLogs (from `https://github.com/DataDog/dd-sdk-ios.git`, branch `feature/flags`) + - DatadogRUM (from `https://github.com/DataDog/dd-sdk-ios.git`, branch `feature/flags`) - DatadogSDKReactNative (from `../../packages/core/DatadogSDKReactNative.podspec`) - DatadogSDKReactNative/Tests (from `../../packages/core/DatadogSDKReactNative.podspec`) - - DatadogTrace (from `https://github.com/DataDog/dd-sdk-ios.git`, commit `2dfe2d0ff`) - - DatadogWebViewTracking (from `https://github.com/DataDog/dd-sdk-ios.git`, commit `2dfe2d0ff`) + - DatadogTrace (from `https://github.com/DataDog/dd-sdk-ios.git`, branch `feature/flags`) + - DatadogWebViewTracking (from `https://github.com/DataDog/dd-sdk-ios.git`, branch `feature/flags`) - DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`) - fast_float (from `../node_modules/react-native/third-party-podspecs/fast_float.podspec`) - FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`) @@ -1724,30 +1724,30 @@ EXTERNAL SOURCES: boost: :podspec: "../node_modules/react-native/third-party-podspecs/boost.podspec" DatadogCore: - :commit: 2dfe2d0ff + :branch: feature/flags :git: https://github.com/DataDog/dd-sdk-ios.git DatadogCrashReporting: - :commit: 2dfe2d0ff + :branch: feature/flags :git: https://github.com/DataDog/dd-sdk-ios.git DatadogFlags: - :commit: 2dfe2d0ff + :branch: feature/flags :git: https://github.com/DataDog/dd-sdk-ios.git DatadogInternal: - :commit: 2dfe2d0ff + :branch: feature/flags :git: https://github.com/DataDog/dd-sdk-ios.git DatadogLogs: - :commit: 2dfe2d0ff + :branch: feature/flags :git: https://github.com/DataDog/dd-sdk-ios.git DatadogRUM: - :commit: 2dfe2d0ff + :branch: feature/flags :git: https://github.com/DataDog/dd-sdk-ios.git DatadogSDKReactNative: :path: "../../packages/core/DatadogSDKReactNative.podspec" DatadogTrace: - :commit: 2dfe2d0ff + :branch: feature/flags :git: https://github.com/DataDog/dd-sdk-ios.git DatadogWebViewTracking: - :commit: 2dfe2d0ff + :branch: feature/flags :git: https://github.com/DataDog/dd-sdk-ios.git DoubleConversion: :podspec: "../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec" @@ -1879,41 +1879,41 @@ EXTERNAL SOURCES: CHECKOUT OPTIONS: DatadogCore: - :commit: 2dfe2d0ff + :commit: 18ee3ce825a9806c350133179f5e38168d78e0ba :git: https://github.com/DataDog/dd-sdk-ios.git DatadogCrashReporting: - :commit: 2dfe2d0ff + :commit: 18ee3ce825a9806c350133179f5e38168d78e0ba :git: https://github.com/DataDog/dd-sdk-ios.git DatadogFlags: - :commit: 2dfe2d0ff + :commit: 18ee3ce825a9806c350133179f5e38168d78e0ba :git: https://github.com/DataDog/dd-sdk-ios.git DatadogInternal: - :commit: 2dfe2d0ff + :commit: 18ee3ce825a9806c350133179f5e38168d78e0ba :git: https://github.com/DataDog/dd-sdk-ios.git DatadogLogs: - :commit: 2dfe2d0ff + :commit: 18ee3ce825a9806c350133179f5e38168d78e0ba :git: https://github.com/DataDog/dd-sdk-ios.git DatadogRUM: - :commit: 2dfe2d0ff + :commit: 18ee3ce825a9806c350133179f5e38168d78e0ba :git: https://github.com/DataDog/dd-sdk-ios.git DatadogTrace: - :commit: 2dfe2d0ff + :commit: 18ee3ce825a9806c350133179f5e38168d78e0ba :git: https://github.com/DataDog/dd-sdk-ios.git DatadogWebViewTracking: - :commit: 2dfe2d0ff + :commit: 18ee3ce825a9806c350133179f5e38168d78e0ba :git: https://github.com/DataDog/dd-sdk-ios.git SPEC CHECKSUMS: boost: 1dca942403ed9342f98334bf4c3621f011aa7946 - DatadogCore: 8f360d91ec79c8799e753ec1abe3169911ee50fa - DatadogCrashReporting: 0a392c47eaf0294df7e04c9ae113e97612af63b3 - DatadogFlags: 2a06a1258e78686246e5383ef90ee71f53dc6bff - DatadogInternal: 291b5ad0142280a651ab196ebd30dcd1d37cf6ff - DatadogLogs: 3ce559b785c8013911be4777845e46202046e618 - DatadogRUM: f26899d603f1797b4a41e00b5ee1aa0f6c972ef2 - DatadogSDKReactNative: 8b0b92cb7ec31e34c97ec6671908662d9203f0ca - DatadogTrace: 95e5fb69876ce3ab223dd8ef3036605c89db3cdf - DatadogWebViewTracking: 6b2d5e5ab1e85481e458f9d0130294078549558b + DatadogCore: d2f51c7fb4308cf3c25e55e2e7242e5d558ee71d + DatadogCrashReporting: f636f1d1c534572c0b0abcdc59df244c884d825d + DatadogFlags: d4237ffb9c06096d1928dbe47aac877739bc6326 + DatadogInternal: 7837b2ce3d525d429682532eeda697b181299fdc + DatadogLogs: 250894b5a99da5b924a019049c0d0326823cdbd6 + DatadogRUM: 0d2a60e1abb8aacfb8827ef84f6d5deb4d5026c8 + DatadogSDKReactNative: e74b171da3b103bf9b2fd372f480fa71c230830d + DatadogTrace: f59e933074cd285ad7e9f5af991f8fe04b095991 + DatadogWebViewTracking: 9bc92b4147aeed47eb1911451f651094aa6dd6c1 DoubleConversion: f16ae600a246532c4020132d54af21d0ddb2a385 fast_float: 06eeec4fe712a76acc9376682e4808b05ce978b6 FBLazyVector: 7605ea4810e0e10ae4815292433c09bf4324ba45 @@ -1981,6 +1981,6 @@ SPEC CHECKSUMS: SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 Yoga: feb4910aba9742cfedc059e2b2902e22ffe9954a -PODFILE CHECKSUM: bda441670d020698768356464e7ec5c2b0573608 +PODFILE CHECKSUM: c0a8ceabe25801227323f2a415c464bfca5e3f73 COCOAPODS: 1.16.2 diff --git a/example/ios/Podfile b/example/ios/Podfile index 1736870c9..da3575afe 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -21,6 +21,16 @@ target 'ddSdkReactnativeExample' do pod 'DatadogSDKReactNativeSessionReplay', :path => '../../packages/react-native-session-replay/DatadogSDKReactNativeSessionReplay.podspec', :testspecs => ['Tests'] pod 'DatadogSDKReactNativeWebView', :path => '../../packages/react-native-webview/DatadogSDKReactNativeWebView.podspec', :testspecs => ['Tests'] + # Pin Datadog* dependencies to a specific reference until they are updated in feature/v3. + pod 'DatadogInternal', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :branch => 'feature/flags' + pod 'DatadogCore', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :branch => 'feature/flags' + pod 'DatadogLogs', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :branch => 'feature/flags' + pod 'DatadogTrace', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :branch => 'feature/flags' + pod 'DatadogRUM', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :branch => 'feature/flags' + pod 'DatadogCrashReporting', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :branch => 'feature/flags' + pod 'DatadogFlags', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :branch => 'feature/flags' + pod 'DatadogWebViewTracking', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :branch => 'feature/flags' + config = use_native_modules! use_react_native!( diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index ca237868f..bac047afc 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -5,6 +5,8 @@ PODS: - DatadogCrashReporting (3.1.0): - DatadogInternal (= 3.1.0) - PLCrashReporter (~> 1.12.0) + - DatadogFlags (3.1.0): + - DatadogInternal (= 3.1.0) - DatadogInternal (3.1.0) - DatadogLogs (3.1.0): - DatadogInternal (= 3.1.0) @@ -13,19 +15,59 @@ PODS: - DatadogSDKReactNative (2.13.0): - DatadogCore (= 3.1.0) - DatadogCrashReporting (= 3.1.0) + - DatadogFlags (= 3.1.0) - DatadogLogs (= 3.1.0) - DatadogRUM (= 3.1.0) - DatadogTrace (= 3.1.0) - DatadogWebViewTracking (= 3.1.0) + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.10.14.00) + - RCTRequired + - RCTTypeSafety - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga - DatadogSDKReactNative/Tests (2.13.0): - DatadogCore (= 3.1.0) - DatadogCrashReporting (= 3.1.0) + - DatadogFlags (= 3.1.0) - DatadogLogs (= 3.1.0) - DatadogRUM (= 3.1.0) - DatadogTrace (= 3.1.0) - DatadogWebViewTracking (= 3.1.0) + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.10.14.00) + - RCTRequired + - RCTTypeSafety - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga - DatadogSDKReactNativeSessionReplay (2.13.0): - DatadogSDKReactNative - DatadogSessionReplay (= 3.1.0) @@ -77,14 +119,52 @@ PODS: - DatadogInternal (= 3.1.0) - DatadogSDKReactNative - DatadogWebViewTracking (= 3.1.0) + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.10.14.00) + - RCTRequired + - RCTTypeSafety - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga - DatadogSDKReactNativeWebView/Tests (2.13.0): - DatadogInternal (= 3.1.0) - DatadogSDKReactNative - DatadogWebViewTracking (= 3.1.0) + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.10.14.00) + - RCTRequired + - RCTTypeSafety - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager - react-native-webview + - React-NativeModulesApple + - React-RCTFabric - React-RCTText + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga - DatadogSessionReplay (3.1.0): - DatadogInternal (= 3.1.0) - DatadogTrace (3.1.0): @@ -142,6 +222,7 @@ PODS: - React-RCTText (= 0.76.9) - React-RCTVibration (= 0.76.9) - React-callinvoker (0.76.9) + - React-Codegen (0.1.0) - React-Core (0.76.9): - glog - hermes-engine @@ -1384,7 +1465,71 @@ PODS: - react-native-crash-tester (0.2.3): - React-Core - react-native-safe-area-context (5.1.0): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.10.14.00) + - RCTRequired + - RCTTypeSafety - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - react-native-safe-area-context/common (= 5.1.0) + - react-native-safe-area-context/fabric (= 5.1.0) + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - react-native-safe-area-context/common (5.1.0): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.10.14.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - react-native-safe-area-context/fabric (5.1.0): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.10.14.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - react-native-safe-area-context/common + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga - react-native-webview (13.14.2): - DoubleConversion - glog @@ -1679,20 +1824,87 @@ PODS: - React-perflogger - React-utils (= 0.76.9) - ReactNativeNavigation (8.0.0-snapshot.1658): + - DoubleConversion + - glog + - hermes-engine - HMSegmentedControl + - RCT-Folly + - RCTRequired + - RCTTypeSafety + - React + - React-Codegen - React-Core - React-CoreModules + - React-cxxreact + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric - React-RCTImage - React-RCTText + - React-rendererdebug + - React-rncore + - React-runtimeexecutor + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core - ReactNativeNavigation/Core (= 8.0.0-snapshot.1658) + - Yoga - ReactNativeNavigation/Core (8.0.0-snapshot.1658): + - DoubleConversion + - glog + - hermes-engine - HMSegmentedControl + - RCT-Folly + - RCTRequired + - RCTTypeSafety + - React + - React-Codegen - React-Core - React-CoreModules + - React-cxxreact + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric - React-RCTImage - React-RCTText + - React-rendererdebug + - React-rncore + - React-runtimeexecutor + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga - RNCAsyncStorage (2.2.0): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.10.14.00) + - RCTRequired + - RCTTypeSafety - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga - RNGestureHandler (2.24.0): - DoubleConversion - glog @@ -1715,6 +1927,29 @@ PODS: - ReactCommon/turbomodule/core - Yoga - RNScreens (4.5.0): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.10.14.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric + - React-RCTImage + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - RNScreens/common (= 4.5.0) + - Yoga + - RNScreens/common (4.5.0): - DoubleConversion - glog - hermes-engine @@ -1741,12 +1976,20 @@ PODS: DEPENDENCIES: - boost (from `../node_modules/react-native/third-party-podspecs/boost.podspec`) + - DatadogCore (from `https://github.com/DataDog/dd-sdk-ios.git`, branch `feature/flags`) + - DatadogCrashReporting (from `https://github.com/DataDog/dd-sdk-ios.git`, branch `feature/flags`) + - DatadogFlags (from `https://github.com/DataDog/dd-sdk-ios.git`, branch `feature/flags`) + - DatadogInternal (from `https://github.com/DataDog/dd-sdk-ios.git`, branch `feature/flags`) + - DatadogLogs (from `https://github.com/DataDog/dd-sdk-ios.git`, branch `feature/flags`) + - DatadogRUM (from `https://github.com/DataDog/dd-sdk-ios.git`, branch `feature/flags`) - DatadogSDKReactNative (from `../../packages/core/DatadogSDKReactNative.podspec`) - DatadogSDKReactNative/Tests (from `../../packages/core/DatadogSDKReactNative.podspec`) - DatadogSDKReactNativeSessionReplay (from `../../packages/react-native-session-replay/DatadogSDKReactNativeSessionReplay.podspec`) - DatadogSDKReactNativeSessionReplay/Tests (from `../../packages/react-native-session-replay/DatadogSDKReactNativeSessionReplay.podspec`) - DatadogSDKReactNativeWebView (from `../../packages/react-native-webview/DatadogSDKReactNativeWebView.podspec`) - DatadogSDKReactNativeWebView/Tests (from `../../packages/react-native-webview/DatadogSDKReactNativeWebView.podspec`) + - DatadogTrace (from `https://github.com/DataDog/dd-sdk-ios.git`, branch `feature/flags`) + - DatadogWebViewTracking (from `https://github.com/DataDog/dd-sdk-ios.git`, branch `feature/flags`) - DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`) - fast_float (from `../node_modules/react-native/third-party-podspecs/fast_float.podspec`) - FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`) @@ -1822,28 +2065,46 @@ DEPENDENCIES: SPEC REPOS: https://github.com/CocoaPods/Specs.git: - - DatadogCore - - DatadogCrashReporting - - DatadogInternal - - DatadogLogs - - DatadogRUM - DatadogSessionReplay - - DatadogTrace - - DatadogWebViewTracking - HMSegmentedControl - OpenTelemetrySwiftApi - PLCrashReporter + - React-Codegen - SocketRocket EXTERNAL SOURCES: boost: :podspec: "../node_modules/react-native/third-party-podspecs/boost.podspec" + DatadogCore: + :branch: feature/flags + :git: https://github.com/DataDog/dd-sdk-ios.git + DatadogCrashReporting: + :branch: feature/flags + :git: https://github.com/DataDog/dd-sdk-ios.git + DatadogFlags: + :branch: feature/flags + :git: https://github.com/DataDog/dd-sdk-ios.git + DatadogInternal: + :branch: feature/flags + :git: https://github.com/DataDog/dd-sdk-ios.git + DatadogLogs: + :branch: feature/flags + :git: https://github.com/DataDog/dd-sdk-ios.git + DatadogRUM: + :branch: feature/flags + :git: https://github.com/DataDog/dd-sdk-ios.git DatadogSDKReactNative: :path: "../../packages/core/DatadogSDKReactNative.podspec" DatadogSDKReactNativeSessionReplay: :path: "../../packages/react-native-session-replay/DatadogSDKReactNativeSessionReplay.podspec" DatadogSDKReactNativeWebView: :path: "../../packages/react-native-webview/DatadogSDKReactNativeWebView.podspec" + DatadogTrace: + :branch: feature/flags + :git: https://github.com/DataDog/dd-sdk-ios.git + DatadogWebViewTracking: + :branch: feature/flags + :git: https://github.com/DataDog/dd-sdk-ios.git DoubleConversion: :podspec: "../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec" fast_float: @@ -1986,16 +2247,43 @@ EXTERNAL SOURCES: Yoga: :path: "../node_modules/react-native/ReactCommon/yoga" +CHECKOUT OPTIONS: + DatadogCore: + :commit: 18ee3ce825a9806c350133179f5e38168d78e0ba + :git: https://github.com/DataDog/dd-sdk-ios.git + DatadogCrashReporting: + :commit: 18ee3ce825a9806c350133179f5e38168d78e0ba + :git: https://github.com/DataDog/dd-sdk-ios.git + DatadogFlags: + :commit: 18ee3ce825a9806c350133179f5e38168d78e0ba + :git: https://github.com/DataDog/dd-sdk-ios.git + DatadogInternal: + :commit: 18ee3ce825a9806c350133179f5e38168d78e0ba + :git: https://github.com/DataDog/dd-sdk-ios.git + DatadogLogs: + :commit: 18ee3ce825a9806c350133179f5e38168d78e0ba + :git: https://github.com/DataDog/dd-sdk-ios.git + DatadogRUM: + :commit: 18ee3ce825a9806c350133179f5e38168d78e0ba + :git: https://github.com/DataDog/dd-sdk-ios.git + DatadogTrace: + :commit: 18ee3ce825a9806c350133179f5e38168d78e0ba + :git: https://github.com/DataDog/dd-sdk-ios.git + DatadogWebViewTracking: + :commit: 18ee3ce825a9806c350133179f5e38168d78e0ba + :git: https://github.com/DataDog/dd-sdk-ios.git + SPEC CHECKSUMS: boost: 1dca942403ed9342f98334bf4c3621f011aa7946 DatadogCore: d2f51c7fb4308cf3c25e55e2e7242e5d558ee71d DatadogCrashReporting: f636f1d1c534572c0b0abcdc59df244c884d825d + DatadogFlags: d4237ffb9c06096d1928dbe47aac877739bc6326 DatadogInternal: 7837b2ce3d525d429682532eeda697b181299fdc DatadogLogs: 250894b5a99da5b924a019049c0d0326823cdbd6 DatadogRUM: 0d2a60e1abb8aacfb8827ef84f6d5deb4d5026c8 - DatadogSDKReactNative: 822ff8092666172584d4d5e56f79c3799887d408 - DatadogSDKReactNativeSessionReplay: afc4e2b1db34ba8af3a442b0691359faaf5e586e - DatadogSDKReactNativeWebView: 00affefdaca0cf2375e669fa03925d8fa75263d0 + DatadogSDKReactNative: e74b171da3b103bf9b2fd372f480fa71c230830d + DatadogSDKReactNativeSessionReplay: fcd758a85e16ef29e0a56c56bcee0d4895fc0c64 + DatadogSDKReactNativeWebView: b4aefc87771441e1598254a2f890b8877613656e DatadogSessionReplay: 6bc71888e2b41dd0de3325f06f0c0b3cee0e6df4 DatadogTrace: f59e933074cd285ad7e9f5af991f8fe04b095991 DatadogWebViewTracking: 9bc92b4147aeed47eb1911451f651094aa6dd6c1 @@ -2008,72 +2296,73 @@ SPEC CHECKSUMS: HMSegmentedControl: 34c1f54d822d8308e7b24f5d901ec674dfa31352 OpenTelemetrySwiftApi: aaee576ed961e0c348af78df58b61300e95bd104 PLCrashReporter: db59ef96fa3d25f3650040d02ec2798cffee75f2 - RCT-Folly: ea9d9256ba7f9322ef911169a9f696e5857b9e17 + RCT-Folly: 7b4f73a92ad9571b9dbdb05bb30fad927fa971e1 RCTDeprecation: ebe712bb05077934b16c6bf25228bdec34b64f83 RCTRequired: ca91e5dd26b64f577b528044c962baf171c6b716 RCTTypeSafety: e7678bd60850ca5a41df9b8dc7154638cb66871f React: 4641770499c39f45d4e7cde1eba30e081f9d8a3d React-callinvoker: 4bef67b5c7f3f68db5929ab6a4d44b8a002998ea - React-Core: a68cea3e762814e60ecc3fa521c7f14c36c99245 - React-CoreModules: d81b1eaf8066add66299bab9d23c9f00c9484c7c - React-cxxreact: 984f8b1feeca37181d4e95301fcd6f5f6501c6ab + React-Codegen: 4b8b4817cea7a54b83851d4c1f91f79aa73de30a + React-Core: 0a06707a0b34982efc4a556aff5dae4b22863455 + React-CoreModules: 907334e94314189c2e5eed4877f3efe7b26d85b0 + React-cxxreact: 3a1d5e8f4faa5e09be26614e9c8bbcae8d11b73d React-debug: 817160c07dc8d24d020fbd1eac7b3558ffc08964 - React-defaultsnativemodule: 21f216e8db975897eb32b5f13247f5bbfaa97f41 - React-domnativemodule: 19270ad4b8d33312838d257f24731a0026809d49 - React-Fabric: f6dade7007533daeb785ba5925039d83f343be4b - React-FabricComponents: b0655cc3e1b5ae12a4a1119aa7d8308f0ad33520 - React-FabricImage: 9b157c4c01ac2bf433f834f0e1e5fe234113a576 + React-defaultsnativemodule: 814830ccbc3fb08d67d0190e63b179ee4098c67b + React-domnativemodule: 270acf94bd0960b026bc3bfb327e703665d27fb4 + React-Fabric: 64586dc191fc1c170372a638b8e722e4f1d0a09b + React-FabricComponents: b0ebd032387468ea700574c581b139f57a7497fb + React-FabricImage: 81f0e0794caf25ad1224fa406d288fbc1986607f React-featureflags: f2792b067a351d86fdc7bec23db3b9a2f2c8d26c - 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-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-native-crash-tester: 3ffaa64141427ca362079cb53559fe9a532487ae + react-native-safe-area-context: 511649fbcdb7b88d29660aa5c0936b3cd03c935d + react-native-webview: 7e0507e49529e26404dd5a37db31f2f8ad3d19b9 React-nativeconfig: 8efdb1ef1e9158c77098a93085438f7e7b463678 - React-NativeModulesApple: cebca2e5320a3d66e123cade23bd90a167ffce5e - React-perflogger: 72e653eb3aba9122f9e57cf012d22d2486f33358 - React-performancetimeline: cd6a9374a72001165995d2ab632f672df04076dc + React-NativeModulesApple: 958d4f6c5c2ace4c0f427cf7ef82e28ae6538a22 + React-perflogger: 9b4f13c0afe56bc7b4a0e93ec74b1150421ee22d + React-performancetimeline: 359db1cb889aa0282fafc5838331b0987c4915a9 React-RCTActionSheet: aacf2375084dea6e7c221f4a727e579f732ff342 - 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-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-rendererconsistency: b4917053ecbaa91469c67a4319701c9dc0d40be6 - React-rendererdebug: aa181c36dd6cf5b35511d1ed875d6638fd38f0ec + React-rendererdebug: 81becbc8852b38d9b1b68672aa504556481330d5 React-rncore: 120d21715c9b4ba8f798bffe986cb769b988dd74 - React-RuntimeApple: d033becbbd1eba6f9f6e3af6f1893030ce203edd - React-RuntimeCore: 38af280bb678e66ba000a3c3d42920b2a138eebb + React-RuntimeApple: 52ed0e9e84a7c2607a901149fb13599a3c057655 + React-RuntimeCore: ca6189d2e53d86db826e2673fe8af6571b8be157 React-runtimeexecutor: 877596f82f5632d073e121cba2d2084b76a76899 - React-RuntimeHermes: 37aad735ff21ca6de2d8450a96de1afe9f86c385 - React-runtimescheduler: 8ec34cc885281a34696ea16c4fd86892d631f38d + React-RuntimeHermes: 3b752dc5d8a1661c9d1687391d6d96acfa385549 + React-runtimescheduler: 8321bb09175ace2a4f0b3e3834637eb85bf42ebe React-timing: 331cbf9f2668c67faddfd2e46bb7f41cbd9320b9 - React-utils: ed818f19ab445000d6b5c4efa9d462449326cc9f - ReactCodegen: f853a20cc9125c5521c8766b4b49375fec20648b - ReactCommon: 300d8d9c5cb1a6cd79a67cf5d8f91e4d477195f9 - ReactNativeNavigation: 445f86273eb245d15b14023ee4ef9d6e4f891ad6 - RNCAsyncStorage: b44e8a4e798c3e1f56bffccd0f591f674fb9198f - RNGestureHandler: cb711d56ee3b03a5adea1d38324d4459ab55653f - RNScreens: f75b26fd4777848c216e27b0a09e1bf9c9f4760a + React-utils: 54df9ada708578c8ad40d92895d6fed03e0e8a9e + ReactCodegen: 21a52ccddc6479448fc91903a437dd23ddc7366c + ReactCommon: bfd3600989d79bc3acbe7704161b171a1480b9fd + ReactNativeNavigation: 3ddd319cc013c5e2b3bbe90b1f7fadaa5e8aa66f + RNCAsyncStorage: 8724c3be379a3d5a02ba83e276d79c2899f8d53c + RNGestureHandler: db5f279b66899cb96c8b9773c0eaac2117fe0e8a + RNScreens: 638e0b7f980df30dbb57e18d42c3b07b9fb4b92e SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 Yoga: feb4910aba9742cfedc059e2b2902e22ffe9954a -PODFILE CHECKSUM: 2be76f6ff2a88869ff51bdbf48edb79d7d863c79 +PODFILE CHECKSUM: 9b10c7cbb4e8f376b26065bb47f577130e22bc52 COCOAPODS: 1.16.2 diff --git a/packages/core/DatadogSDKReactNative.podspec b/packages/core/DatadogSDKReactNative.podspec index 2a65176b0..ccdda06c4 100644 --- a/packages/core/DatadogSDKReactNative.podspec +++ b/packages/core/DatadogSDKReactNative.podspec @@ -19,15 +19,15 @@ Pod::Spec.new do |s| s.dependency "React-Core" # /!\ Remember to keep the versions in sync with DatadogSDKReactNativeSessionReplay.podspec - s.dependency 'DatadogCore', '3.2.0' - s.dependency 'DatadogLogs', '3.2.0' - s.dependency 'DatadogTrace', '3.2.0' - s.dependency 'DatadogRUM', '3.2.0' - s.dependency 'DatadogCrashReporting', '3.2.0' - s.dependency 'DatadogFlags', '3.2.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' + s.dependency 'DatadogFlags', '3.1.0' # DatadogWebViewTracking is not available for tvOS - s.ios.dependency 'DatadogWebViewTracking', '3.2.0' + s.ios.dependency 'DatadogWebViewTracking', '3.1.0' s.test_spec 'Tests' do |test_spec| test_spec.source_files = 'ios/Tests/**/*.{swift,json}' From 91d3ffb1069710785923e78a5491a7ea425549d5 Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Mon, 17 Nov 2025 20:40:25 +0200 Subject: [PATCH 067/526] Revert an auto-formatter change --- .../core/ios/Sources/DdSdkConfiguration.swift | 69 ++++++++++--------- 1 file changed, 36 insertions(+), 33 deletions(-) diff --git a/packages/core/ios/Sources/DdSdkConfiguration.swift b/packages/core/ios/Sources/DdSdkConfiguration.swift index 84d107a78..8f852834e 100644 --- a/packages/core/ios/Sources/DdSdkConfiguration.swift +++ b/packages/core/ios/Sources/DdSdkConfiguration.swift @@ -4,44 +4,47 @@ * Copyright 2016-Present Datadog, Inc. */ +import Foundation import DatadogCore import DatadogFlags import DatadogInternal import DatadogRUM -import Foundation -/// A configuration object to initialize Datadog's features. -/// - Parameters: -/// - clientToken: A valid Datadog client token. -/// - env: The application’s environment, for example: prod, pre-prod, staging, etc. -/// - applicationId: The RUM application ID. -/// - nativeCrashReportEnabled: Whether the SDK should track native (pure iOS or pure Android) crashes (default is false). -/// - nativeLongTaskThresholdMs: The threshold for native long tasks reporting in milliseconds. -/// - longTaskThresholdMs: The threshold for javascript long tasks reporting in milliseconds. -/// - sampleRate: The sample rate (between 0 and 100) of RUM sessions kept. -/// - site: The Datadog site of your organization (can be 'US1', 'US1_FED', 'US3', 'US5', or 'EU1', default is 'US1'). -/// - trackingConsent: Consent, which can take one of the following values: 'pending', 'granted', 'not_granted'. -/// - telemetrySampleRate: The sample rate (between 0 and 100) of telemetry events. -/// - vitalsUpdateFrequency: The frequency at which to measure vitals performance metrics. -/// - uploadFrequency: The frequency at which batches of data are sent. -/// - batchSize: The preferred size of batched data uploaded to Datadog. -/// - trackFrustrations: Whether to track frustration signals or not. -/// - trackBackgroundEvents: Enables/Disables tracking RUM event when no RUM View is active. Might increase number of sessions and billing. -/// - customEndpoints: Custom endpoints for RUM/Logs/Trace features. -/// - additionalConfig: Additional configuration parameters. -/// - configurationForTelemetry: Additional configuration paramters only used for telemetry purposes. -/// - nativeViewTracking: Enables/Disables tracking RUM Views on the native level. -/// - nativeInteractionTracking: Enables/Disables tracking RUM Actions on the native level. -/// - verbosity: Verbosity level of the SDK. -/// - proxyConfig: Configuration for proxying SDK data. -/// - serviceName: Custom service name. -/// - firstPartyHosts: List of backend hosts to enable tracing with. -/// - bundleLogsWithRum: Correlates logs with RUM. -/// - bundleLogsWithTraces: Correlates logs with traces. -/// - appHangThreshold: The threshold for non-fatal app hangs reporting in seconds. -/// - trackWatchdogTerminations: Whether the SDK should track application termination by the watchdog -/// - batchProcessingLevel: Maximum number of batches processed sequentially without a delay -/// - initialResourceThreshold: The amount of time after a view starts where a Resource should be considered when calculating Time to Network-Settled (TNS) +/** + A configuration object to initialize Datadog's features. + - Parameters: + - clientToken: A valid Datadog client token. + - env: The application’s environment, for example: prod, pre-prod, staging, etc. + - applicationId: The RUM application ID. + - nativeCrashReportEnabled: Whether the SDK should track native (pure iOS or pure Android) crashes (default is false). + - nativeLongTaskThresholdMs: The threshold for native long tasks reporting in milliseconds. + - longTaskThresholdMs: The threshold for javascript long tasks reporting in milliseconds. + - sampleRate: The sample rate (between 0 and 100) of RUM sessions kept. + - site: The Datadog site of your organization (can be 'US1', 'US1_FED', 'US3', 'US5', or 'EU1', default is 'US1'). + - trackingConsent: Consent, which can take one of the following values: 'pending', 'granted', 'not_granted'. + - telemetrySampleRate: The sample rate (between 0 and 100) of telemetry events. + - vitalsUpdateFrequency: The frequency at which to measure vitals performance metrics. + - uploadFrequency: The frequency at which batches of data are sent. + - batchSize: The preferred size of batched data uploaded to Datadog. + - trackFrustrations: Whether to track frustration signals or not. + - trackBackgroundEvents: Enables/Disables tracking RUM event when no RUM View is active. Might increase number of sessions and billing. + - customEndpoints: Custom endpoints for RUM/Logs/Trace features. + - additionalConfig: Additional configuration parameters. + - configurationForTelemetry: Additional configuration paramters only used for telemetry purposes. + - nativeViewTracking: Enables/Disables tracking RUM Views on the native level. + - nativeInteractionTracking: Enables/Disables tracking RUM Actions on the native level. + - verbosity: Verbosity level of the SDK. + - proxyConfig: Configuration for proxying SDK data. + - serviceName: Custom service name. + - firstPartyHosts: List of backend hosts to enable tracing with. + - bundleLogsWithRum: Correlates logs with RUM. + - bundleLogsWithTraces: Correlates logs with traces. + - appHangThreshold: The threshold for non-fatal app hangs reporting in seconds. + - trackWatchdogTerminations: Whether the SDK should track application termination by the watchdog + - batchProcessingLevel: Maximum number of batches processed sequentially without a delay + - initialResourceThreshold: The amount of time after a view starts where a Resource should be considered when calculating Time to Network-Settled (TNS) + - configurationForFlags: Configuration for the feature flags feature. + */ @objc(DdSdkConfiguration) public class DdSdkConfiguration: NSObject { public var clientToken: String = "" From f351c697d6d8c61d44c08d4ec9b18f89242b92c0 Mon Sep 17 00:00:00 2001 From: Sergio Barrio Date: Tue, 18 Nov 2025 09:57:58 +0100 Subject: [PATCH 068/526] 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 7d91c9ca4f8b28f8a488cedee6209f667bba1bf1 Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Tue, 18 Nov 2025 17:53:29 +0200 Subject: [PATCH 069/526] Add flagging SDK demo to the old arch example app, update the new arch app demo --- example-new-architecture/App.tsx | 49 +--- example/ios/Podfile.lock | 265 ++---------------- example/src/WixApp.tsx | 21 +- example/src/ddUtils.tsx | 16 +- example/src/screens/MainScreen.tsx | 23 +- packages/core/src/DdSdkReactNative.tsx | 5 + .../src/DdSdkReactNativeConfiguration.tsx | 1 + packages/core/src/index.tsx | 4 +- 8 files changed, 94 insertions(+), 290 deletions(-) diff --git a/example-new-architecture/App.tsx b/example-new-architecture/App.tsx index 454026708..f6aecaac5 100644 --- a/example-new-architecture/App.tsx +++ b/example-new-architecture/App.tsx @@ -92,41 +92,22 @@ function Section({children, title}: SectionProps): React.JSX.Element { } function App(): React.JSX.Element { - const [flagValues, setFlagValues] = React.useState>({}); - + const [testFlagValue, setTestFlagValue] = React.useState(false); React.useEffect(() => { - (async () => { - const flagsClient = DatadogFlags.getClient(); - - // Set flag evaluation context. - await flagsClient.setEvaluationContext({ - targetingKey: 'test-user-1', - attributes: { - country: 'US', - }, - }); - - const [booleanValue, stringValue, jsonValue, integerValue, numberValue] = await Promise.all([ - flagsClient.getBooleanDetails('rn-sdk-test-boolean-flag', false), // https://app.datadoghq.com/feature-flags/046d0e70-626d-41e1-8314-3f009fb79b7a?environmentId=d114cd9a-79ed-4c56-bcf3-bcac9293653b - flagsClient.getStringDetails('rn-sdk-test-string-flag', 'default-value'), // https://app.datadoghq.com/feature-flags/80756d8f-a375-437a-a023-b490c91cd506?environmentId=d114cd9a-79ed-4c56-bcf3-bcac9293653b - flagsClient.getObjectDetails('rn-sdk-test-json-flag', {default: 'value'}), // https://app.datadoghq.com/feature-flags/bcf75cd6-96d8-4182-8871-0b66ad76127a?environmentId=d114cd9a-79ed-4c56-bcf3-bcac9293653b - flagsClient.getNumberDetails('rn-sdk-test-integer-flag', 0), // https://app.datadoghq.com/feature-flags/5cd5a154-65ef-4c15-b539-e68c93eaa7f1?environmentId=d114cd9a-79ed-4c56-bcf3-bcac9293653b - flagsClient.getNumberDetails('rn-sdk-test-number-flag', 0.7), // https://app.datadoghq.com/feature-flags/62b3129a-f9fa-49c0-b8a2-1a772b183bf7?environmentId=d114cd9a-79ed-4c56-bcf3-bcac9293653b - ]); - - const newValues = { - boolean: booleanValue, - json: jsonValue, - integer: integerValue, - string: stringValue, - number: numberValue, - }; - - setFlagValues(newValues); - })().catch(error => console.error(error.message)); + (async () => { + const flagsClient = DatadogFlags.getClient(); + await flagsClient.setEvaluationContext({ + targetingKey: 'test-user-1', + attributes: { + country: 'US', + }, + }); + const flag = await flagsClient.getBooleanDetails('rn-sdk-test-boolean-flag', false); // https://app.datadoghq.com/feature-flags/046d0e70-626d-41e1-8314-3f009fb79b7a?environmentId=d114cd9a-79ed-4c56-bcf3-bcac9293653b + console.log({flag}) + setTestFlagValue(flag.value); + })(); }, []); - const isDarkMode = useColorScheme() === 'dark'; const backgroundStyle = { @@ -143,13 +124,11 @@ function App(): React.JSX.Element { contentInsetAdjustmentBehavior="automatic" style={backgroundStyle}>
+ rn-sdk-test-boolean-flag: {String(testFlagValue)} - - {JSON.stringify(flagValues, (key, value) => value === undefined ? '' : value, 2)} -
Edit App.tsx to change this screen and then come back to see your edits. diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index bac047afc..fb21484ba 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -20,26 +20,7 @@ PODS: - DatadogRUM (= 3.1.0) - DatadogTrace (= 3.1.0) - DatadogWebViewTracking (= 3.1.0) - - DoubleConversion - - glog - - hermes-engine - - RCT-Folly (= 2024.10.14.00) - - RCTRequired - - RCTTypeSafety - React-Core - - React-debug - - React-Fabric - - React-featureflags - - React-graphics - - React-ImageManager - - React-NativeModulesApple - - React-RCTFabric - - React-rendererdebug - - React-utils - - ReactCodegen - - ReactCommon/turbomodule/bridging - - ReactCommon/turbomodule/core - - Yoga - DatadogSDKReactNative/Tests (2.13.0): - DatadogCore (= 3.1.0) - DatadogCrashReporting (= 3.1.0) @@ -48,26 +29,7 @@ PODS: - DatadogRUM (= 3.1.0) - DatadogTrace (= 3.1.0) - DatadogWebViewTracking (= 3.1.0) - - DoubleConversion - - glog - - hermes-engine - - RCT-Folly (= 2024.10.14.00) - - RCTRequired - - RCTTypeSafety - React-Core - - React-debug - - React-Fabric - - React-featureflags - - React-graphics - - React-ImageManager - - React-NativeModulesApple - - React-RCTFabric - - React-rendererdebug - - React-utils - - ReactCodegen - - ReactCommon/turbomodule/bridging - - ReactCommon/turbomodule/core - - Yoga - DatadogSDKReactNativeSessionReplay (2.13.0): - DatadogSDKReactNative - DatadogSessionReplay (= 3.1.0) @@ -119,52 +81,14 @@ PODS: - DatadogInternal (= 3.1.0) - DatadogSDKReactNative - DatadogWebViewTracking (= 3.1.0) - - DoubleConversion - - glog - - hermes-engine - - RCT-Folly (= 2024.10.14.00) - - RCTRequired - - RCTTypeSafety - React-Core - - React-debug - - React-Fabric - - React-featureflags - - React-graphics - - React-ImageManager - - React-NativeModulesApple - - React-RCTFabric - - React-rendererdebug - - React-utils - - ReactCodegen - - ReactCommon/turbomodule/bridging - - ReactCommon/turbomodule/core - - Yoga - DatadogSDKReactNativeWebView/Tests (2.13.0): - DatadogInternal (= 3.1.0) - DatadogSDKReactNative - DatadogWebViewTracking (= 3.1.0) - - DoubleConversion - - glog - - hermes-engine - - RCT-Folly (= 2024.10.14.00) - - RCTRequired - - RCTTypeSafety - React-Core - - React-debug - - React-Fabric - - React-featureflags - - React-graphics - - React-ImageManager - react-native-webview - - React-NativeModulesApple - - React-RCTFabric - React-RCTText - - React-rendererdebug - - React-utils - - ReactCodegen - - ReactCommon/turbomodule/bridging - - ReactCommon/turbomodule/core - - Yoga - DatadogSessionReplay (3.1.0): - DatadogInternal (= 3.1.0) - DatadogTrace (3.1.0): @@ -222,7 +146,6 @@ PODS: - React-RCTText (= 0.76.9) - React-RCTVibration (= 0.76.9) - React-callinvoker (0.76.9) - - React-Codegen (0.1.0) - React-Core (0.76.9): - glog - hermes-engine @@ -1465,71 +1388,7 @@ PODS: - react-native-crash-tester (0.2.3): - React-Core - react-native-safe-area-context (5.1.0): - - DoubleConversion - - glog - - hermes-engine - - RCT-Folly (= 2024.10.14.00) - - RCTRequired - - RCTTypeSafety - React-Core - - React-debug - - React-Fabric - - React-featureflags - - React-graphics - - React-ImageManager - - react-native-safe-area-context/common (= 5.1.0) - - react-native-safe-area-context/fabric (= 5.1.0) - - React-NativeModulesApple - - React-RCTFabric - - React-rendererdebug - - React-utils - - ReactCodegen - - ReactCommon/turbomodule/bridging - - ReactCommon/turbomodule/core - - Yoga - - react-native-safe-area-context/common (5.1.0): - - DoubleConversion - - glog - - hermes-engine - - RCT-Folly (= 2024.10.14.00) - - RCTRequired - - RCTTypeSafety - - React-Core - - React-debug - - React-Fabric - - React-featureflags - - React-graphics - - React-ImageManager - - React-NativeModulesApple - - React-RCTFabric - - React-rendererdebug - - React-utils - - ReactCodegen - - ReactCommon/turbomodule/bridging - - ReactCommon/turbomodule/core - - Yoga - - react-native-safe-area-context/fabric (5.1.0): - - DoubleConversion - - glog - - hermes-engine - - RCT-Folly (= 2024.10.14.00) - - RCTRequired - - RCTTypeSafety - - React-Core - - React-debug - - React-Fabric - - React-featureflags - - React-graphics - - React-ImageManager - - react-native-safe-area-context/common - - React-NativeModulesApple - - React-RCTFabric - - React-rendererdebug - - React-utils - - ReactCodegen - - ReactCommon/turbomodule/bridging - - ReactCommon/turbomodule/core - - Yoga - react-native-webview (13.14.2): - DoubleConversion - glog @@ -1824,87 +1683,20 @@ PODS: - React-perflogger - React-utils (= 0.76.9) - ReactNativeNavigation (8.0.0-snapshot.1658): - - DoubleConversion - - glog - - hermes-engine - HMSegmentedControl - - RCT-Folly - - RCTRequired - - RCTTypeSafety - - React - - React-Codegen - React-Core - React-CoreModules - - React-cxxreact - - React-debug - - React-Fabric - - React-featureflags - - React-graphics - - React-ImageManager - - React-NativeModulesApple - - React-RCTFabric - React-RCTImage - React-RCTText - - React-rendererdebug - - React-rncore - - React-runtimeexecutor - - React-utils - - ReactCodegen - - ReactCommon/turbomodule/bridging - - ReactCommon/turbomodule/core - ReactNativeNavigation/Core (= 8.0.0-snapshot.1658) - - Yoga - ReactNativeNavigation/Core (8.0.0-snapshot.1658): - - DoubleConversion - - glog - - hermes-engine - HMSegmentedControl - - RCT-Folly - - RCTRequired - - RCTTypeSafety - - React - - React-Codegen - React-Core - React-CoreModules - - React-cxxreact - - React-debug - - React-Fabric - - React-featureflags - - React-graphics - - React-ImageManager - - React-NativeModulesApple - - React-RCTFabric - React-RCTImage - React-RCTText - - React-rendererdebug - - React-rncore - - React-runtimeexecutor - - React-utils - - ReactCodegen - - ReactCommon/turbomodule/bridging - - ReactCommon/turbomodule/core - - Yoga - RNCAsyncStorage (2.2.0): - - DoubleConversion - - glog - - hermes-engine - - RCT-Folly (= 2024.10.14.00) - - RCTRequired - - RCTTypeSafety - React-Core - - React-debug - - React-Fabric - - React-featureflags - - React-graphics - - React-ImageManager - - React-NativeModulesApple - - React-RCTFabric - - React-rendererdebug - - React-utils - - ReactCodegen - - ReactCommon/turbomodule/bridging - - ReactCommon/turbomodule/core - - Yoga - RNGestureHandler (2.24.0): - DoubleConversion - glog @@ -1927,29 +1719,6 @@ PODS: - ReactCommon/turbomodule/core - Yoga - RNScreens (4.5.0): - - DoubleConversion - - glog - - hermes-engine - - RCT-Folly (= 2024.10.14.00) - - RCTRequired - - RCTTypeSafety - - React-Core - - React-debug - - React-Fabric - - React-featureflags - - React-graphics - - React-ImageManager - - React-NativeModulesApple - - React-RCTFabric - - React-RCTImage - - React-rendererdebug - - React-utils - - ReactCodegen - - ReactCommon/turbomodule/bridging - - ReactCommon/turbomodule/core - - RNScreens/common (= 4.5.0) - - Yoga - - RNScreens/common (4.5.0): - DoubleConversion - glog - hermes-engine @@ -2069,7 +1838,6 @@ SPEC REPOS: - HMSegmentedControl - OpenTelemetrySwiftApi - PLCrashReporter - - React-Codegen - SocketRocket EXTERNAL SOURCES: @@ -2281,9 +2049,9 @@ SPEC CHECKSUMS: DatadogInternal: 7837b2ce3d525d429682532eeda697b181299fdc DatadogLogs: 250894b5a99da5b924a019049c0d0326823cdbd6 DatadogRUM: 0d2a60e1abb8aacfb8827ef84f6d5deb4d5026c8 - DatadogSDKReactNative: e74b171da3b103bf9b2fd372f480fa71c230830d - DatadogSDKReactNativeSessionReplay: fcd758a85e16ef29e0a56c56bcee0d4895fc0c64 - DatadogSDKReactNativeWebView: b4aefc87771441e1598254a2f890b8877613656e + DatadogSDKReactNative: 4ba420fb772ed237ca2098f2a78ad6a459ce34eb + DatadogSDKReactNativeSessionReplay: 72bf7b80599e2752bff13b622b04fe6605aa1d5e + DatadogSDKReactNativeWebView: 5a7f23efb34f1fa9421dba531499193f8949495d DatadogSessionReplay: 6bc71888e2b41dd0de3325f06f0c0b3cee0e6df4 DatadogTrace: f59e933074cd285ad7e9f5af991f8fe04b095991 DatadogWebViewTracking: 9bc92b4147aeed47eb1911451f651094aa6dd6c1 @@ -2302,21 +2070,20 @@ SPEC CHECKSUMS: RCTTypeSafety: e7678bd60850ca5a41df9b8dc7154638cb66871f React: 4641770499c39f45d4e7cde1eba30e081f9d8a3d React-callinvoker: 4bef67b5c7f3f68db5929ab6a4d44b8a002998ea - React-Codegen: 4b8b4817cea7a54b83851d4c1f91f79aa73de30a React-Core: 0a06707a0b34982efc4a556aff5dae4b22863455 React-CoreModules: 907334e94314189c2e5eed4877f3efe7b26d85b0 React-cxxreact: 3a1d5e8f4faa5e09be26614e9c8bbcae8d11b73d React-debug: 817160c07dc8d24d020fbd1eac7b3558ffc08964 - React-defaultsnativemodule: 814830ccbc3fb08d67d0190e63b179ee4098c67b - React-domnativemodule: 270acf94bd0960b026bc3bfb327e703665d27fb4 + React-defaultsnativemodule: a965cb39fb0a79276ab611793d39f52e59a9a851 + React-domnativemodule: d647f94e503c62c44f54291334b1aa22a30fa08b React-Fabric: 64586dc191fc1c170372a638b8e722e4f1d0a09b React-FabricComponents: b0ebd032387468ea700574c581b139f57a7497fb React-FabricImage: 81f0e0794caf25ad1224fa406d288fbc1986607f React-featureflags: f2792b067a351d86fdc7bec23db3b9a2f2c8d26c - React-featureflagsnativemodule: 0d7091ae344d6160c0557048e127897654a5c00f + React-featureflagsnativemodule: 95a02d895475de8ace78fedd76143866838bb720 React-graphics: cbebe910e4a15b65b0bff94a4d3ed278894d6386 React-hermes: ec18c10f5a69d49fb9b5e17ae95494e9ea13d4d3 - React-idlecallbacksnativemodule: 6b84add48971da9c40403bd1860d4896462590f2 + React-idlecallbacksnativemodule: 0c1ae840cc5587197cd926a3cb76828ad059d116 React-ImageManager: f2a4c01c2ccb2193e60a20c135da74c7ca4d36f2 React-jserrorhandler: 61d205b5a7cbc57fed3371dd7eed48c97f49fc64 React-jsi: 95f7676103137861b79b0f319467627bcfa629ee @@ -2325,19 +2092,19 @@ SPEC CHECKSUMS: React-jsitracing: 3758cdb155ea7711f0e77952572ea62d90c69f0b React-logger: dbca7bdfd4aa5ef69431362bde6b36d49403cb20 React-Mapbuffer: 6efad4a606c1fae7e4a93385ee096681ef0300dc - React-microtasksnativemodule: a645237a841d733861c70b69908ab4a1707b52ad + React-microtasksnativemodule: 8732b71aa66045da4bb341ddee1bb539f71e5f38 react-native-crash-tester: 3ffaa64141427ca362079cb53559fe9a532487ae - react-native-safe-area-context: 511649fbcdb7b88d29660aa5c0936b3cd03c935d - react-native-webview: 7e0507e49529e26404dd5a37db31f2f8ad3d19b9 + react-native-safe-area-context: 04803a01f39f31cc6605a5531280b477b48f8a88 + react-native-webview: 1e12de2fad74c17b4f8b1b53ebd1e3baa0148d71 React-nativeconfig: 8efdb1ef1e9158c77098a93085438f7e7b463678 React-NativeModulesApple: 958d4f6c5c2ace4c0f427cf7ef82e28ae6538a22 React-perflogger: 9b4f13c0afe56bc7b4a0e93ec74b1150421ee22d React-performancetimeline: 359db1cb889aa0282fafc5838331b0987c4915a9 React-RCTActionSheet: aacf2375084dea6e7c221f4a727e579f732ff342 React-RCTAnimation: d8c82deebebe3aaf7a843affac1b57cb2dc073d4 - React-RCTAppDelegate: 1774aa421a29a41a704ecaf789811ef73c4634b6 + React-RCTAppDelegate: 6c0377d9c4058773ea7073bb34bb9ebd6ddf5a84 React-RCTBlob: 70a58c11a6a3500d1a12f2e51ca4f6c99babcff8 - React-RCTFabric: 731cda82aed592aacce2d32ead69d78cde5d9274 + React-RCTFabric: 7eb6dd2c8fda98cb860a572e3f4e4eb60d62c89e React-RCTImage: 5e9d655ba6a790c31e3176016f9b47fd0978fbf0 React-RCTLinking: 2a48338252805091f7521eaf92687206401bdf2a React-RCTNetwork: 0c1282b377257f6b1c81934f72d8a1d0c010e4c3 @@ -2356,10 +2123,10 @@ SPEC CHECKSUMS: React-utils: 54df9ada708578c8ad40d92895d6fed03e0e8a9e ReactCodegen: 21a52ccddc6479448fc91903a437dd23ddc7366c ReactCommon: bfd3600989d79bc3acbe7704161b171a1480b9fd - ReactNativeNavigation: 3ddd319cc013c5e2b3bbe90b1f7fadaa5e8aa66f - RNCAsyncStorage: 8724c3be379a3d5a02ba83e276d79c2899f8d53c - RNGestureHandler: db5f279b66899cb96c8b9773c0eaac2117fe0e8a - RNScreens: 638e0b7f980df30dbb57e18d42c3b07b9fb4b92e + ReactNativeNavigation: 50c1eef68b821e7265eff3a391d27ed18fdce459 + RNCAsyncStorage: 23e56519cc41d3bade3c8d4479f7760cb1c11996 + RNGestureHandler: 950dfa674dbf481460ca389c65b9036ac4ab8ada + RNScreens: 606ab1cf68162f7ba0d049a31f2a84089a6fffb4 SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 Yoga: feb4910aba9742cfedc059e2b2902e22ffe9954a diff --git a/example/src/WixApp.tsx b/example/src/WixApp.tsx index 55e93f6e4..dc6ed2dc5 100644 --- a/example/src/WixApp.tsx +++ b/example/src/WixApp.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { View, Text, Button } from 'react-native'; import MainScreen from './screens/MainScreen'; import ErrorScreen from './screens/ErrorScreen'; @@ -11,7 +11,7 @@ import { } from '@datadog/mobile-react-native-navigation'; import styles from './screens/styles'; -import { DdTrace } from '@datadog/mobile-react-native'; +import { DatadogFlags } from '@datadog/mobile-react-native'; import TraceScreen from './screens/TraceScreen'; const viewPredicate: ViewNamePredicate = ( @@ -44,6 +44,22 @@ function registerScreens() { } const HomeScreen = props => { + const [testFlagValue, setTestFlagValue] = useState(false); + useEffect(() => { + (async () => { + const flagsClient = DatadogFlags.getClient(); + await flagsClient.setEvaluationContext({ + targetingKey: 'test-user-1', + attributes: { + country: 'US', + }, + }); + const flag = await flagsClient.getBooleanDetails('rn-sdk-test-boolean-flag', false); // https://app.datadoghq.com/feature-flags/046d0e70-626d-41e1-8314-3f009fb79b7a?environmentId=d114cd9a-79ed-4c56-bcf3-bcac9293653b + console.log({flag}) + setTestFlagValue(flag.value); + })(); + }, []); + return ( @@ -84,6 +100,7 @@ const HomeScreen = props => { }); }} /> + rn-sdk-test-boolean-flag: {String(testFlagValue)} ); }; diff --git a/example/src/ddUtils.tsx b/example/src/ddUtils.tsx index db0f1b4e3..d10144127 100644 --- a/example/src/ddUtils.tsx +++ b/example/src/ddUtils.tsx @@ -4,7 +4,9 @@ import { DdSdkReactNative, DdSdkReactNativeConfiguration, SdkVerbosity, - TrackingConsent + TrackingConsent, + DatadogFlags, + type DatadogFlagsConfiguration, } from '@datadog/mobile-react-native'; import {APPLICATION_ID, CLIENT_TOKEN, ENVIRONMENT} from './ddCredentials'; @@ -25,6 +27,11 @@ export function getDatadogConfig(trackingConsent: TrackingConsent) { config.serviceName = "com.datadoghq.reactnative.sample" config.verbosity = SdkVerbosity.DEBUG; + const flagsConfiguration: DatadogFlagsConfiguration = { + enabled: true, + } + config.flagsConfiguration = flagsConfiguration + return config } @@ -51,9 +58,16 @@ export function initializeDatadog(trackingConsent: TrackingConsent) { config.serviceName = "com.datadoghq.reactnative.sample" config.verbosity = SdkVerbosity.DEBUG; + const flagsConfiguration: DatadogFlagsConfiguration = { + enabled: true, + } + config.flagsConfiguration = flagsConfiguration + DdSdkReactNative.initialize(config).then(() => { DdLogs.info('The RN Sdk was properly initialized') DdSdkReactNative.setUserInfo({id: "1337", name: "Xavier", email: "xg@example.com", extraInfo: { type: "premium" } }) DdSdkReactNative.addAttributes({campaign: "ad-network"}) }); + + DatadogFlags.enable(flagsConfiguration) } diff --git a/example/src/screens/MainScreen.tsx b/example/src/screens/MainScreen.tsx index 7f0fbdec2..5ac7cddb8 100644 --- a/example/src/screens/MainScreen.tsx +++ b/example/src/screens/MainScreen.tsx @@ -11,7 +11,7 @@ import { } from 'react-native'; import styles from './styles'; import { APPLICATION_KEY, API_KEY } from '../../src/ddCredentials'; -import { DdLogs, DdSdkReactNative, TrackingConsent } from '@datadog/mobile-react-native'; +import { DdLogs, DdSdkReactNative, TrackingConsent, DatadogFlags } from '@datadog/mobile-react-native'; import { getTrackingConsent, saveTrackingConsent } from '../utils'; import { ConsentModal } from '../components/consent'; import { DdRum } from '../../../packages/core/src/rum/DdRum'; @@ -27,6 +27,7 @@ interface MainScreenState { resultTouchableNativeFeedback: string, trackingConsent: TrackingConsent, trackingConsentModalVisible: boolean + testFlagValue: boolean } export default class MainScreen extends Component { @@ -40,7 +41,8 @@ export default class MainScreen extends Component { resultButtonAction: "", resultTouchableOpacityAction: "", trackingConsent: TrackingConsent.PENDING, - trackingConsentModalVisible: false + trackingConsentModalVisible: false, + testFlagValue: false } as MainScreenState; this.consentModal = React.createRef() } @@ -94,6 +96,7 @@ export default class MainScreen extends Component { componentDidMount() { this.updateTrackingConsent() + this.fetchBooleanFlag(); DdLogs.debug("[DATADOG SDK] Test React Native Debug Log"); } @@ -105,6 +108,21 @@ export default class MainScreen extends Component { }) } + fetchBooleanFlag() { + (async () => { + const flagsClient = DatadogFlags.getClient(); + await flagsClient.setEvaluationContext({ + targetingKey: 'test-user-1', + attributes: { + country: 'US', + }, + }); + const flag = await flagsClient.getBooleanDetails('rn-sdk-test-boolean-flag', false); // https://app.datadoghq.com/feature-flags/046d0e70-626d-41e1-8314-3f009fb79b7a?environmentId=d114cd9a-79ed-4c56-bcf3-bcac9293653b + console.log({flag}) + this.setState({ testFlagValue: flag.value }) + })(); + } + setTrackingConsentModalVisible(visible: boolean) { if (visible) { this.consentModal.current.setConsent(this.state.trackingConsent) @@ -205,6 +223,7 @@ export default class MainScreen extends Component { Click me (error) + rn-sdk-test-boolean-flag: {String(this.state.testFlagValue)} } diff --git a/packages/core/src/DdSdkReactNative.tsx b/packages/core/src/DdSdkReactNative.tsx index b7305d67a..5da26b249 100644 --- a/packages/core/src/DdSdkReactNative.tsx +++ b/packages/core/src/DdSdkReactNative.tsx @@ -24,6 +24,7 @@ import { import { InternalLog } from './InternalLog'; import { SdkVerbosity } from './SdkVerbosity'; import type { TrackingConsent } from './TrackingConsent'; +import { DatadogFlags } from './flags/DatadogFlags'; import { DdLogs } from './logs/DdLogs'; import { DdRum } from './rum/DdRum'; import { DdRumErrorTracking } from './rum/instrumentation/DdRumErrorTracking'; @@ -472,6 +473,10 @@ export class DdSdkReactNative { DdRum.registerActionEventMapper(configuration.actionEventMapper); } + if (configuration.flagsConfiguration) { + DatadogFlags.enable(configuration.flagsConfiguration); + } + DdSdkReactNative.wasAutoInstrumented = true; } } diff --git a/packages/core/src/DdSdkReactNativeConfiguration.tsx b/packages/core/src/DdSdkReactNativeConfiguration.tsx index 7a6f88665..5df2ff8d9 100644 --- a/packages/core/src/DdSdkReactNativeConfiguration.tsx +++ b/packages/core/src/DdSdkReactNativeConfiguration.tsx @@ -417,6 +417,7 @@ export type AutoInstrumentationParameters = { readonly actionEventMapper: ActionEventMapper | null; readonly useAccessibilityLabel: boolean; readonly actionNameAttribute?: string; + readonly flagsConfiguration?: DatadogFlagsConfiguration; }; /** diff --git a/packages/core/src/index.tsx b/packages/core/src/index.tsx index b95c2c10a..fcab971ce 100644 --- a/packages/core/src/index.tsx +++ b/packages/core/src/index.tsx @@ -22,6 +22,7 @@ import { ProxyConfiguration, ProxyType } from './ProxyConfiguration'; import { SdkVerbosity } from './SdkVerbosity'; import { TrackingConsent } from './TrackingConsent'; import { DatadogFlags } from './flags/DatadogFlags'; +import type { DatadogFlagsConfiguration } from './flags/types'; import { DdLogs } from './logs/DdLogs'; import { DdRum } from './rum/DdRum'; import { DdBabelInteractionTracking } from './rum/instrumentation/interactionTracking/DdBabelInteractionTracking'; @@ -88,5 +89,6 @@ export type { Timestamp, FirstPartyHost, AutoInstrumentationConfiguration, - PartialInitializationConfiguration + PartialInitializationConfiguration, + DatadogFlagsConfiguration }; From f2fd0d8a4735344bf32f8d7ea7f2126ee92e9858 Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Wed, 19 Nov 2025 13:11:17 +0200 Subject: [PATCH 070/526] Fix failing tests --- packages/core/ios/Tests/DdSdkTests.swift | 6 ++++-- .../sdk/DatadogProvider/__tests__/initialization.test.tsx | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/core/ios/Tests/DdSdkTests.swift b/packages/core/ios/Tests/DdSdkTests.swift index 4a5d13f2e..c64f1acf7 100644 --- a/packages/core/ios/Tests/DdSdkTests.swift +++ b/packages/core/ios/Tests/DdSdkTests.swift @@ -1634,7 +1634,8 @@ extension DdSdkConfiguration { appHangThreshold: Double? = nil, trackWatchdogTerminations: Bool = false, batchProcessingLevel: NSString? = "MEDIUM", - initialResourceThreshold: Double? = nil + initialResourceThreshold: Double? = nil, + configurationForFlags: NSDictionary? = nil ) -> DdSdkConfiguration { DdSdkConfiguration( clientToken: clientToken as String, @@ -1667,7 +1668,8 @@ extension DdSdkConfiguration { appHangThreshold: appHangThreshold, trackWatchdogTerminations: trackWatchdogTerminations, batchProcessingLevel: batchProcessingLevel.asBatchProcessingLevel(), - initialResourceThreshold: initialResourceThreshold + initialResourceThreshold: initialResourceThreshold, + configurationForFlags: configurationForFlags?.asConfigurationForFlags() ) } } diff --git a/packages/core/src/sdk/DatadogProvider/__tests__/initialization.test.tsx b/packages/core/src/sdk/DatadogProvider/__tests__/initialization.test.tsx index 8ace4b731..e896a9bc2 100644 --- a/packages/core/src/sdk/DatadogProvider/__tests__/initialization.test.tsx +++ b/packages/core/src/sdk/DatadogProvider/__tests__/initialization.test.tsx @@ -75,6 +75,7 @@ describe('DatadogProvider', () => { "bundleLogsWithRum": true, "bundleLogsWithTraces": true, "clientToken": "fakeToken", + "configurationForFlags": undefined, "configurationForTelemetry": { "initializationType": "SYNC", "reactNativeVersion": "0.76.9", From 2bedb0d2a87b714f7d27228f706d3e6360c2f811 Mon Sep 17 00:00:00 2001 From: Carlos Nogueira Date: Mon, 3 Nov 2025 18:05:04 +0000 Subject: [PATCH 071/526] 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 072/526] 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 073/526] 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 074/526] 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 daff8ea2054101328e3afb0127ce560e12277449 Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Mon, 24 Nov 2025 22:07:44 +0200 Subject: [PATCH 075/526] Add tests for the flagging functionality --- packages/core/__mocks__/react-native.ts | 40 ++ .../ios/Sources/DdFlagsImplementation.swift | 17 +- .../ios/Sources/RNDdSdkConfiguration.swift | 2 +- packages/core/ios/Tests/DdFlagsTests.swift | 347 ++++++++++++++++++ packages/core/ios/Tests/DdSdkTests.swift | 30 ++ .../src/flags/__tests__/DatadogFlags.test.ts | 54 +++ .../src/flags/__tests__/FlagsClient.test.ts | 197 ++++++++++ 7 files changed, 684 insertions(+), 3 deletions(-) create mode 100644 packages/core/ios/Tests/DdFlagsTests.swift create mode 100644 packages/core/src/flags/__tests__/DatadogFlags.test.ts create mode 100644 packages/core/src/flags/__tests__/FlagsClient.test.ts diff --git a/packages/core/__mocks__/react-native.ts b/packages/core/__mocks__/react-native.ts index 73308f711..8e8b22d78 100644 --- a/packages/core/__mocks__/react-native.ts +++ b/packages/core/__mocks__/react-native.ts @@ -158,4 +158,44 @@ actualRN.NativeModules.DdRum = { ) as jest.MockedFunction }; +actualRN.NativeModules.DdFlags = { + setEvaluationContext: jest.fn().mockImplementation(() => Promise.resolve()), + getBooleanDetails: jest.fn().mockImplementation(() => + Promise.resolve({ + key: 'test-boolean-flag', + value: true, + variant: 'true', + reason: 'STATIC', + error: null + }) + ), + getStringDetails: jest.fn().mockImplementation(() => + Promise.resolve({ + key: 'test-string-flag', + value: 'hello world', + variant: 'hello world', + reason: 'STATIC', + error: null + }) + ), + getNumberDetails: jest.fn().mockImplementation(() => + Promise.resolve({ + key: 'test-number-flag', + value: 6, + variant: '6', + reason: 'STATIC', + error: null + }) + ), + getObjectDetails: jest.fn().mockImplementation(() => + Promise.resolve({ + key: 'test-object-flag', + value: { hello: 'world' }, + variant: 'hello world', + reason: 'STATIC', + error: null + }) + ) +}; + module.exports = actualRN; diff --git a/packages/core/ios/Sources/DdFlagsImplementation.swift b/packages/core/ios/Sources/DdFlagsImplementation.swift index f0eab15a3..7b128db17 100644 --- a/packages/core/ios/Sources/DdFlagsImplementation.swift +++ b/packages/core/ios/Sources/DdFlagsImplementation.swift @@ -10,8 +10,21 @@ import DatadogFlags @objc public class DdFlagsImplementation: NSObject { + private let core: DatadogCoreProtocol + private var clientProviders: [String: () -> FlagsClientProtocol] = [:] + internal init( + core: DatadogCoreProtocol + ) { + self.core = core + } + + @objc + public override convenience init() { + self.init(core: CoreRegistry.default) + } + /// Retrieve a `FlagsClient` instance in a non-interruptive way for usage in methods bridged to React Native. /// /// We create a simple registry of client providers by client name holding closures for retrieving a client since client references are kept internally in the flagging SDK. @@ -24,8 +37,8 @@ public class DdFlagsImplementation: NSObject { return provider() } - let client = FlagsClient.create(name: name) - clientProviders[name] = { FlagsClient.shared(named: name) } + let client = FlagsClient.create(name: name, in: self.core) + clientProviders[name] = { FlagsClient.shared(named: name, in: self.core) } return client } diff --git a/packages/core/ios/Sources/RNDdSdkConfiguration.swift b/packages/core/ios/Sources/RNDdSdkConfiguration.swift index ff0a46807..464161c3f 100644 --- a/packages/core/ios/Sources/RNDdSdkConfiguration.swift +++ b/packages/core/ios/Sources/RNDdSdkConfiguration.swift @@ -103,7 +103,7 @@ extension NSDictionary { } func asConfigurationForFlags() -> Flags.Configuration? { - let enabled = object(forKey: "enabled") as! Bool + let enabled = object(forKey: "enabled") as? Bool ?? false if !enabled { return nil diff --git a/packages/core/ios/Tests/DdFlagsTests.swift b/packages/core/ios/Tests/DdFlagsTests.swift new file mode 100644 index 000000000..7402d4d23 --- /dev/null +++ b/packages/core/ios/Tests/DdFlagsTests.swift @@ -0,0 +1,347 @@ +/* + * 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 XCTest +import DatadogCore +import DatadogFlags +import DatadogInternal +@testable import DatadogSDKReactNative + +class DdFlagsTests: XCTestCase { + + private var core: FlagsTestCore! + + override func setUp() { + super.setUp() + // MockDatadogCore doesn't work here because it returns `nil` in `feature` method. + core = FlagsTestCore() + CoreRegistry.register(default: core) + Flags.enable(in: core) + } + + override func tearDown() { + CoreRegistry.unregisterDefault() + super.tearDown() + } + + // MARK: - AnyValue Tests + + func testAnyValueWrapUnwrapNull() { + let original: Any = NSNull() + let wrapped = AnyValue.wrap(original) + + if case .null = wrapped { + XCTAssertTrue(true) + } else { + XCTFail("Expected .null, got \(wrapped)") + } + + let unwrapped = wrapped.unwrap() + XCTAssertTrue(unwrapped is NSNull) + } + + func testAnyValueWrapUnwrapString() { + let original = "test string" + let wrapped = AnyValue.wrap(original) + + if case .string(let value) = wrapped { + XCTAssertEqual(value, original) + } else { + XCTFail("Expected .string, got \(wrapped)") + } + + let unwrapped = wrapped.unwrap() as? String + XCTAssertEqual(unwrapped, original) + } + + func testAnyValueWrapUnwrapBool() { + let original = true + let wrapped = AnyValue.wrap(original) + + if case .bool(let value) = wrapped { + XCTAssertEqual(value, original) + } else { + XCTFail("Expected .bool, got \(wrapped)") + } + + let unwrapped = wrapped.unwrap() as? Bool + XCTAssertEqual(unwrapped, original) + } + + func testAnyValueWrapUnwrapInt() { + let original = 42 + let wrapped = AnyValue.wrap(original) + + if case .int(let value) = wrapped { + XCTAssertEqual(value, original) + } else { + XCTFail("Expected .int, got \(wrapped)") + } + + let unwrapped = wrapped.unwrap() as? Int + XCTAssertEqual(unwrapped, original) + } + + func testAnyValueWrapUnwrapDouble() { + let original = 3.14 + let wrapped = AnyValue.wrap(original) + + if case .double(let value) = wrapped { + XCTAssertEqual(value, original) + } else { + XCTFail("Expected .double, got \(wrapped)") + } + + let unwrapped = wrapped.unwrap() as? Double + XCTAssertEqual(unwrapped, original) + } + + func testAnyValueWrapUnwrapDictionary() { + let original: [String: Any] = ["key": "value", "number": 1] + let wrapped = AnyValue.wrap(original) + + if case .dictionary(let dict) = wrapped { + XCTAssertEqual(dict.count, 2) + if let val = dict["key"], case .string(let s) = val { + XCTAssertEqual(s, "value") + } else { + XCTFail("Expected string for key") + } + if let val = dict["number"], case .int(let i) = val { + XCTAssertEqual(i, 1) + } else { + XCTFail("Expected int for number") + } + } else { + XCTFail("Expected .dictionary, got \(wrapped)") + } + + let unwrapped = wrapped.unwrap() as? [String: Any] + XCTAssertEqual(unwrapped?["key"] as? String, "value") + XCTAssertEqual(unwrapped?["number"] as? Int, 1) + } + + func testAnyValueWrapUnwrapArray() { + let original: [Any] = ["value", 1] + let wrapped = AnyValue.wrap(original) + + if case .array(let array) = wrapped { + XCTAssertEqual(array.count, 2) + if case .string(let s) = array[0] { + XCTAssertEqual(s, "value") + } else { + XCTFail("Expected string at index 0") + } + if case .int(let i) = array[1] { + XCTAssertEqual(i, 1) + } else { + XCTFail("Expected int at index 1") + } + } else { + XCTFail("Expected .array, got \(wrapped)") + } + + let unwrapped = wrapped.unwrap() as? [Any] + XCTAssertEqual(unwrapped?[0] as? String, "value") + XCTAssertEqual(unwrapped?[1] as? Int, 1) + } + + func testAnyValueWrapUnknown() { + struct UnknownType {} + let original = UnknownType() + let wrapped = AnyValue.wrap(original) + + if case .null = wrapped { + XCTAssertTrue(true) + } else { + XCTFail("Expected .null for unknown type, got \(wrapped)") + } + } + + // MARK: - FlagDetails Tests + + func testFlagDetailsToSerializedDictionarySuccess() { + let details = FlagDetails( + key: "test_flag", + value: "test_value", + variant: "control", + reason: "targeting_match", + error: nil + ) + + let serialized = details.toSerializedDictionary() + + XCTAssertEqual(serialized["key"] as? String, "test_flag") + XCTAssertEqual(serialized["value"] as? String, "test_value") + XCTAssertEqual(serialized["variant"] as? String, "control") + XCTAssertEqual(serialized["reason"] as? String, "targeting_match") + XCTAssertNil(serialized["error"] as? String) + } + + func testFlagDetailsToSerializedDictionaryWithError() { + let details = FlagDetails( + key: "test_flag", + value: false, + variant: nil, + reason: nil, + error: .flagNotFound + ) + + let serialized = details.toSerializedDictionary() + + XCTAssertEqual(serialized["key"] as? String, "test_flag") + XCTAssertTrue(serialized["value"] as? Bool != nil) + XCTAssertNil(serialized["variant"] as? String) + XCTAssertNil(serialized["reason"] as? String) + XCTAssertEqual(serialized["error"] as? String, "FLAG_NOT_FOUND") + } + + func testFlagDetailsToSerializedDictionaryWithOtherErrors() { + let errorCases: [(FlagEvaluationError, String)] = [ + (.providerNotReady, "PROVIDER_NOT_READY"), + (.typeMismatch, "TYPE_MISMATCH"), + (.flagNotFound, "FLAG_NOT_FOUND") + ] + + for (error, expectedString) in errorCases { + let details = FlagDetails( + key: "key", + value: false, + variant: nil, + reason: nil, + error: error + ) + let serialized = details.toSerializedDictionary() + XCTAssertEqual(serialized["error"] as? String, expectedString) + } + } + + func testFlagDetailsToSerializedDictionaryWithDifferentValueTypes() { + let boolDetails = FlagDetails(key: "k", value: true, variant: nil, reason: nil, error: nil) + XCTAssertEqual(boolDetails.toSerializedDictionary()["value"] as? Bool, true) + + let intDetails = FlagDetails(key: "k", value: 123, variant: nil, reason: nil, error: nil) + XCTAssertEqual(intDetails.toSerializedDictionary()["value"] as? Int, 123) + + let doubleDetails = FlagDetails(key: "k", value: 12.34, variant: nil, reason: nil, error: nil) + XCTAssertEqual(doubleDetails.toSerializedDictionary()["value"] as? Double, 12.34) + + let anyValueDetails = FlagDetails(key: "k", value: AnyValue.string("s"), variant: nil, reason: nil, error: nil) + XCTAssertEqual(anyValueDetails.toSerializedDictionary()["value"] as? String, "s") + + struct Unknown: Equatable {} + let unknownDetails = FlagDetails(key: "k", value: Unknown(), variant: nil, reason: nil, error: nil) + XCTAssertTrue(unknownDetails.toSerializedDictionary()["value"] as? NSNull != nil) + } + + // MARK: - get*Details Tests + + func testGetBooleanDetails() { + let implementation = DdFlagsImplementation() + + let expectation = self.expectation(description: "Resolution called") + implementation.getBooleanDetails("default", key: "test_key", defaultValue: true, resolve: { result in + guard let dict = result as? [String: Any] else { + XCTFail("Expected dictionary result") + expectation.fulfill() + return + } + XCTAssertEqual(dict["value"] as? Bool, true) + expectation.fulfill() + }, reject: { _, _, _ in + XCTFail("Should not reject") + expectation.fulfill() + }) + + waitForExpectations(timeout: 1, handler: nil) + } + + func testGetStringDetails() { + let implementation = DdFlagsImplementation() + + let expectation = self.expectation(description: "Resolution called") + implementation.getStringDetails("default", key: "test_key", defaultValue: "default", resolve: { result in + guard let dict = result as? [String: Any] else { + XCTFail("Expected dictionary result") + expectation.fulfill() + return + } + XCTAssertEqual(dict["value"] as? String, "default") + expectation.fulfill() + }, reject: { _, _, _ in + XCTFail("Should not reject") + expectation.fulfill() + }) + + waitForExpectations(timeout: 1, handler: nil) + } + + func testGetNumberDetails() { + let implementation = DdFlagsImplementation() + + let expectation = self.expectation(description: "Resolution called") + implementation.getNumberDetails("default", key: "test_key", defaultValue: 123.45, resolve: { result in + guard let dict = result as? [String: Any] else { + XCTFail("Expected dictionary result") + expectation.fulfill() + return + } + XCTAssertEqual(dict["value"] as? Double, 123.45) + expectation.fulfill() + }, reject: { _, _, _ in + XCTFail("Should not reject") + expectation.fulfill() + }) + + waitForExpectations(timeout: 1, handler: nil) + } + + func testGetObjectDetails() { + let implementation = DdFlagsImplementation(core: core) + let defaultValue: [String: Any] = ["foo": "bar"] + + let expectation = self.expectation(description: "Resolution called") + implementation.getObjectDetails("default", key: "test_key", defaultValue: defaultValue, resolve: { result in + guard let dict = result as? [String: Any] else { + XCTFail("Expected dictionary result") + expectation.fulfill() + return + } + guard let value = dict["value"] as? [String: Any] else { + XCTFail("Expected dictionary value") + expectation.fulfill() + return + } + XCTAssertEqual(value["foo"] as? String, "bar") + expectation.fulfill() + }, reject: { _, _, _ in + XCTFail("Should not reject") + expectation.fulfill() + }) + + waitForExpectations(timeout: 1, handler: nil) + } +} + +private class FlagsTestCore: DatadogCoreProtocol { + private var features: [String: DatadogFeature] = [:] + + func register(feature: T) throws where T : DatadogFeature { + features[T.name] = feature + } + + func feature(named name: String, type: T.Type) -> T? { + return features[name] as? T + } + + func scope(for featureType: T.Type) -> any FeatureScope where T : DatadogFeature { + return NOPFeatureScope() + } + + func send(message: FeatureMessage, else fallback: @escaping () -> Void) {} + func set(context: @escaping () -> Context?) where Context: AdditionalContext {} + func mostRecentModifiedFileAt(before: Date) throws -> Date? { return nil } +} diff --git a/packages/core/ios/Tests/DdSdkTests.swift b/packages/core/ios/Tests/DdSdkTests.swift index c64f1acf7..e3c4334b3 100644 --- a/packages/core/ios/Tests/DdSdkTests.swift +++ b/packages/core/ios/Tests/DdSdkTests.swift @@ -8,6 +8,7 @@ import XCTest @testable import DatadogCore @testable import DatadogCrashReporting +@testable import DatadogFlags @testable import DatadogInternal @testable import DatadogLogs @testable import DatadogRUM @@ -309,6 +310,35 @@ class DdSdkTests: XCTestCase { XCTAssertNotNil(core.features[LogsFeature.name]) XCTAssertNotNil(core.features[TraceFeature.name]) } + + func testFlagsFeatureDisabledByDefault() { + let core = MockDatadogCore() + CoreRegistry.register(default: core) + defer { CoreRegistry.unregisterDefault() } + + let configuration: DdSdkConfiguration = .mockAny(configurationForFlags: nil) + + DdSdkNativeInitialization().enableFeatures( + sdkConfiguration: configuration + ) + + // Flagging SDK is disabled by default if no configuration is provided. + XCTAssertNil(core.features[FlagsFeature.name]) + } + + func testEnableFeatureFlags() { + let core = MockDatadogCore() + CoreRegistry.register(default: core) + defer { CoreRegistry.unregisterDefault() } + + let configuration: DdSdkConfiguration = .mockAny(configurationForFlags: ["enabled":true]) + + DdSdkNativeInitialization().enableFeatures( + sdkConfiguration: configuration + ) + + XCTAssertNotNil(core.features[FlagsFeature.name]) + } func testBuildConfigurationDefaultEndpoint() { let configuration: DdSdkConfiguration = .mockAny() diff --git a/packages/core/src/flags/__tests__/DatadogFlags.test.ts b/packages/core/src/flags/__tests__/DatadogFlags.test.ts new file mode 100644 index 000000000..84f9745a8 --- /dev/null +++ b/packages/core/src/flags/__tests__/DatadogFlags.test.ts @@ -0,0 +1,54 @@ +/* + * 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 { BufferSingleton } from '../../sdk/DatadogProvider/Buffer/BufferSingleton'; +import { DatadogFlags } from '../DatadogFlags'; + +jest.mock('../../InternalLog', () => { + return { + InternalLog: { + log: jest.fn() + }, + DATADOG_MESSAGE_PREFIX: 'DATADOG:' + }; +}); + +describe('DatadogFlags', () => { + beforeEach(() => { + jest.clearAllMocks(); + BufferSingleton.onInitialization(); + }); + + describe('Initialization', () => { + it('should print an error if retrieving the client before the feature is enabled', async () => { + DatadogFlags.getClient(); + + expect(InternalLog.log).toHaveBeenCalledWith( + 'DatadogFlags.getClient() called before DatadogFlags have been initialized. Flag evaluations will resolve to default values.', + SdkVerbosity.ERROR + ); + }); + + it('should print an error if retrieving the client if the feature was not enabled on purpose', async () => { + await DatadogFlags.enable({ enabled: false }); + DatadogFlags.getClient(); + + expect(InternalLog.log).toHaveBeenCalledWith( + 'DatadogFlags.getClient() called before DatadogFlags have been initialized. Flag evaluations will resolve to default values.', + SdkVerbosity.ERROR + ); + }); + + it('should not print an error if retrieving the client after the feature is enabled', async () => { + await DatadogFlags.enable({ enabled: true }); + DatadogFlags.getClient(); + + expect(InternalLog.log).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/packages/core/src/flags/__tests__/FlagsClient.test.ts b/packages/core/src/flags/__tests__/FlagsClient.test.ts new file mode 100644 index 000000000..8fd535d8d --- /dev/null +++ b/packages/core/src/flags/__tests__/FlagsClient.test.ts @@ -0,0 +1,197 @@ +/* + * 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 { NativeModules } from 'react-native'; + +import { InternalLog } from '../../InternalLog'; +import { SdkVerbosity } from '../../SdkVerbosity'; +import { BufferSingleton } from '../../sdk/DatadogProvider/Buffer/BufferSingleton'; +import { DatadogFlags } from '../DatadogFlags'; + +jest.mock('../../InternalLog', () => { + return { + InternalLog: { + log: jest.fn() + }, + DATADOG_MESSAGE_PREFIX: 'DATADOG:' + }; +}); + +describe('FlagsClient', () => { + beforeEach(async () => { + jest.clearAllMocks(); + BufferSingleton.onInitialization(); + + await DatadogFlags.enable({ enabled: true }); + }); + + describe('setEvaluationContext', () => { + it('should set the evaluation context', async () => { + const flagsClient = DatadogFlags.getClient(); + await flagsClient.setEvaluationContext({ + targetingKey: 'test-user-1', + attributes: { + country: 'US' + } + }); + + expect( + NativeModules.DdFlags.setEvaluationContext + ).toHaveBeenCalledWith('default', 'test-user-1', { country: 'US' }); + }); + + it('should print an error if there is an error', async () => { + NativeModules.DdFlags.setEvaluationContext.mockRejectedValue( + new Error('NETWORK_ERROR') + ); + + const flagsClient = DatadogFlags.getClient(); + await flagsClient.setEvaluationContext({ + targetingKey: 'test-user-1', + attributes: { + country: 'US' + } + }); + + expect(InternalLog.log).toHaveBeenCalledWith( + 'Error setting flag evaluation context: NETWORK_ERROR', + SdkVerbosity.ERROR + ); + }); + }); + + describe('getBooleanDetails', () => { + it('should fail the validation if the default value is not valid', async () => { + const flagsClient = DatadogFlags.getClient(); + const details = await flagsClient.getBooleanDetails( + 'test-boolean-flag', + // @ts-expect-error - we want to test the validation + 'true' + ); + + expect(details).toMatchObject({ + value: 'true', // The default value is passed through. + error: 'TYPE_MISMATCH', + reason: null, + variant: null + }); + }); + + it('should fetch the boolean details from native side', async () => { + const flagsClient = DatadogFlags.getClient(); + const details = await flagsClient.getBooleanDetails( + 'test-boolean-flag', + true + ); + + expect(details).toMatchObject({ + value: true, + variant: 'true', + reason: 'STATIC', + error: null + }); + }); + }); + + describe('getStringDetails', () => { + it('should fail the validation if the default value is not valid', async () => { + const flagsClient = DatadogFlags.getClient(); + const details = await flagsClient.getStringDetails( + 'test-string-flag', + // @ts-expect-error - we want to test the validation + true + ); + + expect(details).toMatchObject({ + value: true, // The default value is passed through. + error: 'TYPE_MISMATCH', + reason: null, + variant: null + }); + }); + + it('should fetch the string details from native side', async () => { + const flagsClient = DatadogFlags.getClient(); + const details = await flagsClient.getStringDetails( + 'test-string-flag', + 'hello world' + ); + + expect(details).toMatchObject({ + value: 'hello world', + variant: 'hello world', + reason: 'STATIC', + error: null + }); + }); + }); + + describe('getNumberDetails', () => { + it('should fail the validation if the default value is not valid', async () => { + const flagsClient = DatadogFlags.getClient(); + const details = await flagsClient.getNumberDetails( + 'test-number-flag', + // @ts-expect-error - we want to test the validation + 'hello world' + ); + + expect(details).toMatchObject({ + value: 'hello world', // The default value is passed through. + error: 'TYPE_MISMATCH', + reason: null, + variant: null + }); + }); + + it('should fetch the number details from native side', async () => { + const flagsClient = DatadogFlags.getClient(); + const details = await flagsClient.getNumberDetails( + 'test-number-flag', + 6 + ); + + expect(details).toMatchObject({ + value: 6, + variant: '6', + reason: 'STATIC', + error: null + }); + }); + }); + + describe('getObjectDetails', () => { + it('should fail the validation if the default value is not valid', async () => { + const flagsClient = DatadogFlags.getClient(); + const details = await flagsClient.getObjectDetails( + 'test-object-flag', + // @ts-expect-error - we want to test the validation + 'hello world' + ); + + expect(details).toMatchObject({ + value: 'hello world', // The default value is passed through. + error: 'TYPE_MISMATCH', + reason: null, + variant: null + }); + }); + + it('should fetch the object details from native side', async () => { + const flagsClient = DatadogFlags.getClient(); + const details = await flagsClient.getObjectDetails( + 'test-object-flag', + { hello: 'world' } + ); + + expect(details).toMatchObject({ + value: { hello: 'world' }, + variant: 'hello world', + reason: 'STATIC', + error: null + }); + }); + }); +}); From 8800f64c44a0a42ea18a90d85077225ec0b5fab6 Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Tue, 25 Nov 2025 17:37:07 +0200 Subject: [PATCH 076/526] Implement `DatadogFlags.enable()` method, add JSDoc comments to DatadogFlags --- example-new-architecture/App.tsx | 7 +- packages/core/ios/Sources/DdFlags.mm | 12 +++ .../ios/Sources/DdFlagsImplementation.swift | 11 +++ packages/core/src/flags/DatadogFlags.ts | 75 ++++++++++++++++--- packages/core/src/flags/types.ts | 49 ++++++++++-- packages/core/src/specs/NativeDdFlags.ts | 5 +- 6 files changed, 132 insertions(+), 27 deletions(-) diff --git a/example-new-architecture/App.tsx b/example-new-architecture/App.tsx index f6aecaac5..5e921c234 100644 --- a/example-new-architecture/App.tsx +++ b/example-new-architecture/App.tsx @@ -46,11 +46,7 @@ import {APPLICATION_ID, CLIENT_TOKEN, ENVIRONMENT} from './ddCredentials'; config.telemetrySampleRate = 100; config.uploadFrequency = UploadFrequency.FREQUENT; config.batchSize = BatchSize.SMALL; - config.flagsConfiguration = { - enabled: true, - }; await DdSdkReactNative.initialize(config); - await DatadogFlags.enable(config.flagsConfiguration); await DdRum.startView('main', 'Main'); setTimeout(async () => { await DdRum.addTiming('one_second'); @@ -95,6 +91,8 @@ function App(): React.JSX.Element { const [testFlagValue, setTestFlagValue] = React.useState(false); React.useEffect(() => { (async () => { + await DatadogFlags.enable(); + const flagsClient = DatadogFlags.getClient(); await flagsClient.setEvaluationContext({ targetingKey: 'test-user-1', @@ -103,7 +101,6 @@ function App(): React.JSX.Element { }, }); const flag = await flagsClient.getBooleanDetails('rn-sdk-test-boolean-flag', false); // https://app.datadoghq.com/feature-flags/046d0e70-626d-41e1-8314-3f009fb79b7a?environmentId=d114cd9a-79ed-4c56-bcf3-bcac9293653b - console.log({flag}) setTestFlagValue(flag.value); })(); }, []); diff --git a/packages/core/ios/Sources/DdFlags.mm b/packages/core/ios/Sources/DdFlags.mm index 59939d45b..cbf3abc7e 100644 --- a/packages/core/ios/Sources/DdFlags.mm +++ b/packages/core/ios/Sources/DdFlags.mm @@ -16,6 +16,14 @@ @implementation DdFlags RCT_EXPORT_MODULE() +RCT_REMAP_METHOD(enable, + withConfiguration:(NSDictionary *)configuration + withResolve:(RCTPromiseResolveBlock)resolve + withReject:(RCTPromiseRejectBlock)reject) +{ + [self enable:configuration resolve:resolve reject:reject]; +} + RCT_REMAP_METHOD(setEvaluationContext, withClientName:(NSString *)clientName withTargetingKey:(NSString *)targetingKey @@ -91,6 +99,10 @@ - (dispatch_queue_t)methodQueue { return [RNQueue getSharedQueue]; } +- (void)enable:(NSDictionary *)configuration resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + [self.ddFlagsImplementation enable:configuration resolve:resolve reject:reject]; +} + - (void)setEvaluationContext:(NSString *)clientName targetingKey:(NSString *)targetingKey attributes:(NSDictionary *)attributes resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { [self.ddFlagsImplementation setEvaluationContext:clientName targetingKey:targetingKey attributes:attributes resolve:resolve reject:reject]; } diff --git a/packages/core/ios/Sources/DdFlagsImplementation.swift b/packages/core/ios/Sources/DdFlagsImplementation.swift index 7b128db17..3b2ed4faf 100644 --- a/packages/core/ios/Sources/DdFlagsImplementation.swift +++ b/packages/core/ios/Sources/DdFlagsImplementation.swift @@ -25,6 +25,17 @@ public class DdFlagsImplementation: NSObject { self.init(core: CoreRegistry.default) } + @objc + public func enable(_ configuration: NSDictionary, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) { + if let config = configuration.asConfigurationForFlags() { + Flags.enable(with: config) + } else { + consolePrint("Invalid configuration provided for Flags. Feature initialization skipped.", .error) + } + + resolve(nil) + } + /// Retrieve a `FlagsClient` instance in a non-interruptive way for usage in methods bridged to React Native. /// /// We create a simple registry of client providers by client name holding closures for retrieving a client since client references are kept internally in the flagging SDK. diff --git a/packages/core/src/flags/DatadogFlags.ts b/packages/core/src/flags/DatadogFlags.ts index 195881112..206bbb276 100644 --- a/packages/core/src/flags/DatadogFlags.ts +++ b/packages/core/src/flags/DatadogFlags.ts @@ -6,6 +6,7 @@ import { InternalLog } from '../InternalLog'; import { SdkVerbosity } from '../SdkVerbosity'; +import type { DdNativeFlagsType } from '../nativeModulesTypes'; import { getGlobalInstance } from '../utils/singletonUtils'; import { FlagsClient } from './FlagsClient'; @@ -14,27 +15,81 @@ import type { DatadogFlagsType, DatadogFlagsConfiguration } from './types'; const FLAGS_MODULE = 'com.datadog.reactnative.flags'; class DatadogFlagsWrapper implements DatadogFlagsType { + // eslint-disable-next-line global-require, @typescript-eslint/no-var-requires + private nativeFlags: DdNativeFlagsType = require('../specs/NativeDdFlags') + .default; + private isFeatureEnabled = false; + /** + * Enables the Datadog Flags feature in your application. + * + * Call this method after initializing the Datadog SDK to enable feature flag evaluation. + * This method must be called before creating any `FlagsClient` instances via `DatadogFlags.getClient()`. + * + * @example + * ```ts + * import { DdSdkReactNativeConfiguration, DdSdkReactNative, DatadogFlags } from '@datadog/mobile-react-native'; + * + * // Initialize the Datadog SDK. + * await DdSdkReactNative.initialize(...); + * + * // Optinal flags configuration object. + * const flagsConfig = { + * customFlagsEndpoint: 'https://flags.example.com' + * }; + * + * // Enable the feature. + * await DatadogFlags.enable(flagsConfig); + * + * // Retrieve the client and access feature flags. + * const flagsClient = DatadogFlags.getClient(); + * const flagValue = await flagsClient.getBooleanValue('new-feature', false); + * ``` + * + * @param configuration Configuration options for the Datadog Flags feature. + */ + enable = async ( + configuration?: Omit + ): Promise => { + if (this.isFeatureEnabled) { + InternalLog.log( + 'Datadog Flags feature has already been enabled. Skipping this `DatadogFlags.enable()` call.', + SdkVerbosity.WARN + ); + return; + } + + await this.nativeFlags.enable({ ...configuration, enabled: true }); + + this.isFeatureEnabled = true; + }; + + /** + * Returns a `FlagsClient` instance for further feature flag evaluation. + * + * For most applications, you would need only one client. If you need multiple clients, + * you can retrieve a couple of clients with different names. + * + * @param clientName An optional name of the client to retrieve. Defaults to `'default'`. + * + * @example + * ```ts + * // Reminder: you need to initialize the SDK and enable the Flags feature before retrieving the client. + * const flagsClient = DatadogFlags.getClient(); + * const flagValue = await flagsClient.getBooleanValue('new-feature', false); + * ``` + */ getClient = (clientName: string = 'default'): FlagsClient => { if (!this.isFeatureEnabled) { InternalLog.log( - 'DatadogFlags.getClient() called before DatadogFlags have been initialized. Flag evaluations will resolve to default values.', + '`DatadogFlags.getClient()` called before Datadog Flags feature have been enabled. Client will fall back to serving default flag values.', SdkVerbosity.ERROR ); } return new FlagsClient(clientName); }; - - enable = async ( - _configuration: DatadogFlagsConfiguration - ): Promise => { - // Feature Flags are initialized globally by default for now. - this.isFeatureEnabled = _configuration.enabled; - - return Promise.resolve(); - }; } export const DatadogFlags: DatadogFlagsType = getGlobalInstance( diff --git a/packages/core/src/flags/types.ts b/packages/core/src/flags/types.ts index 6ecb0880f..43ff9aa7f 100644 --- a/packages/core/src/flags/types.ts +++ b/packages/core/src/flags/types.ts @@ -8,17 +8,50 @@ import type { FlagsClient } from './FlagsClient'; export type DatadogFlagsType = { /** - * Returns a `FlagsClient` instance for further feature flag evaluation. + * Enables the Datadog Flags feature in your application. + * + * Call this method after initializing the Datadog SDK to enable feature flag evaluation. + * This method must be called before creating any `FlagsClient` instances via `DatadogFlags.getClient()`. + * + * @example + * ```ts + * import { DdSdkReactNativeConfiguration, DdSdkReactNative, DatadogFlags } from '@datadog/mobile-react-native'; + * + * // Initialize the Datadog SDK. + * await DdSdkReactNative.initialize(...); + * + * // Optinal flags configuration object. + * const flagsConfig = { + * customFlagsEndpoint: 'https://flags.example.com' + * }; + * + * // Enable the feature. + * await DatadogFlags.enable(flagsConfig); + * + * // Retrieve the client and access feature flags. + * const flagsClient = DatadogFlags.getClient(); + * const flagValue = await flagsClient.getBooleanValue('new-feature', false); + * ``` * - * If client name is not provided, the `'default'` client is returned. + * @param configuration Configuration options for the Datadog Flags feature. */ - getClient: (clientName?: string) => FlagsClient; + enable: (configuration?: DatadogFlagsConfiguration) => Promise; /** - * Enables the Datadog Flags feature. + * Returns a `FlagsClient` instance for further feature flag evaluation. + * + * For most applications, you would need only one client. If you need multiple clients, + * you can retrieve a couple of clients with different names. * - * TODO: This method is no-op for now, as flags are initialized globally by default. + * @param clientName An optional name of the client to retrieve. Defaults to `'default'`. + * + * @example + * ```ts + * // Reminder: you need to initialize the SDK and enable the Flags feature before retrieving the client. + * const flagsClient = DatadogFlags.getClient(); + * const flagValue = await flagsClient.getBooleanValue('new-feature', false); + * ``` */ - enable: (configuration: DatadogFlagsConfiguration) => Promise; + getClient: (clientName?: string) => FlagsClient; }; /** @@ -27,7 +60,7 @@ export type DatadogFlagsType = { * Use this type to customize the behavior of feature flag evaluation, including custom endpoints, * exposure tracking, and error handling modes. */ -export interface DatadogFlagsConfiguration { +export type DatadogFlagsConfiguration = { /** * Controls whether the feature flag evaluation feature is enabled. */ @@ -72,7 +105,7 @@ export interface DatadogFlagsConfiguration { * @default true */ rumIntegrationEnabled?: boolean; -} +}; /** * Context information used for feature flag targeting and evaluation. diff --git a/packages/core/src/specs/NativeDdFlags.ts b/packages/core/src/specs/NativeDdFlags.ts index 52461c660..27d349319 100644 --- a/packages/core/src/specs/NativeDdFlags.ts +++ b/packages/core/src/specs/NativeDdFlags.ts @@ -14,10 +14,7 @@ import type { FlagDetails } from '../flags/types'; * Do not import this Spec directly, use DdNativeFlagsType instead. */ export interface Spec extends TurboModule { - // TODO: Flags and all other features are initialized globally for now. We want to change this in the future. - // readonly enable: ( - // configuration: DatadogFlagsConfiguration - // ) => Promise; + readonly enable: (configuration: Object) => Promise; readonly setEvaluationContext: ( clientName: string, From e266879557ac1f6c7c10b37885ab53ad9a58d56b Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Tue, 25 Nov 2025 17:38:27 +0200 Subject: [PATCH 077/526] Remove redundant `gracefulModeEnabled` setting --- packages/core/src/DdSdkReactNative.tsx | 8 -------- 1 file changed, 8 deletions(-) diff --git a/packages/core/src/DdSdkReactNative.tsx b/packages/core/src/DdSdkReactNative.tsx index 5da26b249..8954ea632 100644 --- a/packages/core/src/DdSdkReactNative.tsx +++ b/packages/core/src/DdSdkReactNative.tsx @@ -354,14 +354,6 @@ export class DdSdkReactNative { ] = `${reactNativeVersion}`; } - // Hard set `gracefulModeEnabled` to `true` because crashing an app on misconfiguration - // is not the usual workflow for React Native. - if (configuration.flagsConfiguration) { - Object.assign(configuration.flagsConfiguration, { - gracefulModeEnabled: true - }); - } - return new DdSdkConfiguration( configuration.clientToken, configuration.env, From 0e6a850ae01e809777aa409731d8b8fb0bce89a0 Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Tue, 25 Nov 2025 17:42:58 +0200 Subject: [PATCH 078/526] Move the `DatadogFlags.enable` call to `initializeNativeSDK` --- example-new-architecture/App.tsx | 5 +++-- packages/core/src/DdSdkReactNative.tsx | 8 ++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/example-new-architecture/App.tsx b/example-new-architecture/App.tsx index 5e921c234..a01451704 100644 --- a/example-new-architecture/App.tsx +++ b/example-new-architecture/App.tsx @@ -46,6 +46,9 @@ import {APPLICATION_ID, CLIENT_TOKEN, ENVIRONMENT} from './ddCredentials'; config.telemetrySampleRate = 100; config.uploadFrequency = UploadFrequency.FREQUENT; config.batchSize = BatchSize.SMALL; + config.flagsConfiguration = { + enabled: true, + }; await DdSdkReactNative.initialize(config); await DdRum.startView('main', 'Main'); setTimeout(async () => { @@ -91,8 +94,6 @@ function App(): React.JSX.Element { const [testFlagValue, setTestFlagValue] = React.useState(false); React.useEffect(() => { (async () => { - await DatadogFlags.enable(); - const flagsClient = DatadogFlags.getClient(); await flagsClient.setEvaluationContext({ targetingKey: 'test-user-1', diff --git a/packages/core/src/DdSdkReactNative.tsx b/packages/core/src/DdSdkReactNative.tsx index 8954ea632..c07f54a89 100644 --- a/packages/core/src/DdSdkReactNative.tsx +++ b/packages/core/src/DdSdkReactNative.tsx @@ -99,6 +99,10 @@ export class DdSdkReactNative { DdSdkReactNative.buildConfiguration(configuration, params) ); + if (configuration.flagsConfiguration) { + await DatadogFlags.enable(configuration.flagsConfiguration); + } + InternalLog.log('Datadog SDK was initialized', SdkVerbosity.INFO); GlobalState.instance.isInitialized = true; BufferSingleton.onInitialization(); @@ -465,10 +469,6 @@ export class DdSdkReactNative { DdRum.registerActionEventMapper(configuration.actionEventMapper); } - if (configuration.flagsConfiguration) { - DatadogFlags.enable(configuration.flagsConfiguration); - } - DdSdkReactNative.wasAutoInstrumented = true; } } From ab6fa5f7b42abd426e65c36b9613455392c8bfb6 Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Tue, 25 Nov 2025 22:44:00 +0200 Subject: [PATCH 079/526] Add more tests --- packages/core/__mocks__/react-native.ts | 1 + packages/core/src/DdSdkReactNative.tsx | 5 +- .../src/DdSdkReactNativeConfiguration.tsx | 1 + .../src/__tests__/DdSdkReactNative.test.tsx | 115 +++++++++ .../DdSdkReactNativeConfiguration.test.ts | 222 ++++++++++++++++++ .../src/flags/__tests__/DatadogFlags.test.ts | 25 +- 6 files changed, 356 insertions(+), 13 deletions(-) diff --git a/packages/core/__mocks__/react-native.ts b/packages/core/__mocks__/react-native.ts index 8e8b22d78..336e6be71 100644 --- a/packages/core/__mocks__/react-native.ts +++ b/packages/core/__mocks__/react-native.ts @@ -159,6 +159,7 @@ actualRN.NativeModules.DdRum = { }; actualRN.NativeModules.DdFlags = { + enable: jest.fn().mockImplementation(() => Promise.resolve()), setEvaluationContext: jest.fn().mockImplementation(() => Promise.resolve()), getBooleanDetails: jest.fn().mockImplementation(() => Promise.resolve({ diff --git a/packages/core/src/DdSdkReactNative.tsx b/packages/core/src/DdSdkReactNative.tsx index c07f54a89..9865a0f37 100644 --- a/packages/core/src/DdSdkReactNative.tsx +++ b/packages/core/src/DdSdkReactNative.tsx @@ -99,7 +99,10 @@ export class DdSdkReactNative { DdSdkReactNative.buildConfiguration(configuration, params) ); - if (configuration.flagsConfiguration) { + if ( + configuration.flagsConfiguration && + configuration.flagsConfiguration.enabled !== false + ) { await DatadogFlags.enable(configuration.flagsConfiguration); } diff --git a/packages/core/src/DdSdkReactNativeConfiguration.tsx b/packages/core/src/DdSdkReactNativeConfiguration.tsx index 5df2ff8d9..94db29814 100644 --- a/packages/core/src/DdSdkReactNativeConfiguration.tsx +++ b/packages/core/src/DdSdkReactNativeConfiguration.tsx @@ -485,6 +485,7 @@ export type PartialInitializationConfiguration = { readonly batchProcessingLevel?: BatchProcessingLevel; readonly initialResourceThreshold?: number; readonly trackMemoryWarnings?: boolean; + readonly flagsConfiguration?: DatadogFlagsConfiguration; }; const setConfigurationAttribute = < diff --git a/packages/core/src/__tests__/DdSdkReactNative.test.tsx b/packages/core/src/__tests__/DdSdkReactNative.test.tsx index 5e6f8c447..6ce8a05d6 100644 --- a/packages/core/src/__tests__/DdSdkReactNative.test.tsx +++ b/packages/core/src/__tests__/DdSdkReactNative.test.tsx @@ -12,6 +12,7 @@ import { DdSdkReactNative } from '../DdSdkReactNative'; import { ProxyConfiguration, ProxyType } from '../ProxyConfiguration'; import { SdkVerbosity } from '../SdkVerbosity'; import { TrackingConsent } from '../TrackingConsent'; +import { DatadogFlags } from '../flags/DatadogFlags'; import { DdLogs } from '../logs/DdLogs'; import { DdRum } from '../rum/DdRum'; import { DdRumErrorTracking } from '../rum/instrumentation/DdRumErrorTracking'; @@ -28,6 +29,14 @@ import { version as sdkVersion } from '../version'; jest.mock('../InternalLog'); +jest.mock('../flags/DatadogFlags', () => { + return { + DatadogFlags: { + enable: jest.fn().mockResolvedValue(undefined) + } + }; +}); + jest.mock( '../rum/instrumentation/interactionTracking/DdRumUserInteractionTracking', () => { @@ -75,6 +84,9 @@ beforeEach(async () => { (DdRumErrorTracking.startTracking as jest.MockedFunction< typeof DdRumErrorTracking.startTracking >).mockClear(); + (DatadogFlags.enable as jest.MockedFunction< + typeof DatadogFlags.enable + >).mockClear(); DdLogs.unregisterLogEventMapper(); UserInfoSingleton.reset(); @@ -507,6 +519,109 @@ describe('DdSdkReactNative', () => { }) ); }); + + it('does not enable DatadogFlags when flagsConfiguration is not provided', async () => { + // GIVEN + const fakeAppId = '1'; + const fakeClientToken = '2'; + const fakeEnvName = 'env'; + const configuration = new DdSdkReactNativeConfiguration( + fakeClientToken, + fakeEnvName, + fakeAppId + ); + + NativeModules.DdSdk.initialize.mockResolvedValue(null); + + // WHEN + await DdSdkReactNative.initialize(configuration); + + // THEN + expect(DatadogFlags.enable).not.toHaveBeenCalled(); + }); + + it('enables DatadogFlags when flagsConfiguration is provided', async () => { + // GIVEN + const fakeAppId = '1'; + const fakeClientToken = '2'; + const fakeEnvName = 'env'; + const configuration = new DdSdkReactNativeConfiguration( + fakeClientToken, + fakeEnvName, + fakeAppId + ); + configuration.flagsConfiguration = { + enabled: true + }; + + NativeModules.DdSdk.initialize.mockResolvedValue(null); + + // WHEN + await DdSdkReactNative.initialize(configuration); + + // THEN + expect(DatadogFlags.enable).toHaveBeenCalledTimes(1); + expect(DatadogFlags.enable).toHaveBeenCalledWith({ + enabled: true + }); + }); + + it('enables DatadogFlags with custom configuration when provided', async () => { + // GIVEN + const fakeAppId = '1'; + const fakeClientToken = '2'; + const fakeEnvName = 'env'; + const customFlagsEndpoint = 'https://flags.example.com'; + const customFlagsHeaders = { + Authorization: 'Bearer token123' + }; + const configuration = new DdSdkReactNativeConfiguration( + fakeClientToken, + fakeEnvName, + fakeAppId + ); + configuration.flagsConfiguration = { + enabled: true, + customFlagsEndpoint, + customFlagsHeaders + }; + + NativeModules.DdSdk.initialize.mockResolvedValue(null); + + // WHEN + await DdSdkReactNative.initialize(configuration); + + // THEN + expect(DatadogFlags.enable).toHaveBeenCalledTimes(1); + expect(DatadogFlags.enable).toHaveBeenCalledWith({ + enabled: true, + customFlagsEndpoint, + customFlagsHeaders + }); + }); + + it('does not call DatadogFlags.enable when flagsConfiguration.enabled is false', async () => { + // GIVEN + const fakeAppId = '1'; + const fakeClientToken = '2'; + const fakeEnvName = 'env'; + const configuration = new DdSdkReactNativeConfiguration( + fakeClientToken, + fakeEnvName, + fakeAppId + ); + configuration.flagsConfiguration = { + enabled: false + }; + + NativeModules.DdSdk.initialize.mockResolvedValue(null); + + // WHEN + await DdSdkReactNative.initialize(configuration); + + // THEN + expect(DatadogFlags.enable).not.toHaveBeenCalled(); + }); }); describe('feature enablement', () => { diff --git a/packages/core/src/__tests__/DdSdkReactNativeConfiguration.test.ts b/packages/core/src/__tests__/DdSdkReactNativeConfiguration.test.ts index 60a9d13ff..8193621c7 100644 --- a/packages/core/src/__tests__/DdSdkReactNativeConfiguration.test.ts +++ b/packages/core/src/__tests__/DdSdkReactNativeConfiguration.test.ts @@ -261,5 +261,227 @@ describe('DdSdkReactNativeConfiguration', () => { } `); }); + + it('builds the SDK configuration with flags configuration enabled', () => { + expect( + buildConfigurationFromPartialConfiguration( + { + trackErrors: false, + trackInteractions: false, + trackResources: false + }, + { + applicationId: 'fake-app-id', + clientToken: 'fake-client-token', + env: 'fake-env', + flagsConfiguration: { + enabled: true, + customFlagsEndpoint: 'https://flags.example.com', + customFlagsHeaders: { + Authorization: 'Bearer token123', + 'X-Custom-Header': 'custom-value' + } + } + } + ) + ).toMatchInlineSnapshot(` + DdSdkReactNativeConfiguration { + "actionEventMapper": null, + "additionalConfiguration": {}, + "applicationId": "fake-app-id", + "batchProcessingLevel": "MEDIUM", + "batchSize": "MEDIUM", + "bundleLogsWithRum": true, + "bundleLogsWithTraces": true, + "clientToken": "fake-client-token", + "customEndpoints": {}, + "env": "fake-env", + "errorEventMapper": null, + "firstPartyHosts": [], + "flagsConfiguration": { + "customFlagsEndpoint": "https://flags.example.com", + "customFlagsHeaders": { + "Authorization": "Bearer token123", + "X-Custom-Header": "custom-value", + }, + "enabled": true, + }, + "logEventMapper": null, + "longTaskThresholdMs": 0, + "nativeCrashReportEnabled": false, + "nativeInteractionTracking": false, + "nativeLongTaskThresholdMs": 200, + "nativeViewTracking": false, + "proxyConfig": undefined, + "resourceEventMapper": null, + "resourceTracingSamplingRate": 100, + "serviceName": undefined, + "sessionSamplingRate": 100, + "site": "US1", + "telemetrySampleRate": 20, + "trackBackgroundEvents": false, + "trackErrors": false, + "trackFrustrations": true, + "trackInteractions": false, + "trackMemoryWarnings": true, + "trackResources": false, + "trackWatchdogTerminations": false, + "trackingConsent": "granted", + "uploadFrequency": "AVERAGE", + "useAccessibilityLabel": true, + "verbosity": undefined, + "vitalsUpdateFrequency": "AVERAGE", + } + `); + }); + + it('builds the SDK configuration with flags configuration disabled', () => { + expect( + buildConfigurationFromPartialConfiguration( + { + trackErrors: false, + trackInteractions: false, + trackResources: false + }, + { + applicationId: 'fake-app-id', + clientToken: 'fake-client-token', + env: 'fake-env', + flagsConfiguration: { + enabled: false + } + } + ) + ).toMatchInlineSnapshot(` + DdSdkReactNativeConfiguration { + "actionEventMapper": null, + "additionalConfiguration": {}, + "applicationId": "fake-app-id", + "batchProcessingLevel": "MEDIUM", + "batchSize": "MEDIUM", + "bundleLogsWithRum": true, + "bundleLogsWithTraces": true, + "clientToken": "fake-client-token", + "customEndpoints": {}, + "env": "fake-env", + "errorEventMapper": null, + "firstPartyHosts": [], + "flagsConfiguration": { + "enabled": false, + }, + "logEventMapper": null, + "longTaskThresholdMs": 0, + "nativeCrashReportEnabled": false, + "nativeInteractionTracking": false, + "nativeLongTaskThresholdMs": 200, + "nativeViewTracking": false, + "proxyConfig": undefined, + "resourceEventMapper": null, + "resourceTracingSamplingRate": 100, + "serviceName": undefined, + "sessionSamplingRate": 100, + "site": "US1", + "telemetrySampleRate": 20, + "trackBackgroundEvents": false, + "trackErrors": false, + "trackFrustrations": true, + "trackInteractions": false, + "trackMemoryWarnings": true, + "trackResources": false, + "trackWatchdogTerminations": false, + "trackingConsent": "granted", + "uploadFrequency": "AVERAGE", + "useAccessibilityLabel": true, + "verbosity": undefined, + "vitalsUpdateFrequency": "AVERAGE", + } + `); + }); + + it('builds the SDK configuration with full flags configuration', () => { + expect( + buildConfigurationFromPartialConfiguration( + { + trackErrors: true, + trackInteractions: true, + trackResources: true, + firstPartyHosts: ['api.com'], + resourceTracingSamplingRate: 80 + }, + { + applicationId: 'fake-app-id', + clientToken: 'fake-client-token', + env: 'fake-env', + sessionSamplingRate: 80, + site: 'EU', + verbosity: SdkVerbosity.DEBUG, + serviceName: 'com.test.app', + version: '1.4.5', + flagsConfiguration: { + enabled: true, + customFlagsEndpoint: 'https://flags.example.com', + customFlagsHeaders: { + Authorization: 'Bearer token123' + }, + customExposureEndpoint: + 'https://exposure.example.com', + trackExposures: true + } + } + ) + ).toMatchInlineSnapshot(` + DdSdkReactNativeConfiguration { + "actionEventMapper": null, + "additionalConfiguration": {}, + "applicationId": "fake-app-id", + "batchProcessingLevel": "MEDIUM", + "batchSize": "MEDIUM", + "bundleLogsWithRum": true, + "bundleLogsWithTraces": true, + "clientToken": "fake-client-token", + "customEndpoints": {}, + "env": "fake-env", + "errorEventMapper": null, + "firstPartyHosts": [ + "api.com", + ], + "flagsConfiguration": { + "customExposureEndpoint": "https://exposure.example.com", + "customFlagsEndpoint": "https://flags.example.com", + "customFlagsHeaders": { + "Authorization": "Bearer token123", + }, + "enabled": true, + "trackExposures": true, + }, + "logEventMapper": null, + "longTaskThresholdMs": 0, + "nativeCrashReportEnabled": false, + "nativeInteractionTracking": false, + "nativeLongTaskThresholdMs": 200, + "nativeViewTracking": false, + "proxyConfig": undefined, + "resourceEventMapper": null, + "resourceTracingSamplingRate": 80, + "serviceName": "com.test.app", + "sessionSamplingRate": 80, + "site": "EU", + "telemetrySampleRate": 20, + "trackBackgroundEvents": false, + "trackErrors": true, + "trackFrustrations": true, + "trackInteractions": true, + "trackMemoryWarnings": true, + "trackResources": true, + "trackWatchdogTerminations": false, + "trackingConsent": "granted", + "uploadFrequency": "AVERAGE", + "useAccessibilityLabel": true, + "verbosity": "debug", + "version": "1.4.5", + "vitalsUpdateFrequency": "AVERAGE", + } + `); + }); }); }); diff --git a/packages/core/src/flags/__tests__/DatadogFlags.test.ts b/packages/core/src/flags/__tests__/DatadogFlags.test.ts index 84f9745a8..70a76102f 100644 --- a/packages/core/src/flags/__tests__/DatadogFlags.test.ts +++ b/packages/core/src/flags/__tests__/DatadogFlags.test.ts @@ -4,9 +4,10 @@ * Copyright 2016-Present Datadog, Inc. */ +import { NativeModules } from 'react-native'; + import { InternalLog } from '../../InternalLog'; import { SdkVerbosity } from '../../SdkVerbosity'; -import { BufferSingleton } from '../../sdk/DatadogProvider/Buffer/BufferSingleton'; import { DatadogFlags } from '../DatadogFlags'; jest.mock('../../InternalLog', () => { @@ -21,31 +22,31 @@ jest.mock('../../InternalLog', () => { describe('DatadogFlags', () => { beforeEach(() => { jest.clearAllMocks(); - BufferSingleton.onInitialization(); + // Reset state of DatadogFlags instance. + Object.assign(DatadogFlags, { isFeatureEnabled: false }); }); describe('Initialization', () => { - it('should print an error if retrieving the client before the feature is enabled', async () => { - DatadogFlags.getClient(); + it('should print an error if calling DatadogFlags.enable() for multiple times', async () => { + await DatadogFlags.enable(); + await DatadogFlags.enable(); + await DatadogFlags.enable(); - expect(InternalLog.log).toHaveBeenCalledWith( - 'DatadogFlags.getClient() called before DatadogFlags have been initialized. Flag evaluations will resolve to default values.', - SdkVerbosity.ERROR - ); + expect(InternalLog.log).toHaveBeenCalledTimes(2); + expect(NativeModules.DdFlags.enable).toHaveBeenCalledTimes(1); }); - it('should print an error if retrieving the client if the feature was not enabled on purpose', async () => { - await DatadogFlags.enable({ enabled: false }); + it('should print an error if retrieving the client before the feature is enabled', async () => { DatadogFlags.getClient(); expect(InternalLog.log).toHaveBeenCalledWith( - 'DatadogFlags.getClient() called before DatadogFlags have been initialized. Flag evaluations will resolve to default values.', + '`DatadogFlags.getClient()` called before Datadog Flags feature have been enabled. Client will fall back to serving default flag values.', SdkVerbosity.ERROR ); }); it('should not print an error if retrieving the client after the feature is enabled', async () => { - await DatadogFlags.enable({ enabled: true }); + await DatadogFlags.enable(); DatadogFlags.getClient(); expect(InternalLog.log).not.toHaveBeenCalled(); From 3ef21c52772a5e565e592405a0ba4bc814da63fb Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Tue, 25 Nov 2025 22:46:05 +0200 Subject: [PATCH 080/526] Update Podfile.lock --- example-new-architecture/ios/Podfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example-new-architecture/ios/Podfile.lock b/example-new-architecture/ios/Podfile.lock index 4a49e5325..0b8ceafb3 100644 --- a/example-new-architecture/ios/Podfile.lock +++ b/example-new-architecture/ios/Podfile.lock @@ -1981,6 +1981,6 @@ SPEC CHECKSUMS: SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 Yoga: feb4910aba9742cfedc059e2b2902e22ffe9954a -PODFILE CHECKSUM: c0a8ceabe25801227323f2a415c464bfca5e3f73 +PODFILE CHECKSUM: 470f1ade1ca669373855527342da02c29dfcdfdf COCOAPODS: 1.16.2 From 0af85d385ae1213cd0d26e3dc77df2e81e488a87 Mon Sep 17 00:00:00 2001 From: "Xavier F. Gouchet" Date: Mon, 7 Apr 2025 14:55:53 +0200 Subject: [PATCH 081/526] 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 082/526] 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 083/526] 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 084/526] 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 085/526] 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 086/526] 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 087/526] 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 088/526] 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 089/526] 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 090/526] 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 091/526] 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 092/526] 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 093/526] 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 094/526] 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 095/526] 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 096/526] 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 097/526] 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 098/526] 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 099/526] 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 100/526] 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 101/526] 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 102/526] 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 103/526] 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 104/526] 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 105/526] 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 106/526] 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 107/526] 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 108/526] 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 109/526] 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 110/526] 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 111/526] 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 112/526] 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 113/526] 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 114/526] 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 115/526] 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 116/526] 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 117/526] 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 118/526] 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 119/526] 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 120/526] 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 121/526] 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 122/526] 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 123/526] 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 ea1967de181de080b4ee745de5ef18328e196b3e Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Mon, 1 Dec 2025 14:17:29 +0200 Subject: [PATCH 124/526] Remove Flags initialization and configuration logic from the shared init step --- example-new-architecture/App.tsx | 2 ++ example/src/WixApp.tsx | 3 ++- example/src/ddUtils.tsx | 13 +------------ example/src/screens/MainScreen.tsx | 2 ++ .../ios/Sources/DdSdkNativeInitialization.swift | 5 ----- packages/core/src/DdSdkReactNative.tsx | 11 +---------- packages/core/src/DdSdkReactNativeConfiguration.tsx | 5 ----- packages/core/src/types.tsx | 4 +--- 8 files changed, 9 insertions(+), 36 deletions(-) diff --git a/example-new-architecture/App.tsx b/example-new-architecture/App.tsx index a01451704..f0f4434dd 100644 --- a/example-new-architecture/App.tsx +++ b/example-new-architecture/App.tsx @@ -94,6 +94,8 @@ function App(): React.JSX.Element { const [testFlagValue, setTestFlagValue] = React.useState(false); React.useEffect(() => { (async () => { + await DatadogFlags.enable(); + const flagsClient = DatadogFlags.getClient(); await flagsClient.setEvaluationContext({ targetingKey: 'test-user-1', diff --git a/example/src/WixApp.tsx b/example/src/WixApp.tsx index dc6ed2dc5..6f6482c10 100644 --- a/example/src/WixApp.tsx +++ b/example/src/WixApp.tsx @@ -47,6 +47,8 @@ const HomeScreen = props => { const [testFlagValue, setTestFlagValue] = useState(false); useEffect(() => { (async () => { + await DatadogFlags.enable(); + const flagsClient = DatadogFlags.getClient(); await flagsClient.setEvaluationContext({ targetingKey: 'test-user-1', @@ -55,7 +57,6 @@ const HomeScreen = props => { }, }); const flag = await flagsClient.getBooleanDetails('rn-sdk-test-boolean-flag', false); // https://app.datadoghq.com/feature-flags/046d0e70-626d-41e1-8314-3f009fb79b7a?environmentId=d114cd9a-79ed-4c56-bcf3-bcac9293653b - console.log({flag}) setTestFlagValue(flag.value); })(); }, []); diff --git a/example/src/ddUtils.tsx b/example/src/ddUtils.tsx index d10144127..5f3c43d3f 100644 --- a/example/src/ddUtils.tsx +++ b/example/src/ddUtils.tsx @@ -6,7 +6,6 @@ import { SdkVerbosity, TrackingConsent, DatadogFlags, - type DatadogFlagsConfiguration, } from '@datadog/mobile-react-native'; import {APPLICATION_ID, CLIENT_TOKEN, ENVIRONMENT} from './ddCredentials'; @@ -27,11 +26,6 @@ export function getDatadogConfig(trackingConsent: TrackingConsent) { config.serviceName = "com.datadoghq.reactnative.sample" config.verbosity = SdkVerbosity.DEBUG; - const flagsConfiguration: DatadogFlagsConfiguration = { - enabled: true, - } - config.flagsConfiguration = flagsConfiguration - return config } @@ -58,16 +52,11 @@ export function initializeDatadog(trackingConsent: TrackingConsent) { config.serviceName = "com.datadoghq.reactnative.sample" config.verbosity = SdkVerbosity.DEBUG; - const flagsConfiguration: DatadogFlagsConfiguration = { - enabled: true, - } - config.flagsConfiguration = flagsConfiguration - DdSdkReactNative.initialize(config).then(() => { DdLogs.info('The RN Sdk was properly initialized') DdSdkReactNative.setUserInfo({id: "1337", name: "Xavier", email: "xg@example.com", extraInfo: { type: "premium" } }) DdSdkReactNative.addAttributes({campaign: "ad-network"}) }); - DatadogFlags.enable(flagsConfiguration) + DatadogFlags.enable() } diff --git a/example/src/screens/MainScreen.tsx b/example/src/screens/MainScreen.tsx index 5ac7cddb8..b66caa2dc 100644 --- a/example/src/screens/MainScreen.tsx +++ b/example/src/screens/MainScreen.tsx @@ -110,6 +110,8 @@ export default class MainScreen extends Component { fetchBooleanFlag() { (async () => { + await DatadogFlags.enable(); + const flagsClient = DatadogFlags.getClient(); await flagsClient.setEvaluationContext({ targetingKey: 'test-user-1', diff --git a/packages/core/ios/Sources/DdSdkNativeInitialization.swift b/packages/core/ios/Sources/DdSdkNativeInitialization.swift index a05c7b0a0..bf39f5e97 100644 --- a/packages/core/ios/Sources/DdSdkNativeInitialization.swift +++ b/packages/core/ios/Sources/DdSdkNativeInitialization.swift @@ -6,7 +6,6 @@ import Foundation import DatadogCore -import DatadogFlags import DatadogRUM import DatadogLogs import DatadogTrace @@ -88,10 +87,6 @@ public class DdSdkNativeInitialization: NSObject { let traceConfig = buildTraceConfiguration(configuration: sdkConfiguration) Trace.enable(with: traceConfig) - if let configurationForFlags = sdkConfiguration.configurationForFlags { - Flags.enable(with: configurationForFlags) - } - if sdkConfiguration.nativeCrashReportEnabled ?? false { CrashReporting.enable() } diff --git a/packages/core/src/DdSdkReactNative.tsx b/packages/core/src/DdSdkReactNative.tsx index 9865a0f37..b1ea4f4c1 100644 --- a/packages/core/src/DdSdkReactNative.tsx +++ b/packages/core/src/DdSdkReactNative.tsx @@ -24,7 +24,6 @@ import { import { InternalLog } from './InternalLog'; import { SdkVerbosity } from './SdkVerbosity'; import type { TrackingConsent } from './TrackingConsent'; -import { DatadogFlags } from './flags/DatadogFlags'; import { DdLogs } from './logs/DdLogs'; import { DdRum } from './rum/DdRum'; import { DdRumErrorTracking } from './rum/instrumentation/DdRumErrorTracking'; @@ -99,13 +98,6 @@ export class DdSdkReactNative { DdSdkReactNative.buildConfiguration(configuration, params) ); - if ( - configuration.flagsConfiguration && - configuration.flagsConfiguration.enabled !== false - ) { - await DatadogFlags.enable(configuration.flagsConfiguration); - } - InternalLog.log('Datadog SDK was initialized', SdkVerbosity.INFO); GlobalState.instance.isInitialized = true; BufferSingleton.onInitialization(); @@ -406,8 +398,7 @@ export class DdSdkReactNative { configuration.trackWatchdogTerminations, configuration.batchProcessingLevel, configuration.initialResourceThreshold, - configuration.trackMemoryWarnings, - configuration.flagsConfiguration + configuration.trackMemoryWarnings ); }; diff --git a/packages/core/src/DdSdkReactNativeConfiguration.tsx b/packages/core/src/DdSdkReactNativeConfiguration.tsx index 94db29814..4ec1ef883 100644 --- a/packages/core/src/DdSdkReactNativeConfiguration.tsx +++ b/packages/core/src/DdSdkReactNativeConfiguration.tsx @@ -7,7 +7,6 @@ import type { ProxyConfiguration } from './ProxyConfiguration'; import type { SdkVerbosity } from './SdkVerbosity'; import { TrackingConsent } from './TrackingConsent'; -import type { DatadogFlagsConfiguration } from './flags/types'; import type { ActionEventMapper } from './rum/eventMappers/actionEventMapper'; import type { ErrorEventMapper } from './rum/eventMappers/errorEventMapper'; import type { ResourceEventMapper } from './rum/eventMappers/resourceEventMapper'; @@ -370,8 +369,6 @@ export class DdSdkReactNativeConfiguration { public customEndpoints: CustomEndpoints = DEFAULTS.getCustomEndpoints(); - public flagsConfiguration?: DatadogFlagsConfiguration; - constructor( readonly clientToken: string, readonly env: string, @@ -417,7 +414,6 @@ export type AutoInstrumentationParameters = { readonly actionEventMapper: ActionEventMapper | null; readonly useAccessibilityLabel: boolean; readonly actionNameAttribute?: string; - readonly flagsConfiguration?: DatadogFlagsConfiguration; }; /** @@ -485,7 +481,6 @@ export type PartialInitializationConfiguration = { readonly batchProcessingLevel?: BatchProcessingLevel; readonly initialResourceThreshold?: number; readonly trackMemoryWarnings?: boolean; - readonly flagsConfiguration?: DatadogFlagsConfiguration; }; const setConfigurationAttribute = < diff --git a/packages/core/src/types.tsx b/packages/core/src/types.tsx index 9e5c7ae3f..5c7d64cec 100644 --- a/packages/core/src/types.tsx +++ b/packages/core/src/types.tsx @@ -5,7 +5,6 @@ */ import type { BatchProcessingLevel } from './DdSdkReactNativeConfiguration'; -import type { DatadogFlagsConfiguration } from './flags/types'; declare global { // eslint-disable-next-line no-var, vars-on-top @@ -71,8 +70,7 @@ 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 configurationForFlags: DatadogFlagsConfiguration | undefined + readonly trackMemoryWarnings: boolean ) {} } From eb788adc49fa165641ba2297a7f99a96bf9fa950 Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Mon, 1 Dec 2025 14:23:12 +0200 Subject: [PATCH 125/526] Remove stale tests --- .../src/__tests__/DdSdkReactNative.test.tsx | 106 ------------------ 1 file changed, 106 deletions(-) diff --git a/packages/core/src/__tests__/DdSdkReactNative.test.tsx b/packages/core/src/__tests__/DdSdkReactNative.test.tsx index 6ce8a05d6..810a5bd9d 100644 --- a/packages/core/src/__tests__/DdSdkReactNative.test.tsx +++ b/packages/core/src/__tests__/DdSdkReactNative.test.tsx @@ -84,9 +84,6 @@ beforeEach(async () => { (DdRumErrorTracking.startTracking as jest.MockedFunction< typeof DdRumErrorTracking.startTracking >).mockClear(); - (DatadogFlags.enable as jest.MockedFunction< - typeof DatadogFlags.enable - >).mockClear(); DdLogs.unregisterLogEventMapper(); UserInfoSingleton.reset(); @@ -519,109 +516,6 @@ describe('DdSdkReactNative', () => { }) ); }); - - it('does not enable DatadogFlags when flagsConfiguration is not provided', async () => { - // GIVEN - const fakeAppId = '1'; - const fakeClientToken = '2'; - const fakeEnvName = 'env'; - const configuration = new DdSdkReactNativeConfiguration( - fakeClientToken, - fakeEnvName, - fakeAppId - ); - - NativeModules.DdSdk.initialize.mockResolvedValue(null); - - // WHEN - await DdSdkReactNative.initialize(configuration); - - // THEN - expect(DatadogFlags.enable).not.toHaveBeenCalled(); - }); - - it('enables DatadogFlags when flagsConfiguration is provided', async () => { - // GIVEN - const fakeAppId = '1'; - const fakeClientToken = '2'; - const fakeEnvName = 'env'; - const configuration = new DdSdkReactNativeConfiguration( - fakeClientToken, - fakeEnvName, - fakeAppId - ); - configuration.flagsConfiguration = { - enabled: true - }; - - NativeModules.DdSdk.initialize.mockResolvedValue(null); - - // WHEN - await DdSdkReactNative.initialize(configuration); - - // THEN - expect(DatadogFlags.enable).toHaveBeenCalledTimes(1); - expect(DatadogFlags.enable).toHaveBeenCalledWith({ - enabled: true - }); - }); - - it('enables DatadogFlags with custom configuration when provided', async () => { - // GIVEN - const fakeAppId = '1'; - const fakeClientToken = '2'; - const fakeEnvName = 'env'; - const customFlagsEndpoint = 'https://flags.example.com'; - const customFlagsHeaders = { - Authorization: 'Bearer token123' - }; - const configuration = new DdSdkReactNativeConfiguration( - fakeClientToken, - fakeEnvName, - fakeAppId - ); - configuration.flagsConfiguration = { - enabled: true, - customFlagsEndpoint, - customFlagsHeaders - }; - - NativeModules.DdSdk.initialize.mockResolvedValue(null); - - // WHEN - await DdSdkReactNative.initialize(configuration); - - // THEN - expect(DatadogFlags.enable).toHaveBeenCalledTimes(1); - expect(DatadogFlags.enable).toHaveBeenCalledWith({ - enabled: true, - customFlagsEndpoint, - customFlagsHeaders - }); - }); - - it('does not call DatadogFlags.enable when flagsConfiguration.enabled is false', async () => { - // GIVEN - const fakeAppId = '1'; - const fakeClientToken = '2'; - const fakeEnvName = 'env'; - const configuration = new DdSdkReactNativeConfiguration( - fakeClientToken, - fakeEnvName, - fakeAppId - ); - configuration.flagsConfiguration = { - enabled: false - }; - - NativeModules.DdSdk.initialize.mockResolvedValue(null); - - // WHEN - await DdSdkReactNative.initialize(configuration); - - // THEN - expect(DatadogFlags.enable).not.toHaveBeenCalled(); - }); }); describe('feature enablement', () => { From 7ec774ed49a7d059279940ee026e3548f96a49bc Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Mon, 1 Dec 2025 15:55:09 +0200 Subject: [PATCH 126/526] Finish removal of unnecessary Flags code in global SDK init code --- .../core/ios/Sources/DdSdkConfiguration.swift | 5 --- .../ios/Sources/RNDdSdkConfiguration.swift | 8 ++--- packages/core/ios/Tests/DdSdkTests.swift | 36 ++----------------- .../src/__tests__/DdSdkReactNative.test.tsx | 1 - .../__tests__/initialization.test.tsx | 1 - 5 files changed, 4 insertions(+), 47 deletions(-) diff --git a/packages/core/ios/Sources/DdSdkConfiguration.swift b/packages/core/ios/Sources/DdSdkConfiguration.swift index 8f852834e..ffbc2199d 100644 --- a/packages/core/ios/Sources/DdSdkConfiguration.swift +++ b/packages/core/ios/Sources/DdSdkConfiguration.swift @@ -6,7 +6,6 @@ import Foundation import DatadogCore -import DatadogFlags import DatadogInternal import DatadogRUM @@ -43,7 +42,6 @@ import DatadogRUM - trackWatchdogTerminations: Whether the SDK should track application termination by the watchdog - batchProcessingLevel: Maximum number of batches processed sequentially without a delay - initialResourceThreshold: The amount of time after a view starts where a Resource should be considered when calculating Time to Network-Settled (TNS) - - configurationForFlags: Configuration for the feature flags feature. */ @objc(DdSdkConfiguration) public class DdSdkConfiguration: NSObject { @@ -79,7 +77,6 @@ public class DdSdkConfiguration: NSObject { public var batchProcessingLevel: Datadog.Configuration.BatchProcessingLevel public var initialResourceThreshold: Double? = nil public var trackMemoryWarnings: Bool - public var configurationForFlags: Flags.Configuration? = nil public init( clientToken: String, @@ -114,7 +111,6 @@ public class DdSdkConfiguration: NSObject { batchProcessingLevel: Datadog.Configuration.BatchProcessingLevel, initialResourceThreshold: Double?, trackMemoryWarnings: Bool = true, - configurationForFlags: Flags.Configuration? ) { self.clientToken = clientToken self.env = env @@ -148,7 +144,6 @@ public class DdSdkConfiguration: NSObject { self.batchProcessingLevel = batchProcessingLevel self.initialResourceThreshold = initialResourceThreshold self.trackMemoryWarnings = trackMemoryWarnings - self.configurationForFlags = configurationForFlags } } diff --git a/packages/core/ios/Sources/RNDdSdkConfiguration.swift b/packages/core/ios/Sources/RNDdSdkConfiguration.swift index 464161c3f..f9b05aed0 100644 --- a/packages/core/ios/Sources/RNDdSdkConfiguration.swift +++ b/packages/core/ios/Sources/RNDdSdkConfiguration.swift @@ -45,7 +45,6 @@ extension NSDictionary { let batchProcessingLevel = object(forKey: "batchProcessingLevel") as? NSString let initialResourceThreshold = object(forKey: "initialResourceThreshold") as? Double let trackMemoryWarnings = object(forKey: "trackMemoryWarnings") as? Bool - let configurationForFlags = object(forKey: "configurationForFlags") as? NSDictionary return DdSdkConfiguration( clientToken: (clientToken != nil) ? clientToken! : String(), @@ -79,8 +78,7 @@ extension NSDictionary { trackWatchdogTerminations: trackWatchdogTerminations ?? DefaultConfiguration.trackWatchdogTerminations, batchProcessingLevel: batchProcessingLevel.asBatchProcessingLevel(), initialResourceThreshold: initialResourceThreshold, - trackMemoryWarnings: trackMemoryWarnings ?? DefaultConfiguration.trackMemoryWarnings, - configurationForFlags: configurationForFlags?.asConfigurationForFlags() + trackMemoryWarnings: trackMemoryWarnings ?? DefaultConfiguration.trackMemoryWarnings ) } @@ -284,7 +282,6 @@ extension Dictionary where Key == String, Value == AnyObject { let batchProcessingLevel = configuration["batchProcessingLevel"] as? NSString let initialResourceThreshold = configuration["initialResourceThreshold"] as? Double let trackMemoryWarnings = configuration["trackMemoryWarnings"] as? Bool - let configurationForFlags = configuration["configurationForFlags"] as? NSDictionary return DdSdkConfiguration( clientToken: clientToken ?? String(), @@ -321,8 +318,7 @@ extension Dictionary where Key == String, Value == AnyObject { trackWatchdogTerminations: trackWatchdogTerminations ?? DefaultConfiguration.trackWatchdogTerminations, batchProcessingLevel: batchProcessingLevel.asBatchProcessingLevel(), initialResourceThreshold: initialResourceThreshold, - trackMemoryWarnings: trackMemoryWarnings ?? DefaultConfiguration.trackMemoryWarnings, - configurationForFlags: configurationForFlags?.asConfigurationForFlags() + trackMemoryWarnings: trackMemoryWarnings ?? DefaultConfiguration.trackMemoryWarnings ) } } diff --git a/packages/core/ios/Tests/DdSdkTests.swift b/packages/core/ios/Tests/DdSdkTests.swift index e3c4334b3..4a5d13f2e 100644 --- a/packages/core/ios/Tests/DdSdkTests.swift +++ b/packages/core/ios/Tests/DdSdkTests.swift @@ -8,7 +8,6 @@ import XCTest @testable import DatadogCore @testable import DatadogCrashReporting -@testable import DatadogFlags @testable import DatadogInternal @testable import DatadogLogs @testable import DatadogRUM @@ -310,35 +309,6 @@ class DdSdkTests: XCTestCase { XCTAssertNotNil(core.features[LogsFeature.name]) XCTAssertNotNil(core.features[TraceFeature.name]) } - - func testFlagsFeatureDisabledByDefault() { - let core = MockDatadogCore() - CoreRegistry.register(default: core) - defer { CoreRegistry.unregisterDefault() } - - let configuration: DdSdkConfiguration = .mockAny(configurationForFlags: nil) - - DdSdkNativeInitialization().enableFeatures( - sdkConfiguration: configuration - ) - - // Flagging SDK is disabled by default if no configuration is provided. - XCTAssertNil(core.features[FlagsFeature.name]) - } - - func testEnableFeatureFlags() { - let core = MockDatadogCore() - CoreRegistry.register(default: core) - defer { CoreRegistry.unregisterDefault() } - - let configuration: DdSdkConfiguration = .mockAny(configurationForFlags: ["enabled":true]) - - DdSdkNativeInitialization().enableFeatures( - sdkConfiguration: configuration - ) - - XCTAssertNotNil(core.features[FlagsFeature.name]) - } func testBuildConfigurationDefaultEndpoint() { let configuration: DdSdkConfiguration = .mockAny() @@ -1664,8 +1634,7 @@ extension DdSdkConfiguration { appHangThreshold: Double? = nil, trackWatchdogTerminations: Bool = false, batchProcessingLevel: NSString? = "MEDIUM", - initialResourceThreshold: Double? = nil, - configurationForFlags: NSDictionary? = nil + initialResourceThreshold: Double? = nil ) -> DdSdkConfiguration { DdSdkConfiguration( clientToken: clientToken as String, @@ -1698,8 +1667,7 @@ extension DdSdkConfiguration { appHangThreshold: appHangThreshold, trackWatchdogTerminations: trackWatchdogTerminations, batchProcessingLevel: batchProcessingLevel.asBatchProcessingLevel(), - initialResourceThreshold: initialResourceThreshold, - configurationForFlags: configurationForFlags?.asConfigurationForFlags() + initialResourceThreshold: initialResourceThreshold ) } } diff --git a/packages/core/src/__tests__/DdSdkReactNative.test.tsx b/packages/core/src/__tests__/DdSdkReactNative.test.tsx index 810a5bd9d..74c2637b6 100644 --- a/packages/core/src/__tests__/DdSdkReactNative.test.tsx +++ b/packages/core/src/__tests__/DdSdkReactNative.test.tsx @@ -12,7 +12,6 @@ import { DdSdkReactNative } from '../DdSdkReactNative'; import { ProxyConfiguration, ProxyType } from '../ProxyConfiguration'; import { SdkVerbosity } from '../SdkVerbosity'; import { TrackingConsent } from '../TrackingConsent'; -import { DatadogFlags } from '../flags/DatadogFlags'; import { DdLogs } from '../logs/DdLogs'; import { DdRum } from '../rum/DdRum'; import { DdRumErrorTracking } from '../rum/instrumentation/DdRumErrorTracking'; diff --git a/packages/core/src/sdk/DatadogProvider/__tests__/initialization.test.tsx b/packages/core/src/sdk/DatadogProvider/__tests__/initialization.test.tsx index e896a9bc2..8ace4b731 100644 --- a/packages/core/src/sdk/DatadogProvider/__tests__/initialization.test.tsx +++ b/packages/core/src/sdk/DatadogProvider/__tests__/initialization.test.tsx @@ -75,7 +75,6 @@ describe('DatadogProvider', () => { "bundleLogsWithRum": true, "bundleLogsWithTraces": true, "clientToken": "fakeToken", - "configurationForFlags": undefined, "configurationForTelemetry": { "initializationType": "SYNC", "reactNativeVersion": "0.76.9", From 3d48d77a851f83a4bcd885fcf571f29e4135db18 Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Mon, 1 Dec 2025 16:39:38 +0200 Subject: [PATCH 127/526] Remove unnecessary comma --- packages/core/ios/Sources/DdSdkConfiguration.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/ios/Sources/DdSdkConfiguration.swift b/packages/core/ios/Sources/DdSdkConfiguration.swift index ffbc2199d..0b1e6222f 100644 --- a/packages/core/ios/Sources/DdSdkConfiguration.swift +++ b/packages/core/ios/Sources/DdSdkConfiguration.swift @@ -110,7 +110,7 @@ public class DdSdkConfiguration: NSObject { trackWatchdogTerminations: Bool, batchProcessingLevel: Datadog.Configuration.BatchProcessingLevel, initialResourceThreshold: Double?, - trackMemoryWarnings: Bool = true, + trackMemoryWarnings: Bool = true ) { self.clientToken = clientToken self.env = env From 14799f235c3b6ca11cc0d16999785df8f2a456b2 Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Mon, 1 Dec 2025 18:44:12 +0200 Subject: [PATCH 128/526] Remove leftovers for global init --- example-new-architecture/App.tsx | 3 - .../DdSdkReactNativeConfiguration.test.ts | 222 ------------------ 2 files changed, 225 deletions(-) diff --git a/example-new-architecture/App.tsx b/example-new-architecture/App.tsx index f0f4434dd..5e921c234 100644 --- a/example-new-architecture/App.tsx +++ b/example-new-architecture/App.tsx @@ -46,9 +46,6 @@ import {APPLICATION_ID, CLIENT_TOKEN, ENVIRONMENT} from './ddCredentials'; config.telemetrySampleRate = 100; config.uploadFrequency = UploadFrequency.FREQUENT; config.batchSize = BatchSize.SMALL; - config.flagsConfiguration = { - enabled: true, - }; await DdSdkReactNative.initialize(config); await DdRum.startView('main', 'Main'); setTimeout(async () => { diff --git a/packages/core/src/__tests__/DdSdkReactNativeConfiguration.test.ts b/packages/core/src/__tests__/DdSdkReactNativeConfiguration.test.ts index 8193621c7..60a9d13ff 100644 --- a/packages/core/src/__tests__/DdSdkReactNativeConfiguration.test.ts +++ b/packages/core/src/__tests__/DdSdkReactNativeConfiguration.test.ts @@ -261,227 +261,5 @@ describe('DdSdkReactNativeConfiguration', () => { } `); }); - - it('builds the SDK configuration with flags configuration enabled', () => { - expect( - buildConfigurationFromPartialConfiguration( - { - trackErrors: false, - trackInteractions: false, - trackResources: false - }, - { - applicationId: 'fake-app-id', - clientToken: 'fake-client-token', - env: 'fake-env', - flagsConfiguration: { - enabled: true, - customFlagsEndpoint: 'https://flags.example.com', - customFlagsHeaders: { - Authorization: 'Bearer token123', - 'X-Custom-Header': 'custom-value' - } - } - } - ) - ).toMatchInlineSnapshot(` - DdSdkReactNativeConfiguration { - "actionEventMapper": null, - "additionalConfiguration": {}, - "applicationId": "fake-app-id", - "batchProcessingLevel": "MEDIUM", - "batchSize": "MEDIUM", - "bundleLogsWithRum": true, - "bundleLogsWithTraces": true, - "clientToken": "fake-client-token", - "customEndpoints": {}, - "env": "fake-env", - "errorEventMapper": null, - "firstPartyHosts": [], - "flagsConfiguration": { - "customFlagsEndpoint": "https://flags.example.com", - "customFlagsHeaders": { - "Authorization": "Bearer token123", - "X-Custom-Header": "custom-value", - }, - "enabled": true, - }, - "logEventMapper": null, - "longTaskThresholdMs": 0, - "nativeCrashReportEnabled": false, - "nativeInteractionTracking": false, - "nativeLongTaskThresholdMs": 200, - "nativeViewTracking": false, - "proxyConfig": undefined, - "resourceEventMapper": null, - "resourceTracingSamplingRate": 100, - "serviceName": undefined, - "sessionSamplingRate": 100, - "site": "US1", - "telemetrySampleRate": 20, - "trackBackgroundEvents": false, - "trackErrors": false, - "trackFrustrations": true, - "trackInteractions": false, - "trackMemoryWarnings": true, - "trackResources": false, - "trackWatchdogTerminations": false, - "trackingConsent": "granted", - "uploadFrequency": "AVERAGE", - "useAccessibilityLabel": true, - "verbosity": undefined, - "vitalsUpdateFrequency": "AVERAGE", - } - `); - }); - - it('builds the SDK configuration with flags configuration disabled', () => { - expect( - buildConfigurationFromPartialConfiguration( - { - trackErrors: false, - trackInteractions: false, - trackResources: false - }, - { - applicationId: 'fake-app-id', - clientToken: 'fake-client-token', - env: 'fake-env', - flagsConfiguration: { - enabled: false - } - } - ) - ).toMatchInlineSnapshot(` - DdSdkReactNativeConfiguration { - "actionEventMapper": null, - "additionalConfiguration": {}, - "applicationId": "fake-app-id", - "batchProcessingLevel": "MEDIUM", - "batchSize": "MEDIUM", - "bundleLogsWithRum": true, - "bundleLogsWithTraces": true, - "clientToken": "fake-client-token", - "customEndpoints": {}, - "env": "fake-env", - "errorEventMapper": null, - "firstPartyHosts": [], - "flagsConfiguration": { - "enabled": false, - }, - "logEventMapper": null, - "longTaskThresholdMs": 0, - "nativeCrashReportEnabled": false, - "nativeInteractionTracking": false, - "nativeLongTaskThresholdMs": 200, - "nativeViewTracking": false, - "proxyConfig": undefined, - "resourceEventMapper": null, - "resourceTracingSamplingRate": 100, - "serviceName": undefined, - "sessionSamplingRate": 100, - "site": "US1", - "telemetrySampleRate": 20, - "trackBackgroundEvents": false, - "trackErrors": false, - "trackFrustrations": true, - "trackInteractions": false, - "trackMemoryWarnings": true, - "trackResources": false, - "trackWatchdogTerminations": false, - "trackingConsent": "granted", - "uploadFrequency": "AVERAGE", - "useAccessibilityLabel": true, - "verbosity": undefined, - "vitalsUpdateFrequency": "AVERAGE", - } - `); - }); - - it('builds the SDK configuration with full flags configuration', () => { - expect( - buildConfigurationFromPartialConfiguration( - { - trackErrors: true, - trackInteractions: true, - trackResources: true, - firstPartyHosts: ['api.com'], - resourceTracingSamplingRate: 80 - }, - { - applicationId: 'fake-app-id', - clientToken: 'fake-client-token', - env: 'fake-env', - sessionSamplingRate: 80, - site: 'EU', - verbosity: SdkVerbosity.DEBUG, - serviceName: 'com.test.app', - version: '1.4.5', - flagsConfiguration: { - enabled: true, - customFlagsEndpoint: 'https://flags.example.com', - customFlagsHeaders: { - Authorization: 'Bearer token123' - }, - customExposureEndpoint: - 'https://exposure.example.com', - trackExposures: true - } - } - ) - ).toMatchInlineSnapshot(` - DdSdkReactNativeConfiguration { - "actionEventMapper": null, - "additionalConfiguration": {}, - "applicationId": "fake-app-id", - "batchProcessingLevel": "MEDIUM", - "batchSize": "MEDIUM", - "bundleLogsWithRum": true, - "bundleLogsWithTraces": true, - "clientToken": "fake-client-token", - "customEndpoints": {}, - "env": "fake-env", - "errorEventMapper": null, - "firstPartyHosts": [ - "api.com", - ], - "flagsConfiguration": { - "customExposureEndpoint": "https://exposure.example.com", - "customFlagsEndpoint": "https://flags.example.com", - "customFlagsHeaders": { - "Authorization": "Bearer token123", - }, - "enabled": true, - "trackExposures": true, - }, - "logEventMapper": null, - "longTaskThresholdMs": 0, - "nativeCrashReportEnabled": false, - "nativeInteractionTracking": false, - "nativeLongTaskThresholdMs": 200, - "nativeViewTracking": false, - "proxyConfig": undefined, - "resourceEventMapper": null, - "resourceTracingSamplingRate": 80, - "serviceName": "com.test.app", - "sessionSamplingRate": 80, - "site": "EU", - "telemetrySampleRate": 20, - "trackBackgroundEvents": false, - "trackErrors": true, - "trackFrustrations": true, - "trackInteractions": true, - "trackMemoryWarnings": true, - "trackResources": true, - "trackWatchdogTerminations": false, - "trackingConsent": "granted", - "uploadFrequency": "AVERAGE", - "useAccessibilityLabel": true, - "verbosity": "debug", - "version": "1.4.5", - "vitalsUpdateFrequency": "AVERAGE", - } - `); - }); }); }); From 040c681ac1fde41fe17aec15ac63a6e7045ee60b Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Mon, 1 Dec 2025 19:43:48 +0200 Subject: [PATCH 129/526] Bump datadog SDK package versions, add flags SDK --- packages/core/android/build.gradle | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/core/android/build.gradle b/packages/core/android/build.gradle index 1344b2531..5da0e969e 100644 --- a/packages/core/android/build.gradle +++ b/packages/core/android/build.gradle @@ -195,10 +195,11 @@ 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.datadoghq:dd-sdk-android-flags: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" From c45b654a33a56509780ae644f25261f2360f44da Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Tue, 2 Dec 2025 16:29:22 +0200 Subject: [PATCH 130/526] Initial Android implementation --- .../reactnative/DdFlagsImplementation.kt | 251 ++++++++++++++++++ .../reactnative/DdSdkReactNativePackage.kt | 4 +- .../kotlin/com/datadog/reactnative/DdFlags.kt | 109 ++++++++ .../kotlin/com/datadog/reactnative/DdFlags.kt | 100 +++++++ packages/core/src/flags/types.ts | 7 + 5 files changed, 470 insertions(+), 1 deletion(-) create mode 100644 packages/core/android/src/main/kotlin/com/datadog/reactnative/DdFlagsImplementation.kt create mode 100644 packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdFlags.kt create mode 100644 packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdFlags.kt diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdFlagsImplementation.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdFlagsImplementation.kt new file mode 100644 index 000000000..ec301dfbd --- /dev/null +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdFlagsImplementation.kt @@ -0,0 +1,251 @@ +/* + * 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.api.InternalLogger +import com.datadog.android.api.SdkCore +import com.datadog.android.flags.Flags +import com.datadog.android.flags.FlagsClient +import com.datadog.android.flags.FlagsConfiguration +import com.datadog.android.flags.model.EvaluationContext +import com.datadog.android.flags.model.ErrorCode +import com.datadog.android.flags.model.ResolutionDetails +import com.facebook.react.bridge.Promise +import com.facebook.react.bridge.ReadableMap +import com.facebook.react.bridge.WritableMap +import com.facebook.react.bridge.WritableNativeMap +import org.json.JSONObject + +class DdFlagsImplementation(private val sdkCore: SdkCore = Datadog.getInstance()) { + + private val clients: MutableMap = mutableMapOf() + + /** + * Enable the Flags feature with the provided configuration. + * @param configuration The configuration for Flags. + */ + fun enable(configuration: ReadableMap, promise: Promise) { + val flagsConfig = configuration.asFlagsConfiguration() + if (flagsConfig != null) { + Flags.enable(flagsConfig, sdkCore) + } else { + InternalLogger.UNBOUND.log( + InternalLogger.Level.ERROR, + InternalLogger.Target.USER, + { "Invalid configuration provided for Flags. Feature initialization skipped." } + ) + } + promise.resolve(null) + } + + /** + * Retrieve or create a FlagsClient instance. + * + * Caches clients by name to avoid repeated Builder().build() calls. + * On hot reload, the cache is cleared and clients are recreated - this is safe + * because gracefulModeEnabled=true prevents crashes on duplicate creation. + */ + private fun getClient(name: String): FlagsClient { + return clients.getOrPut(name) { + FlagsClient.Builder(name, sdkCore).build() + } + } + + private fun parseAttributes(attributes: ReadableMap): Map { + val result = mutableMapOf() + val iterator = attributes.entryIterator + while (iterator.hasNext()) { + val entry = iterator.next() + // Convert all values to strings as required by Android SDK + result[entry.key] = entry.value?.toString() ?: "" + } + return result + } + + /** + * Set the evaluation context for a specific client. + * @param clientName The name of the client. + * @param targetingKey The targeting key. + * @param attributes The attributes for the evaluation context (will be converted to strings). + */ + fun setEvaluationContext( + clientName: String, + targetingKey: String, + attributes: ReadableMap, + promise: Promise + ) { + val client = getClient(clientName) + val parsedAttributes = parseAttributes(attributes) + val evaluationContext = EvaluationContext(targetingKey, parsedAttributes) + + client.setEvaluationContext(evaluationContext) + promise.resolve(null) + } + + /** + * Get details for a boolean flag. + * @param clientName The name of the client. + * @param key The flag key. + * @param defaultValue The default value. + */ + fun getBooleanDetails( + clientName: String, + key: String, + defaultValue: Boolean, + promise: Promise + ) { + val client = getClient(clientName) + val details = client.resolve(key, defaultValue) + promise.resolve(details.toReactNativeMap(key)) + } + + /** + * Get details for a string flag. + * @param clientName The name of the client. + * @param key The flag key. + * @param defaultValue The default value. + */ + fun getStringDetails(clientName: String, key: String, defaultValue: String, promise: Promise) { + val client = getClient(clientName) + val details = client.resolve(key, defaultValue) + promise.resolve(details.toReactNativeMap(key)) + } + + /** + * Get details for a number flag. Includes Number and Integer flags. + * @param clientName The name of the client. + * @param key The flag key. + * @param defaultValue The default value. + */ + fun getNumberDetails(clientName: String, key: String, defaultValue: Double, promise: Promise) { + val client = getClient(clientName) + + // Try as Double first. + val doubleDetails = client.resolve(key, defaultValue) + + // If type mismatch and value is an integer, try as Int. + if (doubleDetails.errorCode == ErrorCode.TYPE_MISMATCH) { + val safeInt = defaultValue.toInt() + val intDetails = client.resolve(key, safeInt) + + if (intDetails.errorCode == null) { + promise.resolve(intDetails.toReactNativeMap(key)) + return + } + } + + promise.resolve(doubleDetails.toReactNativeMap(key)) + } + + /** + * Get details for an object flag. + * @param clientName The name of the client. + * @param key The flag key. + * @param defaultValue The default value. + */ + fun getObjectDetails( + clientName: String, + key: String, + defaultValue: ReadableMap, + promise: Promise + ) { + val client = getClient(clientName) + val jsonDefaultValue = defaultValue.toJSONObject() + val details = client.resolve(key, jsonDefaultValue) + promise.resolve(details.toReactNativeMap(key)) + } + + internal companion object { + internal const val NAME = "DdFlags" + } +} + +/** Convert ResolutionDetails to React Native map format expected by the JS layer. */ +private fun ResolutionDetails.toReactNativeMap(key: String): WritableMap { + val map = WritableNativeMap() + map.putString("key", key) + + when (val v = value) { + is Boolean -> map.putBoolean("value", v) + is String -> map.putString("value", v) + is Int -> map.putDouble("value", v.toDouble()) // Convert to double for RN. + is Double -> map.putDouble("value", v) + is JSONObject -> map.putMap("value", v.toWritableMap()) + else -> map.putNull("value") + } + + variant?.let { map.putString("variant", it) } ?: map.putNull("variant") + reason?.let { map.putString("reason", it.name) } ?: map.putNull("reason") + errorCode?.let { map.putString("error", it.name) } ?: map.putNull("error") + + return map +} + +/** Convert ReadableMap to JSONObject for flag default values. */ +private fun ReadableMap.toJSONObject(): JSONObject { + val json = JSONObject() + val iterator = entryIterator + while (iterator.hasNext()) { + val entry = iterator.next() + json.put(entry.key, entry.value) + } + return json +} + +/** Convert JSONObject to WritableMap for React Native. */ +private fun JSONObject.toWritableMap(): WritableMap { + val map = WritableNativeMap() + val keys = keys() + while (keys.hasNext()) { + val key = keys.next() + when (val value = get(key)) { + is Boolean -> map.putBoolean(key, value) + is Int -> map.putInt(key, value) + is Double -> map.putDouble(key, value) + is String -> map.putString(key, value) + is JSONObject -> map.putMap(key, value.toWritableMap()) + JSONObject.NULL -> map.putNull(key) + else -> map.putNull(key) + } + } + return map +} + +/** Parse configuration from ReadableMap to FlagsConfiguration. */ +private fun ReadableMap.asFlagsConfiguration(): FlagsConfiguration? { + val enabled = if (hasKey("enabled")) getBoolean("enabled") else false + + if (!enabled) { + return null + } + + // Hard set `gracefulModeEnabled` to `true` because SDK misconfigurations are handled on JS side. + // This prevents crashes on hot reload when clients are recreated. + val gracefulModeEnabled = true + + val trackExposures = if (hasKey("trackExposures")) getBoolean("trackExposures") else true + val rumIntegrationEnabled = + if (hasKey("rumIntegrationEnabled")) getBoolean("rumIntegrationEnabled") else true + + return FlagsConfiguration.Builder() + .apply { + gracefulModeEnabled(gracefulModeEnabled) + trackExposures(trackExposures) + rumIntegrationEnabled(rumIntegrationEnabled) + + // The SDK automatically appends endpoint names to the custom endpoints. + // The input config expects a base URL rather than a full URL. + if (hasKey("customFlagsEndpoint")) { + getString("customFlagsEndpoint")?.let { useCustomFlagEndpoint("$it/precompute-assignments") } + } + if (hasKey("customExposureEndpoint")) { + getString("customExposureEndpoint")?.let { useCustomExposureEndpoint("$it/api/v2/exposures") } + } + } + .build() +} 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 3a5b022c1..98ffa83db 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 @@ -25,6 +25,7 @@ class DdSdkReactNativePackage : TurboReactPackage() { DdRumImplementation.NAME -> DdRum(reactContext, sdkWrapper) DdTraceImplementation.NAME -> DdTrace(reactContext) DdLogsImplementation.NAME -> DdLogs(reactContext, sdkWrapper) + DdFlagsImplementation.NAME -> DdFlags(reactContext) else -> null } } @@ -36,7 +37,8 @@ class DdSdkReactNativePackage : TurboReactPackage() { DdSdkImplementation.NAME, DdRumImplementation.NAME, DdTraceImplementation.NAME, - DdLogsImplementation.NAME + DdLogsImplementation.NAME, + DdFlagsImplementation.NAME ).associateWith { ReactModuleInfo( it, diff --git a/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdFlags.kt b/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdFlags.kt new file mode 100644 index 000000000..e5a3672c2 --- /dev/null +++ b/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdFlags.kt @@ -0,0 +1,109 @@ +/* + * 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.facebook.react.bridge.Promise +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.ReactMethod +import com.facebook.react.bridge.ReadableMap + +/** The entry point to use Datadog's Flags feature. */ +class DdFlags(reactContext: ReactApplicationContext) : NativeDdFlagsSpec(reactContext) { + + private val implementation = DdFlagsImplementation() + + override fun getName(): String = DdFlagsImplementation.NAME + + /** + * Enable the Flags feature with the provided configuration. + * @param configuration The configuration for Flags. + */ + @ReactMethod + override fun enable(configuration: ReadableMap, promise: Promise) { + implementation.enable(configuration, promise) + } + + /** + * Set the evaluation context for a specific client. + * @param clientName The name of the client. + * @param targetingKey The targeting key. + * @param attributes The attributes for the evaluation context. + */ + @ReactMethod + override fun setEvaluationContext( + clientName: String, + targetingKey: String, + attributes: ReadableMap, + promise: Promise + ) { + implementation.setEvaluationContext(clientName, targetingKey, attributes, promise) + } + + /** + * Get details for a boolean flag. + * @param clientName The name of the client. + * @param key The flag key. + * @param defaultValue The default value. + */ + @ReactMethod + override fun getBooleanDetails( + clientName: String, + key: String, + defaultValue: Boolean, + promise: Promise + ) { + implementation.getBooleanDetails(clientName, key, defaultValue, promise) + } + + /** + * Get details for a string flag. + * @param clientName The name of the client. + * @param key The flag key. + * @param defaultValue The default value. + */ + @ReactMethod + override fun getStringDetails( + clientName: String, + key: String, + defaultValue: String, + promise: Promise + ) { + implementation.getStringDetails(clientName, key, defaultValue, promise) + } + + /** + * Get details for a number flag. Includes Number and Integer flags. + * @param clientName The name of the client. + * @param key The flag key. + * @param defaultValue The default value. + */ + @ReactMethod + override fun getNumberDetails( + clientName: String, + key: String, + defaultValue: Double, + promise: Promise + ) { + implementation.getNumberDetails(clientName, key, defaultValue, promise) + } + + /** + * Get details for an object flag. + * @param clientName The name of the client. + * @param key The flag key. + * @param defaultValue The default value. + */ + @ReactMethod + override fun getObjectDetails( + clientName: String, + key: String, + defaultValue: ReadableMap, + promise: Promise + ) { + implementation.getObjectDetails(clientName, key, defaultValue, promise) + } +} diff --git a/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdFlags.kt b/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdFlags.kt new file mode 100644 index 000000000..8eaaae160 --- /dev/null +++ b/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdFlags.kt @@ -0,0 +1,100 @@ +/* + * 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.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.ReadableMap + +/** The entry point to use Datadog's Flags feature. */ +class DdFlags(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) { + + private val implementation = DdFlagsImplementation() + + override fun getName(): String = DdFlagsImplementation.NAME + + /** + * Enable the Flags feature with the provided configuration. + * @param configuration The configuration for Flags. + */ + @ReactMethod + fun enable(configuration: ReadableMap, promise: Promise) { + implementation.enable(configuration, promise) + } + + /** + * Set the evaluation context for a specific client. + * @param clientName The name of the client. + * @param targetingKey The targeting key. + * @param attributes The attributes for the evaluation context. + */ + @ReactMethod + fun setEvaluationContext( + clientName: String, + targetingKey: String, + attributes: ReadableMap, + promise: Promise + ) { + implementation.setEvaluationContext(clientName, targetingKey, attributes, promise) + } + + /** + * Get details for a boolean flag. + * @param clientName The name of the client. + * @param key The flag key. + * @param defaultValue The default value. + */ + @ReactMethod + fun getBooleanDetails( + clientName: String, + key: String, + defaultValue: Boolean, + promise: Promise + ) { + implementation.getBooleanDetails(clientName, key, defaultValue, promise) + } + + /** + * Get details for a string flag. + * @param clientName The name of the client. + * @param key The flag key. + * @param defaultValue The default value. + */ + @ReactMethod + fun getStringDetails(clientName: String, key: String, defaultValue: String, promise: Promise) { + implementation.getStringDetails(clientName, key, defaultValue, promise) + } + + /** + * Get details for a number flag. Includes Number and Integer flags. + * @param clientName The name of the client. + * @param key The flag key. + * @param defaultValue The default value. + */ + @ReactMethod + fun getNumberDetails(clientName: String, key: String, defaultValue: Double, promise: Promise) { + implementation.getNumberDetails(clientName, key, defaultValue, promise) + } + + /** + * Get details for an object flag. + * @param clientName The name of the client. + * @param key The flag key. + * @param defaultValue The default value. + */ + @ReactMethod + fun getObjectDetails( + clientName: String, + key: String, + defaultValue: ReadableMap, + promise: Promise + ) { + implementation.getObjectDetails(clientName, key, defaultValue, promise) + } +} diff --git a/packages/core/src/flags/types.ts b/packages/core/src/flags/types.ts index 43ff9aa7f..1d80083f3 100644 --- a/packages/core/src/flags/types.ts +++ b/packages/core/src/flags/types.ts @@ -68,6 +68,9 @@ export type DatadogFlagsConfiguration = { /** * Custom server URL for retrieving flag assignments. * + * The provided value should only include the base URL, and the endpoint will be appended automatically. + * For example, if you provide 'https://flags.example.com', the SDK will use 'https://flags.example.com/precompute-assignments'. + * * If not set, the SDK uses the default Datadog Flags endpoint for the configured site. * * @default undefined @@ -84,6 +87,9 @@ export type DatadogFlagsConfiguration = { /** * Custom server URL for sending Flags exposure data. * + * The provided value should only include the base URL, and the endpoint will be appended automatically. + * For example, if you provide 'https://flags.example.com', the SDK will use 'https://flags.example.com/api/v2/exposures'. + * * If not set, the SDK uses the default Datadog Flags exposure endpoint. * * @default undefined @@ -156,6 +162,7 @@ export interface EvaluationContext { export type FlagEvaluationError = | 'PROVIDER_NOT_READY' | 'FLAG_NOT_FOUND' + | 'PARSE_ERROR' | 'TYPE_MISMATCH'; /** From 7094828e02c2295ad7659c2c58d4bf08c07aa379 Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Wed, 3 Dec 2025 13:32:04 +0200 Subject: [PATCH 131/526] Remove `customFlagsHeaders` from iOS wrapper configuration --- packages/core/ios/Sources/RNDdSdkConfiguration.swift | 2 -- packages/core/src/flags/types.ts | 10 ++-------- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/packages/core/ios/Sources/RNDdSdkConfiguration.swift b/packages/core/ios/Sources/RNDdSdkConfiguration.swift index f9b05aed0..6ef79d7d0 100644 --- a/packages/core/ios/Sources/RNDdSdkConfiguration.swift +++ b/packages/core/ios/Sources/RNDdSdkConfiguration.swift @@ -110,7 +110,6 @@ extension NSDictionary { // Hard set `gracefulModeEnabled` to `true` because this misconfiguration is handled on JS side. let gracefulModeEnabled = true - let customFlagsHeaders = object(forKey: "customFlagsHeaders") as? [String: String] let trackExposures = object(forKey: "trackExposures") as? Bool let rumIntegrationEnabled = object(forKey: "rumIntegrationEnabled") as? Bool @@ -126,7 +125,6 @@ extension NSDictionary { return Flags.Configuration( gracefulModeEnabled: gracefulModeEnabled, customFlagsEndpoint: customFlagsEndpointURL, - customFlagsHeaders: customFlagsHeaders, customExposureEndpoint: customExposureEndpointURL, trackExposures: trackExposures ?? true, rumIntegrationEnabled: rumIntegrationEnabled ?? true diff --git a/packages/core/src/flags/types.ts b/packages/core/src/flags/types.ts index 1d80083f3..076546ae7 100644 --- a/packages/core/src/flags/types.ts +++ b/packages/core/src/flags/types.ts @@ -76,14 +76,6 @@ export type DatadogFlagsConfiguration = { * @default undefined */ customFlagsEndpoint?: string; - /** - * Additional HTTP headers to attach to requests made to `customFlagsEndpoint`. - * - * Useful for authentication or routing when using your own Flags service. Ignored when using the default Datadog endpoint. - * - * @default undefined - */ - customFlagsHeaders?: Record; /** * Custom server URL for sending Flags exposure data. * @@ -151,6 +143,8 @@ export interface EvaluationContext { * Attributes can include user properties, session data, or any other contextual information * needed for flag evaluation rules. */ + + // TODO: This should be a map of string to string because Android doesn't support other types attributes: Record; } From 54d1324f451f10e073c28789389da8dcc023de99 Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Wed, 3 Dec 2025 14:34:48 +0200 Subject: [PATCH 132/526] Update Flags SDK to evaluate feature flags synchronously --- example-new-architecture/App.tsx | 11 +- .../ios/Sources/DdFlagsImplementation.swift | 12 +- packages/core/src/flags/DatadogFlags.ts | 17 +- packages/core/src/flags/FlagsClient.ts | 229 +++++++++++++----- packages/core/src/flags/types.ts | 2 + packages/core/src/specs/NativeDdFlags.ts | 8 +- 6 files changed, 201 insertions(+), 78 deletions(-) diff --git a/example-new-architecture/App.tsx b/example-new-architecture/App.tsx index 5e921c234..165cf2791 100644 --- a/example-new-architecture/App.tsx +++ b/example-new-architecture/App.tsx @@ -88,7 +88,8 @@ function Section({children, title}: SectionProps): React.JSX.Element { } function App(): React.JSX.Element { - const [testFlagValue, setTestFlagValue] = React.useState(false); + const testFlagKey = 'rn-sdk-test-json-flag'; + const [testFlagValue, setTestFlagValue] = React.useState<{[key: string]: unknown}>({default: false}); React.useEffect(() => { (async () => { await DatadogFlags.enable(); @@ -100,9 +101,9 @@ function App(): React.JSX.Element { country: 'US', }, }); - const flag = await flagsClient.getBooleanDetails('rn-sdk-test-boolean-flag', false); // https://app.datadoghq.com/feature-flags/046d0e70-626d-41e1-8314-3f009fb79b7a?environmentId=d114cd9a-79ed-4c56-bcf3-bcac9293653b + const flag = flagsClient.getObjectDetails(testFlagKey, {default: {hello: 'world'}}); // https://app.datadoghq.com/feature-flags/046d0e70-626d-41e1-8314-3f009fb79b7a?environmentId=d114cd9a-79ed-4c56-bcf3-bcac9293653b setTestFlagValue(flag.value); - })(); + })().catch(console.error); }, []); const isDarkMode = useColorScheme() === 'dark'; @@ -121,7 +122,9 @@ function App(): React.JSX.Element { contentInsetAdjustmentBehavior="automatic" style={backgroundStyle}>
- rn-sdk-test-boolean-flag: {String(testFlagValue)} + + {testFlagKey}: {JSON.stringify(testFlagValue)} + = {}; + /** * Enables the Datadog Flags feature in your application. * @@ -77,7 +79,16 @@ class DatadogFlagsWrapper implements DatadogFlagsType { * ```ts * // Reminder: you need to initialize the SDK and enable the Flags feature before retrieving the client. * const flagsClient = DatadogFlags.getClient(); - * const flagValue = await flagsClient.getBooleanValue('new-feature', false); + * + * // Set the evaluation context. + * await flagsClient.setEvaluationContext({ + * targetingKey: 'user-123', + * attributes: { + * favoriteFruit: 'apple' + * } + * }); + * + * const flagValue = flagsClient.getBooleanValue('new-feature', false); * ``` */ getClient = (clientName: string = 'default'): FlagsClient => { @@ -88,7 +99,9 @@ class DatadogFlagsWrapper implements DatadogFlagsType { ); } - return new FlagsClient(clientName); + this.clients[clientName] ??= new FlagsClient(clientName); + + return this.clients[clientName]; }; } diff --git a/packages/core/src/flags/FlagsClient.ts b/packages/core/src/flags/FlagsClient.ts index e9ccf00f4..76ddcb71d 100644 --- a/packages/core/src/flags/FlagsClient.ts +++ b/packages/core/src/flags/FlagsClient.ts @@ -8,7 +8,12 @@ import { InternalLog } from '../InternalLog'; import { SdkVerbosity } from '../SdkVerbosity'; import type { DdNativeFlagsType } from '../nativeModulesTypes'; -import type { EvaluationContext, FlagDetails } from './types'; +import type { + ObjectValue, + EvaluationContext, + FlagDetails, + FlagEvaluationError +} from './types'; export class FlagsClient { // eslint-disable-next-line global-require, @typescript-eslint/no-var-requires @@ -17,21 +22,48 @@ export class FlagsClient { private clientName: string; + private evaluationContext: EvaluationContext | undefined = undefined; + private flagsCache: Record> = {}; + constructor(clientName: string = 'default') { this.clientName = clientName; } + /** + * Sets the evaluation context for the client. + * + * Should be called before evaluating any flags. + * + * @param context The evaluation context to associate with the current session. + * + * @example + * ```ts + * const flagsClient = DatadogFlags.getClient(); + * + * await flagsClient.setEvaluationContext({ + * targetingKey: 'user-123', + * attributes: { + * favoriteFruit: 'apple' + * } + * }); + * + * const flagValue = flagsClient.getBooleanValue('new-feature', false); + * ``` + */ setEvaluationContext = async ( context: EvaluationContext ): Promise => { const { targetingKey, attributes } = context; try { - await this.nativeFlags.setEvaluationContext( + const result = await this.nativeFlags.setEvaluationContext( this.clientName, targetingKey, attributes ); + + this.evaluationContext = context; + this.flagsCache = result; } catch (error) { if (error instanceof Error) { InternalLog.log( @@ -42,10 +74,107 @@ export class FlagsClient { } }; - getBooleanDetails = async ( + /** + * Returns the value of a boolean feature flag. + * + * @param key The key of the flag to evaluate. + * @param defaultValue The value to return if the flag is not found or evaluation fails. + * + * @example + * ```ts + * const isNewFeatureEnabled = flagsClient.getBooleanValue('new-feature-enabled', false); + * ``` + */ + getBooleanValue = (key: string, defaultValue: boolean): boolean => { + return this.getBooleanDetails(key, defaultValue).value; + }; + + /** + * Returns the value of a string feature flag. + * + * @param key The key of the flag to evaluate. + * @param defaultValue The value to return if the flag is not found or evaluation fails. + * + * @example + * ```ts + * const appTheme = flagsClient.getStringValue('app-theme', 'light'); + * ``` + */ + getStringValue = (key: string, defaultValue: string): string => { + return this.getStringDetails(key, defaultValue).value; + }; + + /** + * Returns the value of a number feature flag. + * + * @param key The key of the flag to evaluate. + * @param defaultValue The value to return if the flag is not found or evaluation fails. + * + * @example + * ```ts + * const ctaButtonSize = flagsClient.getNumberValue('cta-button-size', 16); + * ``` + */ + getNumberValue = (key: string, defaultValue: number): number => { + return this.getNumberDetails(key, defaultValue).value; + }; + + /** + * Returns the value of an object feature flag. + * + * @param key The key of the flag to evaluate. + * @param defaultValue The value to return if the flag is not found or evaluation fails. + * + * @example + * ```ts + * const pageCalloutOptions = flagsClient.getObjectValue('page-callout', { color: 'purple', text: 'Woof!' }); + * ``` + */ + getObjectValue = (key: string, defaultValue: ObjectValue): ObjectValue => { + return this.getObjectDetails(key, defaultValue).value; + }; + + private getDetails = (key: string, defaultValue: T): FlagDetails => { + let error: FlagEvaluationError | null = null; + + if (!this.evaluationContext) { + InternalLog.log( + `The evaluation context is not set for the client ${this.clientName}. Please, call \`DatadogFlags.setEvaluationContext()\` before evaluating any flags.`, + SdkVerbosity.ERROR + ); + + error = 'PROVIDER_NOT_READY'; + } + + const details = this.flagsCache[key]; + + if (!details) { + error = 'FLAG_NOT_FOUND'; + } + + if (error) { + return { + key, + value: defaultValue, + variant: null, + reason: null, + error + }; + } + + return details as FlagDetails; + }; + + /** + * Evaluates a boolean feature flag with detailed evaluation information. + * + * @param key The key of the flag to evaluate. + * @param defaultValue The value to return if the flag is not found or evaluation fails. + */ + getBooleanDetails = ( key: string, defaultValue: boolean - ): Promise> => { + ): FlagDetails => { if (typeof defaultValue !== 'boolean') { return { key, @@ -56,18 +185,19 @@ export class FlagsClient { }; } - const details = await this.nativeFlags.getBooleanDetails( - this.clientName, - key, - defaultValue - ); - return details; + return this.getDetails(key, defaultValue); }; - getStringDetails = async ( + /** + * Evaluates a string feature flag with detailed evaluation information. + * + * @param key The key of the flag to evaluate. + * @param defaultValue The value to return if the flag is not found or evaluation fails. + */ + getStringDetails = ( key: string, defaultValue: string - ): Promise> => { + ): FlagDetails => { if (typeof defaultValue !== 'string') { return { key, @@ -78,18 +208,19 @@ export class FlagsClient { }; } - const details = await this.nativeFlags.getStringDetails( - this.clientName, - key, - defaultValue - ); - return details; + return this.getDetails(key, defaultValue); }; - getNumberDetails = async ( + /** + * Evaluates a number feature flag with detailed evaluation information. + * + * @param key The key of the flag to evaluate. + * @param defaultValue The value to return if the flag is not found or evaluation fails. + */ + getNumberDetails = ( key: string, defaultValue: number - ): Promise> => { + ): FlagDetails => { if (typeof defaultValue !== 'number') { return { key, @@ -100,18 +231,19 @@ export class FlagsClient { }; } - const details = await this.nativeFlags.getNumberDetails( - this.clientName, - key, - defaultValue - ); - return details; + return this.getDetails(key, defaultValue); }; - getObjectDetails = async ( + /** + * Evaluates an object feature flag with detailed evaluation information. + * + * @param key The key of the flag to evaluate. + * @param defaultValue The value to return if the flag is not found or evaluation fails. + */ + getObjectDetails = ( key: string, - defaultValue: { [key: string]: unknown } - ): Promise> => { + defaultValue: ObjectValue + ): FlagDetails => { if (typeof defaultValue !== 'object' || defaultValue === null) { return { key, @@ -122,43 +254,6 @@ export class FlagsClient { }; } - const details = await this.nativeFlags.getObjectDetails( - this.clientName, - key, - defaultValue - ); - return details; - }; - - getBooleanValue = async ( - key: string, - defaultValue: boolean - ): Promise => { - const details = await this.getBooleanDetails(key, defaultValue); - return details.value; - }; - - getStringValue = async ( - key: string, - defaultValue: string - ): Promise => { - const details = await this.getStringDetails(key, defaultValue); - return details.value; - }; - - getNumberValue = async ( - key: string, - defaultValue: number - ): Promise => { - const details = await this.getNumberDetails(key, defaultValue); - return details.value; - }; - - getObjectValue = async ( - key: string, - defaultValue: { [key: string]: unknown } - ): Promise<{ [key: string]: unknown }> => { - const details = await this.getObjectDetails(key, defaultValue); - return details.value; + return this.getDetails(key, defaultValue); }; } diff --git a/packages/core/src/flags/types.ts b/packages/core/src/flags/types.ts index 43ff9aa7f..9fa8c1118 100644 --- a/packages/core/src/flags/types.ts +++ b/packages/core/src/flags/types.ts @@ -148,6 +148,8 @@ export interface EvaluationContext { attributes: Record; } +export type ObjectValue = { [key: string]: unknown }; + /** * An error tha occurs during feature flag evaluation. * diff --git a/packages/core/src/specs/NativeDdFlags.ts b/packages/core/src/specs/NativeDdFlags.ts index 27d349319..9df521257 100644 --- a/packages/core/src/specs/NativeDdFlags.ts +++ b/packages/core/src/specs/NativeDdFlags.ts @@ -19,8 +19,8 @@ export interface Spec extends TurboModule { readonly setEvaluationContext: ( clientName: string, targetingKey: string, - attributes: { [key: string]: unknown } - ) => Promise; + attributes: Object + ) => Promise<{ [key: string]: FlagDetails }>; readonly getBooleanDetails: ( clientName: string, @@ -43,8 +43,8 @@ export interface Spec extends TurboModule { readonly getObjectDetails: ( clientName: string, key: string, - defaultValue: { [key: string]: unknown } - ) => Promise>; + defaultValue: Object + ) => Promise>; } // eslint-disable-next-line import/no-default-export From d5848d7deee09f4aacc96be8ed64382883cc4c72 Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Wed, 3 Dec 2025 15:40:34 +0200 Subject: [PATCH 133/526] FFL-1460 Track sync flag evaluations --- packages/core/ios/Sources/DdFlags.mm | 13 +++++++++++++ .../core/ios/Sources/DdFlagsImplementation.swift | 7 +++++++ packages/core/src/flags/FlagsClient.ts | 3 +++ packages/core/src/specs/NativeDdFlags.ts | 5 +++++ 4 files changed, 28 insertions(+) diff --git a/packages/core/ios/Sources/DdFlags.mm b/packages/core/ios/Sources/DdFlags.mm index cbf3abc7e..42498cc07 100644 --- a/packages/core/ios/Sources/DdFlags.mm +++ b/packages/core/ios/Sources/DdFlags.mm @@ -34,6 +34,15 @@ @implementation DdFlags [self setEvaluationContext:clientName targetingKey:targetingKey attributes:attributes resolve:resolve reject:reject]; } +RCT_REMAP_METHOD(trackEvaluation, + trackEvaluationWithClientName:(NSString *)clientName + withKey:(NSString *)key + withResolve:(RCTPromiseResolveBlock)resolve + withReject:(RCTPromiseRejectBlock)reject) +{ + [self trackEvaluation:clientName key:key resolve:resolve reject:reject]; +} + RCT_REMAP_METHOD(getBooleanDetails, getBooleanDetailsWithClientName:(NSString *)clientName withKey:(NSString *)key @@ -107,6 +116,10 @@ - (void)setEvaluationContext:(NSString *)clientName targetingKey:(NSString *)tar [self.ddFlagsImplementation setEvaluationContext:clientName targetingKey:targetingKey attributes:attributes resolve:resolve reject:reject]; } +- (void)trackEvaluation:(NSString *)clientName key:(NSString *)key resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + [self.ddFlagsImplementation trackEvaluation:clientName key:key resolve:resolve reject:reject]; +} + - (void)getBooleanDetails:(NSString *)clientName key:(NSString *)key defaultValue:(BOOL)defaultValue resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { [self.ddFlagsImplementation getBooleanDetails:clientName key:key defaultValue:defaultValue resolve:resolve reject:reject]; } diff --git a/packages/core/ios/Sources/DdFlagsImplementation.swift b/packages/core/ios/Sources/DdFlagsImplementation.swift index 6927676d7..b3df62963 100644 --- a/packages/core/ios/Sources/DdFlagsImplementation.swift +++ b/packages/core/ios/Sources/DdFlagsImplementation.swift @@ -102,6 +102,13 @@ public class DdFlagsImplementation: NSObject { } } + @objc + public func trackEvaluation(_ clientName: String, key: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) { + let client = getClient(name: clientName) + client.trackEvaluation(key: key) + resolve(nil) + } + @objc public func getBooleanDetails( _ clientName: String, diff --git a/packages/core/src/flags/FlagsClient.ts b/packages/core/src/flags/FlagsClient.ts index 76ddcb71d..7afb58af6 100644 --- a/packages/core/src/flags/FlagsClient.ts +++ b/packages/core/src/flags/FlagsClient.ts @@ -162,6 +162,9 @@ export class FlagsClient { }; } + // Don't await this; non-blocking. + this.nativeFlags.trackEvaluation(this.clientName, key); + return details as FlagDetails; }; diff --git a/packages/core/src/specs/NativeDdFlags.ts b/packages/core/src/specs/NativeDdFlags.ts index 9df521257..5397097c3 100644 --- a/packages/core/src/specs/NativeDdFlags.ts +++ b/packages/core/src/specs/NativeDdFlags.ts @@ -22,6 +22,11 @@ export interface Spec extends TurboModule { attributes: Object ) => Promise<{ [key: string]: FlagDetails }>; + readonly trackEvaluation: ( + clientName: string, + key: string + ) => Promise; + readonly getBooleanDetails: ( clientName: string, key: string, From 696b72c0c8e55e6ef1feaaac07efc043a9c8678e Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Wed, 3 Dec 2025 15:44:58 +0200 Subject: [PATCH 134/526] FFL-1460 Remove `get*Details` methods from DdFlagsImplementation --- packages/core/ios/Sources/DdFlags.mm | 56 --------------- .../ios/Sources/DdFlagsImplementation.swift | 70 ------------------- packages/core/src/specs/NativeDdFlags.ts | 24 ------- 3 files changed, 150 deletions(-) diff --git a/packages/core/ios/Sources/DdFlags.mm b/packages/core/ios/Sources/DdFlags.mm index 42498cc07..8cdaf8f43 100644 --- a/packages/core/ios/Sources/DdFlags.mm +++ b/packages/core/ios/Sources/DdFlags.mm @@ -43,46 +43,6 @@ @implementation DdFlags [self trackEvaluation:clientName key:key resolve:resolve reject:reject]; } -RCT_REMAP_METHOD(getBooleanDetails, - getBooleanDetailsWithClientName:(NSString *)clientName - withKey:(NSString *)key - withDefaultValue:(BOOL)defaultValue - withResolve:(RCTPromiseResolveBlock)resolve - withReject:(RCTPromiseRejectBlock)reject) -{ - [self getBooleanDetails:clientName key:key defaultValue:defaultValue resolve:resolve reject:reject]; -} - -RCT_REMAP_METHOD(getStringDetails, - getStringDetailsWithClientName:(NSString *)clientName - withKey:(NSString *)key - withDefaultValue:(NSString *)defaultValue - withResolve:(RCTPromiseResolveBlock)resolve - withReject:(RCTPromiseRejectBlock)reject) -{ - [self getStringDetails:clientName key:key defaultValue:defaultValue resolve:resolve reject:reject]; -} - -RCT_REMAP_METHOD(getNumberDetails, - getNumberDetailsWithClientName:(NSString *)clientName - withKey:(NSString *)key - withDefaultValue:(double)defaultValue - withResolve:(RCTPromiseResolveBlock)resolve - withReject:(RCTPromiseRejectBlock)reject) -{ - [self getNumberDetails:clientName key:key defaultValue:defaultValue resolve:resolve reject:reject]; -} - -RCT_REMAP_METHOD(getObjectDetails, - getObjectDetailsWithClientName:(NSString *)clientName - withKey:(NSString *)key - withDefaultValue:(NSDictionary *)defaultValue - withResolve:(RCTPromiseResolveBlock)resolve - withReject:(RCTPromiseRejectBlock)reject) -{ - [self getObjectDetails:clientName key:key defaultValue:defaultValue resolve:resolve reject:reject]; -} - // Thanks to this guard, we won't compile this code when we build for the new architecture. #ifdef RCT_NEW_ARCH_ENABLED - (std::shared_ptr)getTurboModule: @@ -119,20 +79,4 @@ - (void)setEvaluationContext:(NSString *)clientName targetingKey:(NSString *)tar - (void)trackEvaluation:(NSString *)clientName key:(NSString *)key resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { [self.ddFlagsImplementation trackEvaluation:clientName key:key resolve:resolve reject:reject]; } - -- (void)getBooleanDetails:(NSString *)clientName key:(NSString *)key defaultValue:(BOOL)defaultValue resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { - [self.ddFlagsImplementation getBooleanDetails:clientName key:key defaultValue:defaultValue resolve:resolve reject:reject]; -} - -- (void)getStringDetails:(NSString *)clientName key:(NSString *)key defaultValue:(NSString *)defaultValue resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { - [self.ddFlagsImplementation getStringDetails:clientName key:key defaultValue:defaultValue resolve:resolve reject:reject]; -} - -- (void)getNumberDetails:(NSString *)clientName key:(NSString *)key defaultValue:(double)defaultValue resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { - [self.ddFlagsImplementation getNumberDetails:clientName key:key defaultValue:defaultValue resolve:resolve reject:reject]; -} - -- (void)getObjectDetails:(NSString *)clientName key:(NSString *)key defaultValue:(NSDictionary *)defaultValue resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { - [self.ddFlagsImplementation getObjectDetails:clientName key:key defaultValue:defaultValue resolve:resolve reject:reject]; -} @end diff --git a/packages/core/ios/Sources/DdFlagsImplementation.swift b/packages/core/ios/Sources/DdFlagsImplementation.swift index b3df62963..7f0544be9 100644 --- a/packages/core/ios/Sources/DdFlagsImplementation.swift +++ b/packages/core/ios/Sources/DdFlagsImplementation.swift @@ -108,76 +108,6 @@ public class DdFlagsImplementation: NSObject { client.trackEvaluation(key: key) resolve(nil) } - - @objc - public func getBooleanDetails( - _ clientName: String, - key: String, - defaultValue: Bool, - resolve: RCTPromiseResolveBlock, - reject: RCTPromiseRejectBlock - ) { - let client = getClient(name: clientName) - let details = client.getBooleanDetails(key: key, defaultValue: defaultValue) - let serializedDetails = details.toSerializedDictionary() - resolve(serializedDetails) - } - - @objc - public func getStringDetails( - _ clientName: String, - key: String, - defaultValue: String, - resolve: RCTPromiseResolveBlock, - reject: RCTPromiseRejectBlock - ) { - let client = getClient(name: clientName) - let details = client.getStringDetails(key: key, defaultValue: defaultValue) - let serializedDetails = details.toSerializedDictionary() - resolve(serializedDetails) - } - - @objc - public func getNumberDetails( - _ clientName: String, - key: String, - defaultValue: Double, - resolve: RCTPromiseResolveBlock, - reject: RCTPromiseRejectBlock - ) { - let client = getClient(name: clientName) - - let doubleDetails = client.getDoubleDetails(key: key, defaultValue: defaultValue) - - // Try to retrieve this flag as Integer, not a Number flag type. - if doubleDetails.error == .typeMismatch { - if let safeInt = Int(exactly: defaultValue) { - let intDetails = client.getIntegerDetails(key: key, defaultValue: safeInt) - - // If resolved correctly, return Integer details. - if intDetails.error == nil { - resolve(intDetails.toSerializedDictionary()) - return - } - } - } - - resolve(doubleDetails.toSerializedDictionary()) - } - - @objc - public func getObjectDetails( - _ clientName: String, - key: String, - defaultValue: [String: Any], - resolve: RCTPromiseResolveBlock, - reject: RCTPromiseRejectBlock - ) { - let client = getClient(name: clientName) - let details = client.getObjectDetails(key: key, defaultValue: AnyValue.wrap(defaultValue)) - let serializedDetails = details.toSerializedDictionary() - resolve(serializedDetails) - } } extension AnyValue { diff --git a/packages/core/src/specs/NativeDdFlags.ts b/packages/core/src/specs/NativeDdFlags.ts index 5397097c3..f9f47a973 100644 --- a/packages/core/src/specs/NativeDdFlags.ts +++ b/packages/core/src/specs/NativeDdFlags.ts @@ -26,30 +26,6 @@ export interface Spec extends TurboModule { clientName: string, key: string ) => Promise; - - readonly getBooleanDetails: ( - clientName: string, - key: string, - defaultValue: boolean - ) => Promise>; - - readonly getStringDetails: ( - clientName: string, - key: string, - defaultValue: string - ) => Promise>; - - readonly getNumberDetails: ( - clientName: string, - key: string, - defaultValue: number - ) => Promise>; - - readonly getObjectDetails: ( - clientName: string, - key: string, - defaultValue: Object - ) => Promise>; } // eslint-disable-next-line import/no-default-export From 7183cc79b5bc55e276819ca23183286817206695 Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Wed, 3 Dec 2025 17:16:30 +0200 Subject: [PATCH 135/526] Address PR comments regarding DatadogFlags enable method --- packages/core/src/flags/DatadogFlags.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/core/src/flags/DatadogFlags.ts b/packages/core/src/flags/DatadogFlags.ts index 206bbb276..01d4cd858 100644 --- a/packages/core/src/flags/DatadogFlags.ts +++ b/packages/core/src/flags/DatadogFlags.ts @@ -50,14 +50,17 @@ class DatadogFlagsWrapper implements DatadogFlagsType { * @param configuration Configuration options for the Datadog Flags feature. */ enable = async ( - configuration?: Omit + configuration?: DatadogFlagsConfiguration ): Promise => { + if (configuration?.enabled === false) { + return; + } + if (this.isFeatureEnabled) { InternalLog.log( 'Datadog Flags feature has already been enabled. Skipping this `DatadogFlags.enable()` call.', SdkVerbosity.WARN ); - return; } await this.nativeFlags.enable({ ...configuration, enabled: true }); From 7ebf3a8babdc6411f045b9ec30a440ed67b673fb Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Wed, 3 Dec 2025 17:18:17 +0200 Subject: [PATCH 136/526] Fix Obj-C remap names --- packages/core/ios/Sources/DdFlags.mm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/ios/Sources/DdFlags.mm b/packages/core/ios/Sources/DdFlags.mm index cbf3abc7e..c7034a6c3 100644 --- a/packages/core/ios/Sources/DdFlags.mm +++ b/packages/core/ios/Sources/DdFlags.mm @@ -17,7 +17,7 @@ @implementation DdFlags RCT_EXPORT_MODULE() RCT_REMAP_METHOD(enable, - withConfiguration:(NSDictionary *)configuration + enableDdFlagsWithConfiguration:(NSDictionary *)configuration withResolve:(RCTPromiseResolveBlock)resolve withReject:(RCTPromiseRejectBlock)reject) { @@ -25,7 +25,7 @@ @implementation DdFlags } RCT_REMAP_METHOD(setEvaluationContext, - withClientName:(NSString *)clientName + setEvaluationContextWithClientName:(NSString *)clientName withTargetingKey:(NSString *)targetingKey withAttributes:(NSDictionary *)attributes withResolve:(RCTPromiseResolveBlock)resolve From 916943c499dd56318e8d4d6f5ad6771e9396043c Mon Sep 17 00:00:00 2001 From: Sergio Barrio Date: Wed, 3 Dec 2025 17:38:46 +0100 Subject: [PATCH 137/526] 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 138/526] 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 139/526] 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 140/526] 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 c1f9446db12ee67745e0b19f9731a70c10768c1c Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Mon, 8 Dec 2025 15:01:25 +0200 Subject: [PATCH 141/526] Fix JS tests --- packages/core/src/flags/DatadogFlags.ts | 3 ++- packages/core/src/flags/__tests__/DatadogFlags.test.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/core/src/flags/DatadogFlags.ts b/packages/core/src/flags/DatadogFlags.ts index 01d4cd858..666226eba 100644 --- a/packages/core/src/flags/DatadogFlags.ts +++ b/packages/core/src/flags/DatadogFlags.ts @@ -63,7 +63,8 @@ class DatadogFlagsWrapper implements DatadogFlagsType { ); } - await this.nativeFlags.enable({ ...configuration, enabled: true }); + // Default `enabled` to `true`. + await this.nativeFlags.enable({ enabled: true, ...configuration }); this.isFeatureEnabled = true; }; diff --git a/packages/core/src/flags/__tests__/DatadogFlags.test.ts b/packages/core/src/flags/__tests__/DatadogFlags.test.ts index 70a76102f..7e579d533 100644 --- a/packages/core/src/flags/__tests__/DatadogFlags.test.ts +++ b/packages/core/src/flags/__tests__/DatadogFlags.test.ts @@ -33,7 +33,8 @@ describe('DatadogFlags', () => { await DatadogFlags.enable(); expect(InternalLog.log).toHaveBeenCalledTimes(2); - expect(NativeModules.DdFlags.enable).toHaveBeenCalledTimes(1); + // We let the native part of the SDK handle this gracefully. + expect(NativeModules.DdFlags.enable).toHaveBeenCalledTimes(3); }); it('should print an error if retrieving the client before the feature is enabled', async () => { From b7cebb47c7552e63ccf52b9c2826e4444750ffdf Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Mon, 8 Dec 2025 19:35:19 +0200 Subject: [PATCH 142/526] Support the `client.getFlagsDetails` -> `client.getAllFlagsDetails` rename --- .../core/ios/Sources/DdFlagsImplementation.swift | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/core/ios/Sources/DdFlagsImplementation.swift b/packages/core/ios/Sources/DdFlagsImplementation.swift index 7f0544be9..98e37ca1e 100644 --- a/packages/core/ios/Sources/DdFlagsImplementation.swift +++ b/packages/core/ios/Sources/DdFlagsImplementation.swift @@ -6,7 +6,7 @@ import Foundation import DatadogInternal -@_spi(Internal) +@_spi(Internal) import DatadogFlags @objc @@ -69,21 +69,21 @@ public class DdFlagsImplementation: NSObject { public func setEvaluationContext(_ clientName: String, targetingKey: String, attributes: NSDictionary, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { let client = getClient(name: clientName) - let parsedAttributes = parseAttributes(attributes: attributes) + let parsedAttributes = parseAttributes(attributes: attributes) let evaluationContext = FlagsEvaluationContext(targetingKey: targetingKey, attributes: parsedAttributes) client.setEvaluationContext(evaluationContext) { result in switch result { case .success: - guard let flagsDetails = client.getFlagsDetails() else { + guard let flagsDetails = client.getAllFlagsDetails() else { reject(nil, "CLIENT_NOT_INITIALIZED", nil) return } - + let result = flagsDetails.compactMapValues { details in details.toSerializedDictionary() } - + resolve(result) case .failure(let error): var errorCode: String @@ -165,7 +165,7 @@ extension FlagDetails { return dict } - + private func getSerializedValue() -> Any { if let boolValue = value as? Bool { return boolValue @@ -182,7 +182,7 @@ extension FlagDetails { // Fallback for unexpected types. return NSNull() } - + private func getSerializedError() -> String? { guard let error = error else { return nil From 0266a89db8fe59726125255ea5a055df1e99624b Mon Sep 17 00:00:00 2001 From: Marco Saia Date: Fri, 10 Oct 2025 11:31:03 +0200 Subject: [PATCH 143/526] 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 144/526] 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 145/526] 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 146/526] 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 147/526] 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 148/526] 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 149/526] 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 2adedb02866735fe3aebac032179f872a80b4f23 Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Tue, 9 Dec 2025 19:15:01 +0200 Subject: [PATCH 150/526] Cast to FlagsClientInternal when using "internal" iOS APIs --- packages/core/ios/Sources/DdFlagsImplementation.swift | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/core/ios/Sources/DdFlagsImplementation.swift b/packages/core/ios/Sources/DdFlagsImplementation.swift index 98e37ca1e..2afe4cf97 100644 --- a/packages/core/ios/Sources/DdFlagsImplementation.swift +++ b/packages/core/ios/Sources/DdFlagsImplementation.swift @@ -68,6 +68,9 @@ public class DdFlagsImplementation: NSObject { @objc public func setEvaluationContext(_ clientName: String, targetingKey: String, attributes: NSDictionary, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { let client = getClient(name: clientName) + guard let clientInternal = getClient(name: clientName) as? FlagsClientInternal else { + return + } let parsedAttributes = parseAttributes(attributes: attributes) let evaluationContext = FlagsEvaluationContext(targetingKey: targetingKey, attributes: parsedAttributes) @@ -75,7 +78,7 @@ public class DdFlagsImplementation: NSObject { client.setEvaluationContext(evaluationContext) { result in switch result { case .success: - guard let flagsDetails = client.getAllFlagsDetails() else { + guard let flagsDetails = clientInternal.getAllFlagsDetails() else { reject(nil, "CLIENT_NOT_INITIALIZED", nil) return } @@ -104,7 +107,10 @@ public class DdFlagsImplementation: NSObject { @objc public func trackEvaluation(_ clientName: String, key: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) { - let client = getClient(name: clientName) + guard let client = getClient(name: clientName) as? FlagsClientInternal else { + return + } + client.trackEvaluation(key: key) resolve(nil) } From 02b5bfe2a101658fa0527a9e915b094862a4522b Mon Sep 17 00:00:00 2001 From: Sergio Barrio Date: Fri, 28 Nov 2025 16:25:22 +0100 Subject: [PATCH 151/526] 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 =>