From b6c3981d1557652776b028b2fb7b682fbe0c4680 Mon Sep 17 00:00:00 2001 From: Carlos Nogueira Date: Sat, 27 Jun 2026 20:43:45 +0100 Subject: [PATCH 01/10] Make native module specs resolve lazily --- packages/core/src/flags/DdFlags.ts | 6 ++-- packages/core/src/flags/FlagsClient.ts | 6 ++-- packages/core/src/logs/DdLogs.ts | 6 ++-- packages/core/src/rum/DdRum.ts | 6 ++-- .../DdBabelInteractionTracking.ts | 6 ++-- .../DdBabelInteractionTracking.test.ts | 4 +-- .../DatadogDefaultEventEmitter.tsx | 6 ++-- packages/core/src/sdk/DdSdkInternal.ts | 28 +++++++++++++++++-- packages/core/src/specs/NativeDdFlags.ts | 15 ++++++++-- packages/core/src/specs/NativeDdLogs.ts | 15 ++++++++-- packages/core/src/specs/NativeDdRum.ts | 15 ++++++++-- packages/core/src/specs/NativeDdSdk.ts | 15 ++++++++-- packages/core/src/specs/NativeDdTrace.ts | 15 ++++++++-- packages/core/src/trace/DdTrace.ts | 5 ++-- 14 files changed, 111 insertions(+), 37 deletions(-) diff --git a/packages/core/src/flags/DdFlags.ts b/packages/core/src/flags/DdFlags.ts index 43714d598..afb794e5e 100644 --- a/packages/core/src/flags/DdFlags.ts +++ b/packages/core/src/flags/DdFlags.ts @@ -9,6 +9,8 @@ import { SdkVerbosity } from '../config/types/SdkVerbosity'; import type { DdNativeFlagsType } from '../nativeModulesTypes'; import { getGlobalInstance } from '../utils/singletonUtils'; +import { getNativeDdFlags } from '../specs/NativeDdFlags'; + import { FlagsClient } from './FlagsClient'; import type { DdFlagsType, FlagsConfiguration } from './types'; @@ -18,9 +20,7 @@ const FLAGS_MODULE = 'com.datadog.reactnative.flags'; * Implementation class for {@link DdFlagsType}. Please see the interface for documentation. */ class DdFlagsWrapper implements DdFlagsType { - // eslint-disable-next-line global-require, @typescript-eslint/no-var-requires - private nativeFlags: DdNativeFlagsType = require('../specs/NativeDdFlags') - .default; + private nativeFlags: DdNativeFlagsType = getNativeDdFlags() as DdNativeFlagsType; private isFeatureEnabled = false; diff --git a/packages/core/src/flags/FlagsClient.ts b/packages/core/src/flags/FlagsClient.ts index c1e56ff57..a2bd3b4da 100644 --- a/packages/core/src/flags/FlagsClient.ts +++ b/packages/core/src/flags/FlagsClient.ts @@ -16,6 +16,8 @@ import type { ParsedFlagsConfiguration, ParsedPrecomputedConfiguration } from './configuration'; +import { getNativeDdFlags } from '../specs/NativeDdFlags'; + import { processEvaluationContext } from './internal'; import type { FlagCacheEntry } from './internal'; import type { JsonValue, EvaluationContext, FlagDetails } from './types'; @@ -82,9 +84,7 @@ interface OfflineFlagsEvaluator { } export class FlagsClient { - // eslint-disable-next-line global-require, @typescript-eslint/no-var-requires - private nativeFlags: DdNativeFlagsType = require('../specs/NativeDdFlags') - .default; + private nativeFlags: DdNativeFlagsType = getNativeDdFlags() as DdNativeFlagsType; private clientName: string; diff --git a/packages/core/src/logs/DdLogs.ts b/packages/core/src/logs/DdLogs.ts index 8dcb7421a..6e9264388 100644 --- a/packages/core/src/logs/DdLogs.ts +++ b/packages/core/src/logs/DdLogs.ts @@ -14,6 +14,8 @@ import { bufferVoidNativeCall } from '../sdk/DatadogProvider/Buffer/bufferNative import type { ErrorSource, LogEventMapper } from '../types'; import { getGlobalInstance } from '../utils/singletonUtils'; +import { getNativeDdLogs } from '../specs/NativeDdLogs'; + import { generateEventMapper } from './eventMapper'; import type { DdLogsType, @@ -44,9 +46,7 @@ const isLogWithError = ( }; class DdLogsWrapper implements DdLogsType { - // eslint-disable-next-line global-require, @typescript-eslint/no-var-requires - private nativeLogs: DdNativeLogsType = require('../specs/NativeDdLogs') - .default; + private nativeLogs: DdNativeLogsType = getNativeDdLogs() as DdNativeLogsType; private logEventMapper = generateEventMapper(undefined); debug = (...args: LogArguments | LogWithErrorArguments): Promise => { diff --git a/packages/core/src/rum/DdRum.ts b/packages/core/src/rum/DdRum.ts index 10db74599..f531bcfad 100644 --- a/packages/core/src/rum/DdRum.ts +++ b/packages/core/src/rum/DdRum.ts @@ -40,6 +40,8 @@ import { getTracingContext, getTracingContextForPropagators } from './instrumentation/resourceTracking/distributedTracing/distributedTracingHeaders'; +import { getNativeDdRum } from '../specs/NativeDdRum'; + import type { DdRumType, FirstPartyHost, @@ -77,9 +79,7 @@ const touchDataFromEvent = ( }; class DdRumWrapper implements DdRumType { - // eslint-disable-next-line global-require, @typescript-eslint/no-var-requires - private nativeRum: DdNativeRumType = require('../specs/NativeDdRum') - .default; + private nativeRum: DdNativeRumType = getNativeDdRum() as DdNativeRumType; private lastActionData?: { type: RumActionType; name: string }; private errorEventMapper = generateErrorEventMapper(undefined); private resourceEventMapper = generateResourceEventMapper(undefined); diff --git a/packages/core/src/rum/instrumentation/interactionTracking/DdBabelInteractionTracking.ts b/packages/core/src/rum/instrumentation/interactionTracking/DdBabelInteractionTracking.ts index e92d12e1a..57a59b29e 100644 --- a/packages/core/src/rum/instrumentation/interactionTracking/DdBabelInteractionTracking.ts +++ b/packages/core/src/rum/instrumentation/interactionTracking/DdBabelInteractionTracking.ts @@ -4,7 +4,7 @@ * Copyright 2016-Present Datadog, Inc. */ -import DdSdk from '../../../specs/NativeDdSdk'; +import { getNativeDdSdk } from '../../../specs/NativeDdSdk'; import { getGlobalInstance } from '../../../utils/singletonUtils'; import { DefaultTimeProvider } from '../../../utils/time-provider/DefaultTimeProvider'; import type { TimeProvider } from '../../../utils/time-provider/TimeProvider'; @@ -130,7 +130,7 @@ class BabelInteractionTracking { ): (...args: any[]) => any { return (...args: any[]) => { if (!this.telemetrySent) { - DdSdk?.sendTelemetryLog( + getNativeDdSdk()?.sendTelemetryLog( BABEL_PLUGIN_TELEMETRY, this.getTelemetryConfig(), { onlyOnce: true } @@ -154,7 +154,7 @@ class BabelInteractionTracking { ) .catch(e => { if (e instanceof Error) { - DdSdk?.telemetryError( + getNativeDdSdk()?.telemetryError( e.message, e.stack || '', 'BabelActionTrack' diff --git a/packages/core/src/rum/instrumentation/interactionTracking/__tests__/DdBabelInteractionTracking.test.ts b/packages/core/src/rum/instrumentation/interactionTracking/__tests__/DdBabelInteractionTracking.test.ts index 2a2e93e8b..682bd63f4 100644 --- a/packages/core/src/rum/instrumentation/interactionTracking/__tests__/DdBabelInteractionTracking.test.ts +++ b/packages/core/src/rum/instrumentation/interactionTracking/__tests__/DdBabelInteractionTracking.test.ts @@ -9,10 +9,10 @@ import { DdBabelInteractionTracking } from '../DdBabelInteractionTracking'; jest.mock('../../../../specs/NativeDdSdk', () => ({ __esModule: true, - default: { + getNativeDdSdk: jest.fn().mockReturnValue({ sendTelemetryLog: jest.fn(), telemetryError: jest.fn() - } + }) })); jest.mock('../../../../utils/time-provider/DefaultTimeProvider', () => ({ diff --git a/packages/core/src/sdk/DatadogEventEmitter/DatadogDefaultEventEmitter.tsx b/packages/core/src/sdk/DatadogEventEmitter/DatadogDefaultEventEmitter.tsx index f04bf7ba2..b55926272 100644 --- a/packages/core/src/sdk/DatadogEventEmitter/DatadogDefaultEventEmitter.tsx +++ b/packages/core/src/sdk/DatadogEventEmitter/DatadogDefaultEventEmitter.tsx @@ -5,6 +5,7 @@ */ import { NativeModules } from 'react-native'; +import { getNativeDdSdk } from '../../specs/NativeDdSdk'; import { DatadogBatchedBridgeEventEmitter } from './DatadogBatchedBridgeEventEmitter'; import type { DatadogEventEmitter } from './DatadogEventEmitter'; import { DatadogNativeEventEmitter } from './DatadogNativeEventEmitter'; @@ -18,10 +19,7 @@ export class DatadogDefaultEventEmitter implements DatadogEventEmitter { constructor(errorHandler: (err: any) => void) { try { - const ddSdkModule = - NativeModules.DdSdk || - // eslint-disable-next-line global-require, @typescript-eslint/no-var-requires - require('../../specs/NativeDdSdk').default; + const ddSdkModule = NativeModules.DdSdk || getNativeDdSdk(); this.eventEmitter = this.isNewArchitecture ? new DatadogNativeEventEmitter(ddSdkModule, errorHandler) : new DatadogBatchedBridgeEventEmitter(errorHandler); diff --git a/packages/core/src/sdk/DdSdkInternal.ts b/packages/core/src/sdk/DdSdkInternal.ts index 2c1d74685..62f2c6477 100644 --- a/packages/core/src/sdk/DdSdkInternal.ts +++ b/packages/core/src/sdk/DdSdkInternal.ts @@ -6,12 +6,34 @@ import type { DdSdkNativeConfiguration } from '../config/features/CoreConfigurationNative'; import type { DdNativeSdkType } from '../nativeModulesTypes'; +import { getNativeDdSdk } from '../specs/NativeDdSdk'; import type { AttributeEncoder } from './AttributesEncoding/types'; -// eslint-disable-next-line global-require, @typescript-eslint/no-var-requires -const NativeDdSdkSpec: DdNativeSdkType = require('../specs/NativeDdSdk') - .default; +let cachedNativeDdSdk: DdNativeSdkType | null | undefined; +const resolveNativeDdSdk = (): DdNativeSdkType | null => { + if (cachedNativeDdSdk === undefined) { + cachedNativeDdSdk = getNativeDdSdk() as DdNativeSdkType | null; + } + return cachedNativeDdSdk; +}; + +// Lazily-backed handle. Resolution happens on first property access (runtime), +// never at import — so importing this module is safe on platforms without the +// native module (e.g. Vega). +const NativeDdSdkSpec: DdNativeSdkType = new Proxy({} as DdNativeSdkType, { + get: (_target, prop) => { + const resolved = resolveNativeDdSdk() as Record< + string | symbol, + unknown + > | null; + const value = resolved ? resolved[prop] : undefined; + // Return the raw value without binding — TurboModule methods are stateless + // so `this` is irrelevant, and returning the original function reference + // preserves Jest mock-tracking in tests. + return value; + } +}); export type DdSdkType = DdNativeSdkType & { readonly attributeEncoders: AttributeEncoder[]; diff --git a/packages/core/src/specs/NativeDdFlags.ts b/packages/core/src/specs/NativeDdFlags.ts index 5c8afd010..6efdb0fe5 100644 --- a/packages/core/src/specs/NativeDdFlags.ts +++ b/packages/core/src/specs/NativeDdFlags.ts @@ -31,5 +31,16 @@ export interface Spec extends TurboModule { ) => Promise; } -// eslint-disable-next-line import/no-default-export -export default TurboModuleRegistry.get('DdFlags'); +let cachedModule: Spec | null | undefined; + +/** + * Lazily resolves the native TurboModule on first call and caches the result. + * Resolving lazily (instead of at module load) keeps this package importable on + * platforms where the native module is absent (e.g. Vega). + */ +export const getNativeDdFlags = (): Spec | null => { + if (cachedModule === undefined) { + cachedModule = TurboModuleRegistry.get('DdFlags'); + } + return cachedModule; +}; diff --git a/packages/core/src/specs/NativeDdLogs.ts b/packages/core/src/specs/NativeDdLogs.ts index f1a60f4c7..0933f5002 100644 --- a/packages/core/src/specs/NativeDdLogs.ts +++ b/packages/core/src/specs/NativeDdLogs.ts @@ -107,5 +107,16 @@ export interface Spec extends TurboModule { ) => Promise; } -// eslint-disable-next-line import/no-default-export -export default TurboModuleRegistry.get('DdLogs'); +let cachedModule: Spec | null | undefined; + +/** + * Lazily resolves the native TurboModule on first call and caches the result. + * Resolving lazily (instead of at module load) keeps this package importable on + * platforms where the native module is absent (e.g. Vega). + */ +export const getNativeDdLogs = (): Spec | null => { + if (cachedModule === undefined) { + cachedModule = TurboModuleRegistry.get('DdLogs'); + } + return cachedModule; +}; diff --git a/packages/core/src/specs/NativeDdRum.ts b/packages/core/src/specs/NativeDdRum.ts index 00950807a..9da6129df 100644 --- a/packages/core/src/specs/NativeDdRum.ts +++ b/packages/core/src/specs/NativeDdRum.ts @@ -236,5 +236,16 @@ export interface Spec extends TurboModule { ): Promise; } -// eslint-disable-next-line import/no-default-export -export default TurboModuleRegistry.get('DdRum'); +let cachedModule: Spec | null | undefined; + +/** + * Lazily resolves the native TurboModule on first call and caches the result. + * Resolving lazily (instead of at module load) keeps this package importable on + * platforms where the native module is absent (e.g. Vega). + */ +export const getNativeDdRum = (): Spec | null => { + if (cachedModule === undefined) { + cachedModule = TurboModuleRegistry.get('DdRum'); + } + return cachedModule; +}; diff --git a/packages/core/src/specs/NativeDdSdk.ts b/packages/core/src/specs/NativeDdSdk.ts index 68c9c4711..2b2c5a8e2 100644 --- a/packages/core/src/specs/NativeDdSdk.ts +++ b/packages/core/src/specs/NativeDdSdk.ts @@ -134,5 +134,16 @@ export interface Spec extends TurboModule { removeListeners: (count: number) => void; } -// eslint-disable-next-line import/no-default-export -export default TurboModuleRegistry.get('DdSdk'); +let cachedModule: Spec | null | undefined; + +/** + * Lazily resolves the native TurboModule on first call and caches the result. + * Resolving lazily (instead of at module load) keeps this package importable on + * platforms where the native module is absent (e.g. Vega). + */ +export const getNativeDdSdk = (): Spec | null => { + if (cachedModule === undefined) { + cachedModule = TurboModuleRegistry.get('DdSdk'); + } + return cachedModule; +}; diff --git a/packages/core/src/specs/NativeDdTrace.ts b/packages/core/src/specs/NativeDdTrace.ts index 8f3526f37..d67b71cb4 100644 --- a/packages/core/src/specs/NativeDdTrace.ts +++ b/packages/core/src/specs/NativeDdTrace.ts @@ -39,5 +39,16 @@ export interface Spec extends TurboModule { ): Promise; } -// eslint-disable-next-line import/no-default-export -export default TurboModuleRegistry.get('DdTrace'); +let cachedModule: Spec | null | undefined; + +/** + * Lazily resolves the native TurboModule on first call and caches the result. + * Resolving lazily (instead of at module load) keeps this package importable on + * platforms where the native module is absent (e.g. Vega). + */ +export const getNativeDdTrace = (): Spec | null => { + if (cachedModule === undefined) { + cachedModule = TurboModuleRegistry.get('DdTrace'); + } + return cachedModule; +}; diff --git a/packages/core/src/trace/DdTrace.ts b/packages/core/src/trace/DdTrace.ts index 72fe2375b..119863074 100644 --- a/packages/core/src/trace/DdTrace.ts +++ b/packages/core/src/trace/DdTrace.ts @@ -12,6 +12,7 @@ import { bufferNativeCallReturningId, bufferNativeCallWithId } from '../sdk/DatadogProvider/Buffer/bufferNativeCall'; +import { getNativeDdTrace } from '../specs/NativeDdTrace'; import type { DdTraceType } from '../types'; import { getGlobalInstance } from '../utils/singletonUtils'; import { DefaultTimeProvider } from '../utils/time-provider/DefaultTimeProvider'; @@ -21,9 +22,7 @@ const TRACE_MODULE = 'com.datadog.reactnative.trace'; const timeProvider = new DefaultTimeProvider(); class DdTraceWrapper implements DdTraceType { - // eslint-disable-next-line global-require, @typescript-eslint/no-var-requires - private nativeTrace: DdNativeTraceType = require('../specs/NativeDdTrace') - .default; + private nativeTrace: DdNativeTraceType = getNativeDdTrace() as DdNativeTraceType; startSpan = ( operation: string, From 8eefac328a3f10cb40c04d42beee6e274cccbd7e Mon Sep 17 00:00:00 2001 From: Carlos Nogueira Date: Sat, 27 Jun 2026 20:44:02 +0100 Subject: [PATCH 02/10] Add import safety regression tests --- .../core/src/__tests__/importSafety.test.ts | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 packages/core/src/__tests__/importSafety.test.ts diff --git a/packages/core/src/__tests__/importSafety.test.ts b/packages/core/src/__tests__/importSafety.test.ts new file mode 100644 index 000000000..7ff745e01 --- /dev/null +++ b/packages/core/src/__tests__/importSafety.test.ts @@ -0,0 +1,53 @@ +import { TurboModuleRegistry } from 'react-native'; + +describe('native spec modules are import-safe (lazy)', () => { + const cases: Array<[string, string, string]> = [ + ['../specs/NativeDdSdk', 'getNativeDdSdk', 'DdSdk'], + ['../specs/NativeDdRum', 'getNativeDdRum', 'DdRum'], + ['../specs/NativeDdLogs', 'getNativeDdLogs', 'DdLogs'], + ['../specs/NativeDdTrace', 'getNativeDdTrace', 'DdTrace'], + ['../specs/NativeDdFlags', 'getNativeDdFlags', 'DdFlags'] + ]; + + it.each(cases)( + '%s does not resolve native at import but does on getter call', + (modulePath, getterName, registryName) => { + const getSpy = jest.spyOn(TurboModuleRegistry, 'get'); + let mod: any; + jest.isolateModules(() => { + mod = require(modulePath); + }); + expect(getSpy).not.toHaveBeenCalled(); // not at import + mod[getterName](); + expect(getSpy).toHaveBeenCalledWith(registryName); // only on use + getSpy.mockRestore(); + } + ); +}); + +describe('DdSdkInternal is import-safe', () => { + it('importing DdSdkInternal does not resolve native until a method is used', () => { + const getSpy = jest.spyOn(TurboModuleRegistry, 'get'); + let internal: any; + jest.isolateModules(() => { + internal = require('../sdk/DdSdkInternal'); + }); + expect(getSpy).not.toHaveBeenCalled(); // import-safe + // Touch a property on the proxy -> now it resolves + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + internal.NativeDdSdk.getConstants; + expect(getSpy).toHaveBeenCalledWith('DdSdk'); + getSpy.mockRestore(); + }); +}); + +describe('public barrel is import-safe', () => { + it('importing the package index performs no native TurboModule lookup', () => { + const getSpy = jest.spyOn(TurboModuleRegistry, 'get'); + jest.isolateModules(() => { + require('../index'); + }); + expect(getSpy).not.toHaveBeenCalled(); + getSpy.mockRestore(); + }); +}); From 5ba18cc3cbaa77d415b0065f19f095db1891644a Mon Sep 17 00:00:00 2001 From: Carlos Nogueira Date: Sat, 27 Jun 2026 20:46:48 +0100 Subject: [PATCH 03/10] Expose core internals entry point --- packages/core/package.json | 5 +++ .../core/src/__tests__/internalEntry.test.ts | 34 +++++++++++++++++++ packages/core/src/internal.ts | 34 +++++++++++++++++++ 3 files changed, 73 insertions(+) create mode 100644 packages/core/src/__tests__/internalEntry.test.ts create mode 100644 packages/core/src/internal.ts diff --git a/packages/core/package.json b/packages/core/package.json index 3c9948b6c..ce970b6b5 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -44,6 +44,11 @@ "require": "./lib/commonjs/index.js", "types": "./lib/typescript/index.d.ts" }, + "./internal": { + "import": "./lib/module/internal.js", + "require": "./lib/commonjs/internal.js", + "types": "./lib/typescript/internal.d.ts" + }, "./metro": { "import": "./lib/module/metro.js", "require": "./lib/commonjs/metro.js", diff --git a/packages/core/src/__tests__/internalEntry.test.ts b/packages/core/src/__tests__/internalEntry.test.ts new file mode 100644 index 000000000..145066dc5 --- /dev/null +++ b/packages/core/src/__tests__/internalEntry.test.ts @@ -0,0 +1,34 @@ +import { TurboModuleRegistry } from 'react-native'; + +describe('./internal entry', () => { + it('exports the internals react-native-vega needs', () => { + const internal = require('../internal'); + const expected = [ + 'addDefaultValuesToAutoInstrumentationConfiguration', + 'buildConfigurationFromPartialConfiguration', + 'DdSdkNativeConfiguration', + 'RUM_DEFAULTS', + 'adaptLongTaskThreshold', + 'version', + 'AccountInfoSingleton', + 'AttributesSingleton', + 'UserInfoSingleton', + 'GlobalState', + 'BufferSingleton', + 'DatadogProviderState', + 'DdRumResourceTracking' + ]; + for (const name of expected) { + expect(internal[name]).toBeDefined(); + } + }); + + it('is import-safe (no native lookup at import)', () => { + const getSpy = jest.spyOn(TurboModuleRegistry, 'get'); + jest.isolateModules(() => { + require('../internal'); + }); + expect(getSpy).not.toHaveBeenCalled(); + getSpy.mockRestore(); + }); +}); diff --git a/packages/core/src/internal.ts b/packages/core/src/internal.ts new file mode 100644 index 000000000..260d1e705 --- /dev/null +++ b/packages/core/src/internal.ts @@ -0,0 +1,34 @@ +/* + * 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. + */ + +/** + * Sanctioned entry point for cross-package internals consumed by platform + * packages (e.g. @datadog/mobile-react-native-vega). NOT part of the public API + * and may change between minor versions. Everything re-exported here must be + * native-free at import (must not call TurboModuleRegistry / NativeModules at + * module load). + */ +export { + addDefaultValuesToAutoInstrumentationConfiguration +} from './config/async/AutoInstrumentationConfiguration'; +export { buildConfigurationFromPartialConfiguration } from './config/async/asyncInitializationHelper'; +export { DdSdkNativeConfiguration } from './config/features/CoreConfigurationNative'; +export { RUM_DEFAULTS } from './config/features/RumConfiguration'; +export { adaptLongTaskThreshold } from './utils/longTasksUtils'; +export { version } from './version'; +export { AccountInfoSingleton } from './sdk/AccountInfoSingleton/AccountInfoSingleton'; +export { AttributesSingleton } from './sdk/AttributesSingleton/AttributesSingleton'; +export { UserInfoSingleton } from './sdk/UserInfoSingleton/UserInfoSingleton'; +export { GlobalState } from './sdk/GlobalState/GlobalState'; +export { BufferSingleton } from './sdk/DatadogProvider/Buffer/BufferSingleton'; +export { DatadogProviderState } from './sdk/DatadogProvider/DatadogProviderState'; +export { DdRumResourceTracking } from './rum/instrumentation/resourceTracking/DdRumResourceTracking'; + +export type { Attributes } from './sdk/AttributesSingleton/types'; +export type { LogsNativeConfiguration } from './config/features/LogsConfigurationNative'; +export type { RumNativeConfiguration } from './config/features/RumConfigurationNative'; +export type { TraceNativeConfiguration } from './config/features/TraceConfigurationNative'; +export type { InitializationModeForTelemetry } from './config/types/InitializationModeForTelemetry'; From dd71829e871ea1f0e0f8bac33db9b7b412ea2d4f Mon Sep 17 00:00:00 2001 From: "carlos.nogueira" Date: Tue, 27 Oct 2026 11:23:31 +0000 Subject: [PATCH 04/10] Adapt native module tests after rebase --- .../src/__tests__/DdSdkReactNative.test.tsx | 42 ++++++------ .../core/src/__tests__/importSafety.test.ts | 9 ++- .../DdRumUserInteractionTracking.test.tsx | 6 +- packages/core/src/rum/__tests__/DdRum.test.ts | 8 +-- .../Buffer/__tests__/BoundedBuffer.test.ts | 10 +-- .../__tests__/EventMapper.test.ts | 4 +- .../src/__tests__/entrypoints.test.ts | 4 +- .../offlineProvider.delegation.test.ts | 8 +-- .../offlineProvider.integration.test.ts | 4 +- .../__tests__/rumContext.integration.test.ts | 64 +++++++++---------- 10 files changed, 80 insertions(+), 79 deletions(-) diff --git a/packages/core/src/__tests__/DdSdkReactNative.test.tsx b/packages/core/src/__tests__/DdSdkReactNative.test.tsx index 1b35fc49e..6b4491b51 100644 --- a/packages/core/src/__tests__/DdSdkReactNative.test.tsx +++ b/packages/core/src/__tests__/DdSdkReactNative.test.tsx @@ -26,7 +26,7 @@ import { AttributesSingleton } from '../sdk/AttributesSingleton/AttributesSingle import { DdSdkWrapper } from '../sdk/DdSdkInternal'; import { GlobalState } from '../sdk/GlobalState/GlobalState'; import { UserInfoSingleton } from '../sdk/UserInfoSingleton/UserInfoSingleton'; -import NativeDdSdkSpec from '../specs/NativeDdSdk'; +import { getNativeDdSdk } from '../specs/NativeDdSdk'; import type { LogEvent } from '../types'; import { ErrorSource } from '../types'; import { version as sdkVersion } from '../version'; @@ -1318,8 +1318,8 @@ describe('DdSdkReactNative', () => { await DdSdkReactNative.addAttribute(key, value); // THEN - expect(NativeDdSdkSpec.addAttribute).toHaveBeenCalledTimes(1); - expect(NativeDdSdkSpec.addAttribute).toHaveBeenCalledWith(key, { + expect(getNativeDdSdk()?.addAttribute).toHaveBeenCalledTimes(1); + expect(getNativeDdSdk()?.addAttribute).toHaveBeenCalledWith(key, { value }); expect(AttributesSingleton.getInstance().getAttribute(key)).toEqual( @@ -1339,8 +1339,8 @@ describe('DdSdkReactNative', () => { await DdSdkReactNative.removeAttribute(key); // THEN - expect(NativeDdSdkSpec.removeAttribute).toHaveBeenCalledTimes(1); - expect(NativeDdSdkSpec.removeAttribute).toHaveBeenCalledWith(key); + expect(getNativeDdSdk()?.removeAttribute).toHaveBeenCalledTimes(1); + expect(getNativeDdSdk()?.removeAttribute).toHaveBeenCalledWith(key); expect(AttributesSingleton.getInstance().getAttribute(key)).toEqual( undefined ); @@ -1357,8 +1357,8 @@ describe('DdSdkReactNative', () => { await DdSdkReactNative.addAttributes(attributes); // THEN - expect(NativeDdSdkSpec.addAttributes).toHaveBeenCalledTimes(1); - expect(NativeDdSdkSpec.addAttributes).toHaveBeenCalledWith( + expect(getNativeDdSdk()?.addAttributes).toHaveBeenCalledTimes(1); + expect(getNativeDdSdk()?.addAttributes).toHaveBeenCalledWith( attributes ); expect(AttributesSingleton.getInstance().getAttributes()).toEqual({ @@ -1377,8 +1377,8 @@ describe('DdSdkReactNative', () => { await DdSdkReactNative.removeAttributes(['foo', 'baz']); // THEN - expect(NativeDdSdkSpec.removeAttributes).toHaveBeenCalledTimes(1); - expect(NativeDdSdkSpec.removeAttributes).toHaveBeenCalledWith([ + expect(getNativeDdSdk()?.removeAttributes).toHaveBeenCalledTimes(1); + expect(getNativeDdSdk()?.removeAttributes).toHaveBeenCalledWith([ 'foo', 'baz' ]); @@ -1404,8 +1404,10 @@ describe('DdSdkReactNative', () => { await DdSdkReactNative.setUserInfo(userInfo); // THEN - expect(NativeDdSdkSpec.setUserInfo).toHaveBeenCalledTimes(1); - expect(NativeDdSdkSpec.setUserInfo).toHaveBeenCalledWith(userInfo); + expect(getNativeDdSdk()?.setUserInfo).toHaveBeenCalledTimes(1); + expect(getNativeDdSdk()?.setUserInfo).toHaveBeenCalledWith( + userInfo + ); expect(UserInfoSingleton.getInstance().getUserInfo()).toEqual( userInfo ); @@ -1425,8 +1427,8 @@ describe('DdSdkReactNative', () => { await DdSdkReactNative.addUserExtraInfo(extraInfo); // THEN - expect(NativeDdSdkSpec.addUserExtraInfo).toHaveBeenCalledTimes(1); - expect(NativeDdSdkSpec.addUserExtraInfo).toHaveBeenCalledWith( + expect(getNativeDdSdk()?.addUserExtraInfo).toHaveBeenCalledTimes(1); + expect(getNativeDdSdk()?.addUserExtraInfo).toHaveBeenCalledWith( extraInfo ); expect(UserInfoSingleton.getInstance().getUserInfo()).toEqual({ @@ -1446,7 +1448,7 @@ describe('DdSdkReactNative', () => { await DdSdkReactNative.addUserExtraInfo(extraInfo); // THEN - expect(NativeDdSdkSpec.addUserExtraInfo).toHaveBeenCalledWith( + expect(getNativeDdSdk()?.addUserExtraInfo).toHaveBeenCalledWith( extraInfo ); expect(UserInfoSingleton.getInstance().getUserInfo()).toEqual({ @@ -1475,8 +1477,8 @@ describe('DdSdkReactNative', () => { await DdSdkReactNative.clearUserInfo(); // THEN - expect(NativeDdSdkSpec.clearUserInfo).toHaveBeenCalledTimes(1); - expect(NativeDdSdkSpec.setUserInfo).toHaveBeenCalled(); + expect(getNativeDdSdk()?.clearUserInfo).toHaveBeenCalledTimes(1); + expect(getNativeDdSdk()?.setUserInfo).toHaveBeenCalled(); expect( UserInfoSingleton.getInstance().getUserInfo() ).toBeUndefined(); @@ -1493,8 +1495,10 @@ describe('DdSdkReactNative', () => { DdSdkReactNative.setTrackingConsent(consent); // THEN - expect(NativeDdSdkSpec.setTrackingConsent).toHaveBeenCalledTimes(1); - expect(NativeDdSdkSpec.setTrackingConsent).toHaveBeenCalledWith( + expect(getNativeDdSdk()?.setTrackingConsent).toHaveBeenCalledTimes( + 1 + ); + expect(getNativeDdSdk()?.setTrackingConsent).toHaveBeenCalledWith( consent ); }); @@ -1506,7 +1510,7 @@ describe('DdSdkReactNative', () => { DdSdkReactNative.clearAllData(); // THEN - expect(NativeDdSdkSpec.clearAllData).toHaveBeenCalledTimes(1); + expect(getNativeDdSdk()?.clearAllData).toHaveBeenCalledTimes(1); }); }); diff --git a/packages/core/src/__tests__/importSafety.test.ts b/packages/core/src/__tests__/importSafety.test.ts index 7ff745e01..22fcbcde0 100644 --- a/packages/core/src/__tests__/importSafety.test.ts +++ b/packages/core/src/__tests__/importSafety.test.ts @@ -15,6 +15,7 @@ describe('native spec modules are import-safe (lazy)', () => { const getSpy = jest.spyOn(TurboModuleRegistry, 'get'); let mod: any; jest.isolateModules(() => { + // eslint-disable-next-line global-require, import/no-dynamic-require mod = require(modulePath); }); expect(getSpy).not.toHaveBeenCalled(); // not at import @@ -30,12 +31,13 @@ describe('DdSdkInternal is import-safe', () => { const getSpy = jest.spyOn(TurboModuleRegistry, 'get'); let internal: any; jest.isolateModules(() => { + // eslint-disable-next-line global-require internal = require('../sdk/DdSdkInternal'); }); expect(getSpy).not.toHaveBeenCalled(); // import-safe - // Touch a property on the proxy -> now it resolves - // eslint-disable-next-line @typescript-eslint/no-unused-expressions - internal.NativeDdSdk.getConstants; + const sdk = new internal.DdSdkWrapper(); + expect(getSpy).not.toHaveBeenCalled(); + sdk.getConstants(); expect(getSpy).toHaveBeenCalledWith('DdSdk'); getSpy.mockRestore(); }); @@ -45,6 +47,7 @@ describe('public barrel is import-safe', () => { it('importing the package index performs no native TurboModule lookup', () => { const getSpy = jest.spyOn(TurboModuleRegistry, 'get'); jest.isolateModules(() => { + // eslint-disable-next-line global-require require('../index'); }); expect(getSpy).not.toHaveBeenCalled(); diff --git a/packages/core/src/__tests__/rum/instrumentation/DdRumUserInteractionTracking.test.tsx b/packages/core/src/__tests__/rum/instrumentation/DdRumUserInteractionTracking.test.tsx index ef25f72ba..684ecdbd9 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 NativeDdSdkSpec from '../../../specs/NativeDdSdk'; +import { getNativeDdSdk } from '../../../specs/NativeDdSdk'; const styles = StyleSheet.create({ button: { @@ -467,7 +467,7 @@ describe('startTracking', () => { jest.setMock('react/jsx-runtime', {}); DdRumUserInteractionTracking.startTracking({}); expect(DdRumUserInteractionTracking['isTracking']).toBe(true); - expect(NativeDdSdkSpec.telemetryDebug).toBeCalledWith( + expect(getNativeDdSdk()?.telemetryDebug).toBeCalledWith( 'React jsx runtime does not export new jsx transform' ); }); @@ -477,7 +477,7 @@ describe('startTracking', () => { DdRumUserInteractionTracking.startTracking({}); expect(DdRumUserInteractionTracking['isTracking']).toBe(true); - expect(NativeDdSdkSpec.telemetryDebug).toBeCalledWith( + expect(getNativeDdSdk()?.telemetryDebug).toBeCalledWith( 'React version does not support new jsx transform' ); }); diff --git a/packages/core/src/rum/__tests__/DdRum.test.ts b/packages/core/src/rum/__tests__/DdRum.test.ts index ea0fccf67..af8049c19 100644 --- a/packages/core/src/rum/__tests__/DdRum.test.ts +++ b/packages/core/src/rum/__tests__/DdRum.test.ts @@ -12,7 +12,7 @@ import { InternalLog } from '../../InternalLog'; import { SdkVerbosity } from '../../config/types'; import { BufferSingleton } from '../../sdk/DatadogProvider/Buffer/BufferSingleton'; import { GlobalState } from '../../sdk/GlobalState/GlobalState'; -import NativeDdSdkSpec from '../../specs/NativeDdSdk'; +import { getNativeDdSdk } from '../../specs/NativeDdSdk'; import { ErrorSource } from '../../types'; import { DdRum } from '../DdRum'; import type { ActionEventMapper } from '../eventMappers/actionEventMapper'; @@ -1444,7 +1444,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(NativeDdSdkSpec.telemetryDebug).not.toHaveBeenCalled(); + expect(getNativeDdSdk()?.telemetryDebug).not.toHaveBeenCalled(); }); test('calls the native SDK when called with old API', async () => { @@ -1456,7 +1456,7 @@ describe('DdRum', () => { { user: 'me' }, 789 ); - expect(NativeDdSdkSpec.telemetryDebug).toHaveBeenCalledWith( + expect(getNativeDdSdk()?.telemetryDebug).toHaveBeenCalledWith( 'DDdRum.stopAction called with the old signature' ); }); @@ -1470,7 +1470,7 @@ describe('DdRum', () => { {}, 456 ); - expect(NativeDdSdkSpec.telemetryDebug).toHaveBeenCalledWith( + expect(getNativeDdSdk()?.telemetryDebug).toHaveBeenCalledWith( 'DDdRum.stopAction called with the old signature' ); }); 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 6a6eeb8c6..39e0b80dc 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 NativeDdSdkSpec from '../../../../specs/NativeDdSdk'; +import { getNativeDdSdk } from '../../../../specs/NativeDdSdk'; 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(NativeDdSdkSpec.telemetryError).toHaveBeenCalledWith( + expect(getNativeDdSdk()?.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(NativeDdSdkSpec.telemetryError).toHaveBeenCalledWith( + expect(getNativeDdSdk()?.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(NativeDdSdkSpec.telemetryError).toHaveBeenCalledWith( + expect(getNativeDdSdk()?.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(NativeDdSdkSpec.telemetryError).not.toHaveBeenCalled(); + expect(getNativeDdSdk()?.telemetryError).not.toHaveBeenCalled(); }); }); }); diff --git a/packages/core/src/sdk/EventMappers/__tests__/EventMapper.test.ts b/packages/core/src/sdk/EventMappers/__tests__/EventMapper.test.ts index 42077abce..7b67e59e1 100644 --- a/packages/core/src/sdk/EventMappers/__tests__/EventMapper.test.ts +++ b/packages/core/src/sdk/EventMappers/__tests__/EventMapper.test.ts @@ -4,7 +4,7 @@ * Copyright 2016-Present Datadog, Inc. */ -import NativeDdSdkSpec from '../../../specs/NativeDdSdk'; +import { getNativeDdSdk } from '../../../specs/NativeDdSdk'; import { EventMapper } from '../EventMapper'; describe('EventMapper', () => { @@ -26,7 +26,7 @@ describe('EventMapper', () => { ).toEqual({ someData: 'some data' }); - expect(NativeDdSdkSpec.telemetryDebug).toHaveBeenCalledWith( + expect(getNativeDdSdk()?.telemetryDebug).toHaveBeenCalledWith( 'Error while running the event mapper' ); }); diff --git a/packages/react-native-openfeature/src/__tests__/entrypoints.test.ts b/packages/react-native-openfeature/src/__tests__/entrypoints.test.ts index be93c4d00..c71b0fd1f 100644 --- a/packages/react-native-openfeature/src/__tests__/entrypoints.test.ts +++ b/packages/react-native-openfeature/src/__tests__/entrypoints.test.ts @@ -8,11 +8,11 @@ jest.mock('../../../core/src/specs/NativeDdFlags', () => ({ __esModule: true, - default: { + getNativeDdFlags: jest.fn().mockReturnValue({ enable: jest.fn(() => Promise.resolve()), setEvaluationContext: jest.fn(() => Promise.resolve({})), trackEvaluation: jest.fn(() => Promise.resolve()) - } + }) })); // The rules parser loads Protobuf-ES. Only the `/rules-based` entry may load it. diff --git a/packages/react-native-openfeature/src/__tests__/offlineProvider.delegation.test.ts b/packages/react-native-openfeature/src/__tests__/offlineProvider.delegation.test.ts index 34580c5c5..95e8ff80f 100644 --- a/packages/react-native-openfeature/src/__tests__/offlineProvider.delegation.test.ts +++ b/packages/react-native-openfeature/src/__tests__/offlineProvider.delegation.test.ts @@ -19,7 +19,7 @@ import { ProviderStatus } from '@openfeature/web-sdk'; -import NativeDdFlagsModule from '../../../core/src/specs/NativeDdFlags'; +import { getNativeDdFlags } from '../../../core/src/specs/NativeDdFlags'; import { DatadogCoreEvaluationProvider } from '../datadogCoreProvider'; import { DatadogOfflineOpenFeatureProvider } from '../index'; import { coreConfigurationFromString } from '../rules-based'; @@ -33,15 +33,15 @@ import { jest.mock('../../../core/src/specs/NativeDdFlags', () => ({ __esModule: true, - default: { + getNativeDdFlags: jest.fn().mockReturnValue({ enable: jest.fn(() => Promise.resolve()), setEvaluationContext: jest.fn(() => Promise.resolve({})), trackEvaluation: jest.fn(() => Promise.resolve()) - } + }) })); const NativeDdFlags = jest.mocked( - NativeDdFlagsModule as NonNullable + getNativeDdFlags() as NonNullable> ); let sequence = 0; diff --git a/packages/react-native-openfeature/src/__tests__/offlineProvider.integration.test.ts b/packages/react-native-openfeature/src/__tests__/offlineProvider.integration.test.ts index 6c5d2d4d2..f1574b71d 100644 --- a/packages/react-native-openfeature/src/__tests__/offlineProvider.integration.test.ts +++ b/packages/react-native-openfeature/src/__tests__/offlineProvider.integration.test.ts @@ -20,11 +20,11 @@ import { DatadogOfflineOpenFeatureProvider } from '../offlineProvider'; // ready-state evaluation can call trackEvaluation without throwing. jest.mock('../../../core/src/specs/NativeDdFlags', () => ({ __esModule: true, - default: { + getNativeDdFlags: jest.fn().mockReturnValue({ enable: jest.fn(() => Promise.resolve()), setEvaluationContext: jest.fn(() => Promise.resolve({})), trackEvaluation: jest.fn(() => Promise.resolve()) - } + }) })); const wireFor = (targetingKey: string): string => diff --git a/packages/react-native-openfeature/src/__tests__/rumContext.integration.test.ts b/packages/react-native-openfeature/src/__tests__/rumContext.integration.test.ts index cd181cde3..d293b507b 100644 --- a/packages/react-native-openfeature/src/__tests__/rumContext.integration.test.ts +++ b/packages/react-native-openfeature/src/__tests__/rumContext.integration.test.ts @@ -10,13 +10,13 @@ import { InMemoryProvider, OpenFeature } from '@openfeature/web-sdk'; import { InternalLog } from '../../../core/src/InternalLog'; import { SdkVerbosity } from '../../../core/src/config/types/SdkVerbosity'; import { UserInfoSingleton } from '../../../core/src/sdk/UserInfoSingleton/UserInfoSingleton'; -import NativeDdFlags from '../../../core/src/specs/NativeDdFlags'; +import { getNativeDdFlags } from '../../../core/src/specs/NativeDdFlags'; import { DatadogOpenFeatureProvider } from '../provider'; import { enrichWithRumUser } from '../rumContext'; jest.mock('../../../core/src/specs/NativeDdFlags', () => ({ __esModule: true, - default: { + getNativeDdFlags: jest.fn().mockReturnValue({ enable: jest.fn(() => Promise.resolve()), setEvaluationContext: jest.fn(() => Promise.resolve({ @@ -34,15 +34,15 @@ jest.mock('../../../core/src/specs/NativeDdFlags', () => ({ }) ), trackEvaluation: jest.fn(() => Promise.resolve()) - } + }) })); jest.mock('../../../core/src/specs/NativeDdSdk', () => ({ __esModule: true, - default: { + getNativeDdSdk: jest.fn().mockReturnValue({ setUserInfo: jest.fn(() => Promise.resolve()), clearUserInfo: jest.fn(() => Promise.resolve()) - } + }) })); let testSequence = 0; @@ -87,7 +87,7 @@ describe('explicit RUM context enrichment', () => { const { clientName } = await setupProvider({}); - expect(NativeDdFlags.setEvaluationContext).toHaveBeenCalledWith( + expect(getNativeDdFlags()?.setEvaluationContext).toHaveBeenCalledWith( clientName, '', {} @@ -119,12 +119,12 @@ describe('explicit RUM context enrichment', () => { company_name: 'Example, Inc.' }; expect(OpenFeature.getContext(domain)).toStrictEqual(enrichedContext); - expect(NativeDdFlags.setEvaluationContext).toHaveBeenCalledWith( + expect(getNativeDdFlags()?.setEvaluationContext).toHaveBeenCalledWith( clientName, 'rum-user', expectedAttributes ); - expect(NativeDdFlags.trackEvaluation).toHaveBeenCalledWith( + expect(getNativeDdFlags()?.trackEvaluation).toHaveBeenCalledWith( clientName, 'test-flag', expect.any(Object), @@ -185,12 +185,10 @@ describe('explicit RUM context enrichment', () => { "The evaluation context targetingKey is not a string. Using the anonymous subject ('') instead.", SdkVerbosity.WARN ); - expect(NativeDdFlags.setEvaluationContext).toHaveBeenCalledWith( - clientName, - '', - expectedAttributes - ); - expect(NativeDdFlags.trackEvaluation).toHaveBeenCalledWith( + expect( + getNativeDdFlags()?.setEvaluationContext + ).toHaveBeenCalledWith(clientName, '', expectedAttributes); + expect(getNativeDdFlags()?.trackEvaluation).toHaveBeenCalledWith( clientName, 'test-flag', expect.any(Object), @@ -220,12 +218,12 @@ describe('explicit RUM context enrichment', () => { targetingKey: 'rum-user', ...expectedAttributes }); - expect(NativeDdFlags.setEvaluationContext).toHaveBeenCalledWith( + expect(getNativeDdFlags()?.setEvaluationContext).toHaveBeenCalledWith( clientName, 'rum-user', expectedAttributes ); - expect(NativeDdFlags.trackEvaluation).toHaveBeenCalledWith( + expect(getNativeDdFlags()?.trackEvaluation).toHaveBeenCalledWith( clientName, 'test-flag', expect.any(Object), @@ -262,17 +260,15 @@ describe('explicit RUM context enrichment', () => { region: 'us' }; expect(OpenFeature.getContext(domain)).toStrictEqual(expectedContext); - expect(NativeDdFlags.setEvaluationContext).toHaveBeenLastCalledWith( - clientName, - 'rum-user-b', - { - email: 'b@example.com', - plan: 'pro', - region: 'us' - } - ); expect( - NativeDdFlags.trackEvaluation + getNativeDdFlags()?.setEvaluationContext + ).toHaveBeenLastCalledWith(clientName, 'rum-user-b', { + email: 'b@example.com', + plan: 'pro', + region: 'us' + }); + expect( + getNativeDdFlags()?.trackEvaluation ).toHaveBeenLastCalledWith( clientName, 'test-flag', @@ -313,10 +309,10 @@ describe('explicit RUM context enrichment', () => { applicationContext ); expect( - NativeDdFlags.setEvaluationContext + getNativeDdFlags()?.setEvaluationContext ).toHaveBeenLastCalledWith(clientName, '', { region: 'us' }); expect( - NativeDdFlags.trackEvaluation + getNativeDdFlags()?.trackEvaluation ).toHaveBeenLastCalledWith( clientName, 'test-flag', @@ -376,11 +372,9 @@ describe('explicit RUM context enrichment', () => { const { clientName } = await setupProvider(enrichWithRumUser({})); - expect(NativeDdFlags.setEvaluationContext).toHaveBeenLastCalledWith( - clientName, - 'rum-user', - {} - ); + expect( + getNativeDdFlags()?.setEvaluationContext + ).toHaveBeenLastCalledWith(clientName, 'rum-user', {}); }); it('uses undefined application fields as tombstones for RUM attributes', async () => { @@ -398,12 +392,12 @@ describe('explicit RUM context enrichment', () => { expect(OpenFeature.getContext(domain)).toStrictEqual({ targetingKey: 'rum-user' }); - expect(NativeDdFlags.setEvaluationContext).toHaveBeenCalledWith( + expect(getNativeDdFlags()?.setEvaluationContext).toHaveBeenCalledWith( clientName, 'rum-user', {} ); - expect(NativeDdFlags.trackEvaluation).toHaveBeenCalledWith( + expect(getNativeDdFlags()?.trackEvaluation).toHaveBeenCalledWith( clientName, 'test-flag', expect.any(Object), From 1836b10dda24f5aeb44dded021f4f6450d2af3d9 Mon Sep 17 00:00:00 2001 From: "carlos.nogueira" Date: Tue, 27 Oct 2026 14:31:48 +0000 Subject: [PATCH 05/10] Fix lint errors --- packages/core/src/__tests__/internalEntry.test.ts | 2 ++ packages/core/src/flags/DdFlags.ts | 3 +-- packages/core/src/flags/FlagsClient.ts | 3 +-- packages/core/src/internal.ts | 4 +--- packages/core/src/logs/DdLogs.ts | 3 +-- packages/core/src/rum/DdRum.ts | 3 +-- .../sdk/DatadogEventEmitter/DatadogDefaultEventEmitter.tsx | 1 + 7 files changed, 8 insertions(+), 11 deletions(-) diff --git a/packages/core/src/__tests__/internalEntry.test.ts b/packages/core/src/__tests__/internalEntry.test.ts index 145066dc5..0c26881fd 100644 --- a/packages/core/src/__tests__/internalEntry.test.ts +++ b/packages/core/src/__tests__/internalEntry.test.ts @@ -2,6 +2,7 @@ import { TurboModuleRegistry } from 'react-native'; describe('./internal entry', () => { it('exports the internals react-native-vega needs', () => { + // eslint-disable-next-line global-require, @typescript-eslint/no-var-requires const internal = require('../internal'); const expected = [ 'addDefaultValuesToAutoInstrumentationConfiguration', @@ -26,6 +27,7 @@ describe('./internal entry', () => { it('is import-safe (no native lookup at import)', () => { const getSpy = jest.spyOn(TurboModuleRegistry, 'get'); jest.isolateModules(() => { + // eslint-disable-next-line global-require require('../internal'); }); expect(getSpy).not.toHaveBeenCalled(); diff --git a/packages/core/src/flags/DdFlags.ts b/packages/core/src/flags/DdFlags.ts index afb794e5e..699d6e862 100644 --- a/packages/core/src/flags/DdFlags.ts +++ b/packages/core/src/flags/DdFlags.ts @@ -7,9 +7,8 @@ import { InternalLog } from '../InternalLog'; import { SdkVerbosity } from '../config/types/SdkVerbosity'; import type { DdNativeFlagsType } from '../nativeModulesTypes'; -import { getGlobalInstance } from '../utils/singletonUtils'; - import { getNativeDdFlags } from '../specs/NativeDdFlags'; +import { getGlobalInstance } from '../utils/singletonUtils'; import { FlagsClient } from './FlagsClient'; import type { DdFlagsType, FlagsConfiguration } from './types'; diff --git a/packages/core/src/flags/FlagsClient.ts b/packages/core/src/flags/FlagsClient.ts index a2bd3b4da..7182d316d 100644 --- a/packages/core/src/flags/FlagsClient.ts +++ b/packages/core/src/flags/FlagsClient.ts @@ -7,6 +7,7 @@ import { InternalLog } from '../InternalLog'; import { SdkVerbosity } from '../config/types/SdkVerbosity'; import type { DdNativeFlagsType } from '../nativeModulesTypes'; +import { getNativeDdFlags } from '../specs/NativeDdFlags'; // Imported directly (not via the module index): context matching is an internal helper, // used here only to detect a runtime context an offline precomputed configuration cannot honor. @@ -16,8 +17,6 @@ import type { ParsedFlagsConfiguration, ParsedPrecomputedConfiguration } from './configuration'; -import { getNativeDdFlags } from '../specs/NativeDdFlags'; - import { processEvaluationContext } from './internal'; import type { FlagCacheEntry } from './internal'; import type { JsonValue, EvaluationContext, FlagDetails } from './types'; diff --git a/packages/core/src/internal.ts b/packages/core/src/internal.ts index 260d1e705..e13a3bbfb 100644 --- a/packages/core/src/internal.ts +++ b/packages/core/src/internal.ts @@ -11,9 +11,7 @@ * native-free at import (must not call TurboModuleRegistry / NativeModules at * module load). */ -export { - addDefaultValuesToAutoInstrumentationConfiguration -} from './config/async/AutoInstrumentationConfiguration'; +export { addDefaultValuesToAutoInstrumentationConfiguration } from './config/async/AutoInstrumentationConfiguration'; export { buildConfigurationFromPartialConfiguration } from './config/async/asyncInitializationHelper'; export { DdSdkNativeConfiguration } from './config/features/CoreConfigurationNative'; export { RUM_DEFAULTS } from './config/features/RumConfiguration'; diff --git a/packages/core/src/logs/DdLogs.ts b/packages/core/src/logs/DdLogs.ts index 6e9264388..c5c252737 100644 --- a/packages/core/src/logs/DdLogs.ts +++ b/packages/core/src/logs/DdLogs.ts @@ -11,11 +11,10 @@ import { debugId } from '../metro/debugIdResolver'; import type { DdNativeLogsType } from '../nativeModulesTypes'; import { encodeAttributes } from '../sdk/AttributesEncoding/attributesEncoding'; import { bufferVoidNativeCall } from '../sdk/DatadogProvider/Buffer/bufferNativeCall'; +import { getNativeDdLogs } from '../specs/NativeDdLogs'; import type { ErrorSource, LogEventMapper } from '../types'; import { getGlobalInstance } from '../utils/singletonUtils'; -import { getNativeDdLogs } from '../specs/NativeDdLogs'; - import { generateEventMapper } from './eventMapper'; import type { DdLogsType, diff --git a/packages/core/src/rum/DdRum.ts b/packages/core/src/rum/DdRum.ts index f531bcfad..273d7679b 100644 --- a/packages/core/src/rum/DdRum.ts +++ b/packages/core/src/rum/DdRum.ts @@ -15,6 +15,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 { getNativeDdRum } from '../specs/NativeDdRum'; import type { ErrorSource, FeatureOperationFailure } from '../types'; import { getGlobalInstance } from '../utils/singletonUtils'; import { DefaultTimeProvider } from '../utils/time-provider/DefaultTimeProvider'; @@ -40,8 +41,6 @@ import { getTracingContext, getTracingContextForPropagators } from './instrumentation/resourceTracking/distributedTracing/distributedTracingHeaders'; -import { getNativeDdRum } from '../specs/NativeDdRum'; - import type { DdRumType, FirstPartyHost, diff --git a/packages/core/src/sdk/DatadogEventEmitter/DatadogDefaultEventEmitter.tsx b/packages/core/src/sdk/DatadogEventEmitter/DatadogDefaultEventEmitter.tsx index b55926272..7ff889b64 100644 --- a/packages/core/src/sdk/DatadogEventEmitter/DatadogDefaultEventEmitter.tsx +++ b/packages/core/src/sdk/DatadogEventEmitter/DatadogDefaultEventEmitter.tsx @@ -6,6 +6,7 @@ import { NativeModules } from 'react-native'; import { getNativeDdSdk } from '../../specs/NativeDdSdk'; + import { DatadogBatchedBridgeEventEmitter } from './DatadogBatchedBridgeEventEmitter'; import type { DatadogEventEmitter } from './DatadogEventEmitter'; import { DatadogNativeEventEmitter } from './DatadogNativeEventEmitter'; From e401ee35e5e82e1bfddc1a737720b47753e8f264 Mon Sep 17 00:00:00 2001 From: "carlos.nogueira" Date: Tue, 27 Oct 2026 15:12:12 +0000 Subject: [PATCH 06/10] Resolve native modules lazily in SDK wrappers The DdRum, DdLogs, DdTrace and DdFlags singletons are built at module evaluation, so field initializers calling the native getters still performed TurboModule lookups when importing the public barrel or the internal entry point. Resolve them through getters at call time instead. --- packages/core/src/__tests__/mock.test.ts | 4 +- .../src/__tests__/wrapperImportSafety.test.ts | 54 +++++++++++++++++++ packages/core/src/flags/DdFlags.ts | 4 +- packages/core/src/logs/DdLogs.ts | 4 +- packages/core/src/rum/DdRum.ts | 4 +- packages/core/src/trace/DdTrace.ts | 4 +- 6 files changed, 67 insertions(+), 7 deletions(-) create mode 100644 packages/core/src/__tests__/wrapperImportSafety.test.ts diff --git a/packages/core/src/__tests__/mock.test.ts b/packages/core/src/__tests__/mock.test.ts index 722998f10..570ee4a7e 100644 --- a/packages/core/src/__tests__/mock.test.ts +++ b/packages/core/src/__tests__/mock.test.ts @@ -22,9 +22,8 @@ import { DdLogs, DdRum, DdTrace, DdSdkReactNative } from '../index'; */ const privateProperties = { - DdTrace: ['nativeTrace'], + DdTrace: [], DdLogs: [ - 'nativeLogs', 'logEventMapper', 'printLogDroppedSdkNotInitialized', 'printLogDroppedByMapper', @@ -33,7 +32,6 @@ const privateProperties = { 'logWithError' ], DdRum: [ - 'nativeRum', 'errorEventMapper', 'resourceEventMapper', 'actionEventMapper', diff --git a/packages/core/src/__tests__/wrapperImportSafety.test.ts b/packages/core/src/__tests__/wrapperImportSafety.test.ts new file mode 100644 index 000000000..65b30dfd2 --- /dev/null +++ b/packages/core/src/__tests__/wrapperImportSafety.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. + */ + +/** + * The spy must be installed on the `react-native` instance from inside + * `jest.isolateModules`: lookups made by isolated modules go through that + * registry, not the one imported at the top of a test file. + */ +const requireCountingLookups = (modulePath: string) => { + let lookups: string[] = []; + let mod: any; + jest.isolateModules(() => { + // eslint-disable-next-line global-require, @typescript-eslint/no-var-requires + const { TurboModuleRegistry } = require('react-native'); + const getSpy = jest.spyOn(TurboModuleRegistry, 'get'); + // eslint-disable-next-line global-require, import/no-dynamic-require + mod = require(modulePath); + lookups = getSpy.mock.calls.map(call => call[0] as string); + }); + return { mod, lookups }; +}; + +describe('SDK wrappers are import-safe', () => { + afterEach(() => { + jest.restoreAllMocks(); + // Singletons live on globalThis, which isolateModules does not reset. + const g = globalThis as Record; + for (const symbol of Object.getOwnPropertySymbols(g)) { + if (symbol.description?.startsWith('com.datadog.reactnative')) { + delete g[symbol]; + } + } + }); + + it.each([ + ['../rum/DdRum', 'DdRum'], + ['../logs/DdLogs', 'DdLogs'], + ['../trace/DdTrace', 'DdTrace'], + ['../flags/DdFlags', 'DdFlags'] + ])('importing %s performs no native lookup', modulePath => { + expect(requireCountingLookups(modulePath).lookups).toEqual([]); + }); + + it('importing the public barrel performs no native lookup', () => { + expect(requireCountingLookups('../index').lookups).toEqual([]); + }); + + it('importing the internal entry performs no native lookup', () => { + expect(requireCountingLookups('../internal').lookups).toEqual([]); + }); +}); diff --git a/packages/core/src/flags/DdFlags.ts b/packages/core/src/flags/DdFlags.ts index 699d6e862..89090878f 100644 --- a/packages/core/src/flags/DdFlags.ts +++ b/packages/core/src/flags/DdFlags.ts @@ -19,7 +19,9 @@ const FLAGS_MODULE = 'com.datadog.reactnative.flags'; * Implementation class for {@link DdFlagsType}. Please see the interface for documentation. */ class DdFlagsWrapper implements DdFlagsType { - private nativeFlags: DdNativeFlagsType = getNativeDdFlags() as DdNativeFlagsType; + private get nativeFlags(): DdNativeFlagsType { + return getNativeDdFlags() as DdNativeFlagsType; + } private isFeatureEnabled = false; diff --git a/packages/core/src/logs/DdLogs.ts b/packages/core/src/logs/DdLogs.ts index c5c252737..38988cbe5 100644 --- a/packages/core/src/logs/DdLogs.ts +++ b/packages/core/src/logs/DdLogs.ts @@ -45,7 +45,9 @@ const isLogWithError = ( }; class DdLogsWrapper implements DdLogsType { - private nativeLogs: DdNativeLogsType = getNativeDdLogs() as DdNativeLogsType; + private get nativeLogs(): DdNativeLogsType { + return getNativeDdLogs() as DdNativeLogsType; + } private logEventMapper = generateEventMapper(undefined); debug = (...args: LogArguments | LogWithErrorArguments): Promise => { diff --git a/packages/core/src/rum/DdRum.ts b/packages/core/src/rum/DdRum.ts index 273d7679b..c7ecfe78f 100644 --- a/packages/core/src/rum/DdRum.ts +++ b/packages/core/src/rum/DdRum.ts @@ -78,7 +78,9 @@ const touchDataFromEvent = ( }; class DdRumWrapper implements DdRumType { - private nativeRum: DdNativeRumType = getNativeDdRum() as DdNativeRumType; + private get nativeRum(): DdNativeRumType { + return getNativeDdRum() as DdNativeRumType; + } private lastActionData?: { type: RumActionType; name: string }; private errorEventMapper = generateErrorEventMapper(undefined); private resourceEventMapper = generateResourceEventMapper(undefined); diff --git a/packages/core/src/trace/DdTrace.ts b/packages/core/src/trace/DdTrace.ts index 119863074..c51fc6326 100644 --- a/packages/core/src/trace/DdTrace.ts +++ b/packages/core/src/trace/DdTrace.ts @@ -22,7 +22,9 @@ const TRACE_MODULE = 'com.datadog.reactnative.trace'; const timeProvider = new DefaultTimeProvider(); class DdTraceWrapper implements DdTraceType { - private nativeTrace: DdNativeTraceType = getNativeDdTrace() as DdNativeTraceType; + private get nativeTrace(): DdNativeTraceType { + return getNativeDdTrace() as DdNativeTraceType; + } startSpan = ( operation: string, From 17e64ed50cbd008d4910964d534f78536bf1c157 Mon Sep 17 00:00:00 2001 From: "carlos.nogueira" Date: Tue, 27 Oct 2026 15:12:19 +0000 Subject: [PATCH 07/10] Don't cache missing native modules The native getters cached a null lookup result forever, so a module that was unavailable on first use could never be picked up later. Only cache a module once it is found, and drop the redundant cache in DdSdkInternal. --- .../src/__tests__/nativeSpecGetters.test.ts | 58 +++++++++++++++++++ packages/core/src/sdk/DdSdkInternal.ts | 11 ++-- packages/core/src/specs/NativeDdFlags.ts | 7 ++- packages/core/src/specs/NativeDdLogs.ts | 7 ++- packages/core/src/specs/NativeDdRum.ts | 7 ++- packages/core/src/specs/NativeDdSdk.ts | 7 ++- packages/core/src/specs/NativeDdTrace.ts | 7 ++- 7 files changed, 82 insertions(+), 22 deletions(-) create mode 100644 packages/core/src/__tests__/nativeSpecGetters.test.ts diff --git a/packages/core/src/__tests__/nativeSpecGetters.test.ts b/packages/core/src/__tests__/nativeSpecGetters.test.ts new file mode 100644 index 000000000..28e3bb0f5 --- /dev/null +++ b/packages/core/src/__tests__/nativeSpecGetters.test.ts @@ -0,0 +1,58 @@ +/* + * 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. + */ + +describe('native spec getters', () => { + afterEach(() => { + jest.restoreAllMocks(); + }); + + it.each([ + ['../specs/NativeDdSdk', 'getNativeDdSdk', 'DdSdk'], + ['../specs/NativeDdRum', 'getNativeDdRum', 'DdRum'], + ['../specs/NativeDdLogs', 'getNativeDdLogs', 'DdLogs'], + ['../specs/NativeDdTrace', 'getNativeDdTrace', 'DdTrace'], + ['../specs/NativeDdFlags', 'getNativeDdFlags', 'DdFlags'] + ])( + '%s retries a missing module and caches it once found', + (modulePath, getterName, registryName) => { + jest.isolateModules(() => { + // eslint-disable-next-line global-require, @typescript-eslint/no-var-requires + const { TurboModuleRegistry } = require('react-native'); + const nativeModule = { name: registryName }; + const getSpy = jest + .spyOn(TurboModuleRegistry, 'get') + .mockReturnValueOnce(null) + .mockReturnValue(nativeModule); + // eslint-disable-next-line global-require, import/no-dynamic-require, @typescript-eslint/no-var-requires + const getter = require(modulePath)[getterName]; + + expect(getter()).toBeNull(); + expect(getter()).toBe(nativeModule); + expect(getter()).toBe(nativeModule); + expect(getSpy).toHaveBeenCalledTimes(2); + expect(getSpy).toHaveBeenCalledWith(registryName); + }); + } + ); + + it('DdSdkWrapper picks up a module that was missing on first use', () => { + jest.isolateModules(() => { + // eslint-disable-next-line global-require, @typescript-eslint/no-var-requires + const { TurboModuleRegistry } = require('react-native'); + const nativeModule = { clearAllData: jest.fn() }; + jest.spyOn(TurboModuleRegistry, 'get') + .mockReturnValueOnce(null) + .mockReturnValue(nativeModule); + // eslint-disable-next-line global-require, @typescript-eslint/no-var-requires + const { DdSdkWrapper } = require('../sdk/DdSdkInternal'); + const sdk = new DdSdkWrapper(); + + expect(() => sdk.clearAllData()).toThrow(); + sdk.clearAllData(); + expect(nativeModule.clearAllData).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/packages/core/src/sdk/DdSdkInternal.ts b/packages/core/src/sdk/DdSdkInternal.ts index 62f2c6477..bb3f845dc 100644 --- a/packages/core/src/sdk/DdSdkInternal.ts +++ b/packages/core/src/sdk/DdSdkInternal.ts @@ -10,13 +10,10 @@ import { getNativeDdSdk } from '../specs/NativeDdSdk'; import type { AttributeEncoder } from './AttributesEncoding/types'; -let cachedNativeDdSdk: DdNativeSdkType | null | undefined; -const resolveNativeDdSdk = (): DdNativeSdkType | null => { - if (cachedNativeDdSdk === undefined) { - cachedNativeDdSdk = getNativeDdSdk() as DdNativeSdkType | null; - } - return cachedNativeDdSdk; -}; +// `getNativeDdSdk` handles caching, so a module that is missing on first use +// is picked up once it becomes available. +const resolveNativeDdSdk = (): DdNativeSdkType | null => + getNativeDdSdk() as DdNativeSdkType | null; // Lazily-backed handle. Resolution happens on first property access (runtime), // never at import — so importing this module is safe on platforms without the diff --git a/packages/core/src/specs/NativeDdFlags.ts b/packages/core/src/specs/NativeDdFlags.ts index 6efdb0fe5..5774bf0f3 100644 --- a/packages/core/src/specs/NativeDdFlags.ts +++ b/packages/core/src/specs/NativeDdFlags.ts @@ -31,15 +31,16 @@ export interface Spec extends TurboModule { ) => Promise; } -let cachedModule: Spec | null | undefined; +let cachedModule: Spec | null = null; /** - * Lazily resolves the native TurboModule on first call and caches the result. + * Lazily resolves the native TurboModule on first call and caches it once found. + * A missing module is not cached, so later calls retry the lookup. * Resolving lazily (instead of at module load) keeps this package importable on * platforms where the native module is absent (e.g. Vega). */ export const getNativeDdFlags = (): Spec | null => { - if (cachedModule === undefined) { + if (!cachedModule) { cachedModule = TurboModuleRegistry.get('DdFlags'); } return cachedModule; diff --git a/packages/core/src/specs/NativeDdLogs.ts b/packages/core/src/specs/NativeDdLogs.ts index 0933f5002..dd3b1eebb 100644 --- a/packages/core/src/specs/NativeDdLogs.ts +++ b/packages/core/src/specs/NativeDdLogs.ts @@ -107,15 +107,16 @@ export interface Spec extends TurboModule { ) => Promise; } -let cachedModule: Spec | null | undefined; +let cachedModule: Spec | null = null; /** - * Lazily resolves the native TurboModule on first call and caches the result. + * Lazily resolves the native TurboModule on first call and caches it once found. + * A missing module is not cached, so later calls retry the lookup. * Resolving lazily (instead of at module load) keeps this package importable on * platforms where the native module is absent (e.g. Vega). */ export const getNativeDdLogs = (): Spec | null => { - if (cachedModule === undefined) { + if (!cachedModule) { cachedModule = TurboModuleRegistry.get('DdLogs'); } return cachedModule; diff --git a/packages/core/src/specs/NativeDdRum.ts b/packages/core/src/specs/NativeDdRum.ts index 9da6129df..337e607e1 100644 --- a/packages/core/src/specs/NativeDdRum.ts +++ b/packages/core/src/specs/NativeDdRum.ts @@ -236,15 +236,16 @@ export interface Spec extends TurboModule { ): Promise; } -let cachedModule: Spec | null | undefined; +let cachedModule: Spec | null = null; /** - * Lazily resolves the native TurboModule on first call and caches the result. + * Lazily resolves the native TurboModule on first call and caches it once found. + * A missing module is not cached, so later calls retry the lookup. * Resolving lazily (instead of at module load) keeps this package importable on * platforms where the native module is absent (e.g. Vega). */ export const getNativeDdRum = (): Spec | null => { - if (cachedModule === undefined) { + if (!cachedModule) { cachedModule = TurboModuleRegistry.get('DdRum'); } return cachedModule; diff --git a/packages/core/src/specs/NativeDdSdk.ts b/packages/core/src/specs/NativeDdSdk.ts index 2b2c5a8e2..8233488db 100644 --- a/packages/core/src/specs/NativeDdSdk.ts +++ b/packages/core/src/specs/NativeDdSdk.ts @@ -134,15 +134,16 @@ export interface Spec extends TurboModule { removeListeners: (count: number) => void; } -let cachedModule: Spec | null | undefined; +let cachedModule: Spec | null = null; /** - * Lazily resolves the native TurboModule on first call and caches the result. + * Lazily resolves the native TurboModule on first call and caches it once found. + * A missing module is not cached, so later calls retry the lookup. * Resolving lazily (instead of at module load) keeps this package importable on * platforms where the native module is absent (e.g. Vega). */ export const getNativeDdSdk = (): Spec | null => { - if (cachedModule === undefined) { + if (!cachedModule) { cachedModule = TurboModuleRegistry.get('DdSdk'); } return cachedModule; diff --git a/packages/core/src/specs/NativeDdTrace.ts b/packages/core/src/specs/NativeDdTrace.ts index d67b71cb4..a6c8d646f 100644 --- a/packages/core/src/specs/NativeDdTrace.ts +++ b/packages/core/src/specs/NativeDdTrace.ts @@ -39,15 +39,16 @@ export interface Spec extends TurboModule { ): Promise; } -let cachedModule: Spec | null | undefined; +let cachedModule: Spec | null = null; /** - * Lazily resolves the native TurboModule on first call and caches the result. + * Lazily resolves the native TurboModule on first call and caches it once found. + * A missing module is not cached, so later calls retry the lookup. * Resolving lazily (instead of at module load) keeps this package importable on * platforms where the native module is absent (e.g. Vega). */ export const getNativeDdTrace = (): Spec | null => { - if (cachedModule === undefined) { + if (!cachedModule) { cachedModule = TurboModuleRegistry.get('DdTrace'); } return cachedModule; From 89808f9d94768ec3fba1cd26b5416e40432123b5 Mon Sep 17 00:00:00 2001 From: "carlos.nogueira" Date: Tue, 27 Oct 2026 17:39:22 +0000 Subject: [PATCH 08/10] Remove Proxy from DdSdkInternal Proxy is not available on older Android JSC and Hermes versions still supported by the SDK, so importing the package threw a ReferenceError there. Resolve the native module through a private getter on DdSdkWrapper instead, matching the other SDK wrappers. --- .../src/__tests__/wrapperImportSafety.test.ts | 53 +++++++++++++++ packages/core/src/sdk/DdSdkInternal.ts | 68 +++++++------------ 2 files changed, 79 insertions(+), 42 deletions(-) diff --git a/packages/core/src/__tests__/wrapperImportSafety.test.ts b/packages/core/src/__tests__/wrapperImportSafety.test.ts index 65b30dfd2..27dac1f57 100644 --- a/packages/core/src/__tests__/wrapperImportSafety.test.ts +++ b/packages/core/src/__tests__/wrapperImportSafety.test.ts @@ -52,3 +52,56 @@ describe('SDK wrappers are import-safe', () => { expect(requireCountingLookups('../internal').lookups).toEqual([]); }); }); + +describe('SDK wrappers do not rely on Proxy', () => { + // Older Android JSC and Hermes versions supported by the SDK lack Proxy. + it.each(['../sdk/DdSdkInternal', '../index', '../internal'])( + 'importing %s works without Proxy', + modulePath => { + const g = globalThis as Record; + const originalProxy = g.Proxy; + try { + delete g.Proxy; + expect(() => requireCountingLookups(modulePath)).not.toThrow(); + } finally { + g.Proxy = originalProxy; + } + } + ); + + it('DdSdkWrapper forwards every method to the native module', () => { + jest.isolateModules(() => { + // eslint-disable-next-line global-require, @typescript-eslint/no-var-requires + const { TurboModuleRegistry } = require('react-native'); + const nativeModule: Record = {}; + jest.spyOn(TurboModuleRegistry, 'get').mockReturnValue( + nativeModule + ); + // eslint-disable-next-line global-require, @typescript-eslint/no-var-requires + const { DdSdkWrapper } = require('../sdk/DdSdkInternal'); + const sdk = new DdSdkWrapper(); + const methods = Object.getOwnPropertyNames( + DdSdkWrapper.prototype + ).filter( + name => + name !== 'constructor' && + !name.startsWith('_') && + typeof Object.getOwnPropertyDescriptor( + DdSdkWrapper.prototype, + name + )?.value === 'function' + ); + for (const name of methods) { + nativeModule[name] = jest.fn().mockReturnValue(name); + } + + for (const name of methods) { + const arg = + name === 'initialize' ? { attributeEncoders: [] } : 'arg'; + expect(sdk[name](arg)).toBe(name); + expect(nativeModule[name]).toHaveBeenCalledTimes(1); + } + expect(methods.length).toBeGreaterThan(15); + }); + }); +}); diff --git a/packages/core/src/sdk/DdSdkInternal.ts b/packages/core/src/sdk/DdSdkInternal.ts index bb3f845dc..8d9f0d0fd 100644 --- a/packages/core/src/sdk/DdSdkInternal.ts +++ b/packages/core/src/sdk/DdSdkInternal.ts @@ -10,28 +10,6 @@ import { getNativeDdSdk } from '../specs/NativeDdSdk'; import type { AttributeEncoder } from './AttributesEncoding/types'; -// `getNativeDdSdk` handles caching, so a module that is missing on first use -// is picked up once it becomes available. -const resolveNativeDdSdk = (): DdNativeSdkType | null => - getNativeDdSdk() as DdNativeSdkType | null; - -// Lazily-backed handle. Resolution happens on first property access (runtime), -// never at import — so importing this module is safe on platforms without the -// native module (e.g. Vega). -const NativeDdSdkSpec: DdNativeSdkType = new Proxy({} as DdNativeSdkType, { - get: (_target, prop) => { - const resolved = resolveNativeDdSdk() as Record< - string | symbol, - unknown - > | null; - const value = resolved ? resolved[prop] : undefined; - // Return the raw value without binding — TurboModule methods are stateless - // so `this` is irrelevant, and returning the original function reference - // preserves Jest mock-tracking in tests. - return value; - } -}); - export type DdSdkType = DdNativeSdkType & { readonly attributeEncoders: AttributeEncoder[]; }; @@ -42,51 +20,57 @@ export class DdSdkWrapper implements DdNativeSdkType { } private _attributeEncoders: AttributeEncoder[] = []; + // Resolved on use rather than at import, so importing this module is safe + // on platforms without the native module (e.g. Vega). + private get nativeSdk(): DdNativeSdkType { + return getNativeDdSdk() as DdNativeSdkType; + } + initialize(configuration: DdSdkNativeConfiguration): Promise { this._attributeEncoders = [...configuration.attributeEncoders]; - return NativeDdSdkSpec.initialize(configuration); + return this.nativeSdk.initialize(configuration); } getConstants() { - return NativeDdSdkSpec.getConstants(); + return this.nativeSdk.getConstants(); } setUserInfo(user: object): Promise { - return NativeDdSdkSpec.setUserInfo(user); + return this.nativeSdk.setUserInfo(user); } clearUserInfo(): Promise { - return NativeDdSdkSpec.clearUserInfo(); + return this.nativeSdk.clearUserInfo(); } addUserExtraInfo(extraInfo: object): Promise { - return NativeDdSdkSpec.addUserExtraInfo(extraInfo); + return this.nativeSdk.addUserExtraInfo(extraInfo); } addAttribute(key: string, value: object): Promise { - return NativeDdSdkSpec.addAttribute(key, value); + return this.nativeSdk.addAttribute(key, value); } removeAttribute(key: string): Promise { - return NativeDdSdkSpec.removeAttribute(key); + return this.nativeSdk.removeAttribute(key); } addAttributes(attributes: object): Promise { - return NativeDdSdkSpec.addAttributes(attributes); + return this.nativeSdk.addAttributes(attributes); } removeAttributes(keys: string[]): Promise { - return NativeDdSdkSpec.removeAttributes(keys); + return this.nativeSdk.removeAttributes(keys); } setAccountInfo(account: object): Promise { - return NativeDdSdkSpec.setAccountInfo(account); + return this.nativeSdk.setAccountInfo(account); } clearAccountInfo(): Promise { - return NativeDdSdkSpec.clearAccountInfo(); + return this.nativeSdk.clearAccountInfo(); } addAccountExtraInfo(extraInfo: object): Promise { - return NativeDdSdkSpec.addAccountExtraInfo(extraInfo); + return this.nativeSdk.addAccountExtraInfo(extraInfo); } setTrackingConsent(trackingConsent: string): Promise { - return NativeDdSdkSpec.setTrackingConsent(trackingConsent); + return this.nativeSdk.setTrackingConsent(trackingConsent); } sendTelemetryLog( @@ -94,11 +78,11 @@ export class DdSdkWrapper implements DdNativeSdkType { attributes: object, config: object ): Promise { - return NativeDdSdkSpec.sendTelemetryLog(message, attributes, config); + return this.nativeSdk.sendTelemetryLog(message, attributes, config); } telemetryDebug(message: string): Promise { - return NativeDdSdkSpec.telemetryDebug(message); + return this.nativeSdk.telemetryDebug(message); } telemetryError( @@ -106,23 +90,23 @@ export class DdSdkWrapper implements DdNativeSdkType { stack: string, kind: string ): Promise { - return NativeDdSdkSpec.telemetryError(message, stack, kind); + return this.nativeSdk.telemetryError(message, stack, kind); } consumeWebviewEvent(message: string): Promise { - return NativeDdSdkSpec.consumeWebviewEvent(message); + return this.nativeSdk.consumeWebviewEvent(message); } clearAllData(): Promise { - return NativeDdSdkSpec.clearAllData(); + return this.nativeSdk.clearAllData(); } addListener(eventType: string): void { - return NativeDdSdkSpec.addListener(eventType); + return this.nativeSdk.addListener(eventType); } removeListeners(count: number): void { - return NativeDdSdkSpec.removeListeners(count); + return this.nativeSdk.removeListeners(count); } _setAttributeEncodersForTesting( From 4a5e70ff742bf66082f4ea93d972dce6256bfad4 Mon Sep 17 00:00:00 2001 From: "carlos.nogueira" Date: Tue, 27 Oct 2026 19:05:03 +0000 Subject: [PATCH 09/10] Fix import safety and bridge tests The import-safety assertions in importSafety.test.ts and internalEntry.test.ts spied on TurboModuleRegistry outside jest.isolateModules, so they could not observe lookups made by the isolated modules and passed regardless. Remove them and cover the spec modules and DdSdkWrapper in wrapperImportSafety.test.ts, which installs the spy inside the isolate. Update the DdSdkNativeBridge mocks to the getNativeDdSdk contract and cover the fallback used when NativeModules.DdSdk is missing. --- .../core/src/__tests__/importSafety.test.ts | 56 ------------------- .../core/src/__tests__/internalEntry.test.ts | 12 +--- .../src/__tests__/wrapperImportSafety.test.ts | 30 ++++++++-- .../__tests__/DdSdkNativeBridge.test.tsx | 37 +++++++++++- 4 files changed, 62 insertions(+), 73 deletions(-) delete mode 100644 packages/core/src/__tests__/importSafety.test.ts diff --git a/packages/core/src/__tests__/importSafety.test.ts b/packages/core/src/__tests__/importSafety.test.ts deleted file mode 100644 index 22fcbcde0..000000000 --- a/packages/core/src/__tests__/importSafety.test.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { TurboModuleRegistry } from 'react-native'; - -describe('native spec modules are import-safe (lazy)', () => { - const cases: Array<[string, string, string]> = [ - ['../specs/NativeDdSdk', 'getNativeDdSdk', 'DdSdk'], - ['../specs/NativeDdRum', 'getNativeDdRum', 'DdRum'], - ['../specs/NativeDdLogs', 'getNativeDdLogs', 'DdLogs'], - ['../specs/NativeDdTrace', 'getNativeDdTrace', 'DdTrace'], - ['../specs/NativeDdFlags', 'getNativeDdFlags', 'DdFlags'] - ]; - - it.each(cases)( - '%s does not resolve native at import but does on getter call', - (modulePath, getterName, registryName) => { - const getSpy = jest.spyOn(TurboModuleRegistry, 'get'); - let mod: any; - jest.isolateModules(() => { - // eslint-disable-next-line global-require, import/no-dynamic-require - mod = require(modulePath); - }); - expect(getSpy).not.toHaveBeenCalled(); // not at import - mod[getterName](); - expect(getSpy).toHaveBeenCalledWith(registryName); // only on use - getSpy.mockRestore(); - } - ); -}); - -describe('DdSdkInternal is import-safe', () => { - it('importing DdSdkInternal does not resolve native until a method is used', () => { - const getSpy = jest.spyOn(TurboModuleRegistry, 'get'); - let internal: any; - jest.isolateModules(() => { - // eslint-disable-next-line global-require - internal = require('../sdk/DdSdkInternal'); - }); - expect(getSpy).not.toHaveBeenCalled(); // import-safe - const sdk = new internal.DdSdkWrapper(); - expect(getSpy).not.toHaveBeenCalled(); - sdk.getConstants(); - expect(getSpy).toHaveBeenCalledWith('DdSdk'); - getSpy.mockRestore(); - }); -}); - -describe('public barrel is import-safe', () => { - it('importing the package index performs no native TurboModule lookup', () => { - const getSpy = jest.spyOn(TurboModuleRegistry, 'get'); - jest.isolateModules(() => { - // eslint-disable-next-line global-require - require('../index'); - }); - expect(getSpy).not.toHaveBeenCalled(); - getSpy.mockRestore(); - }); -}); diff --git a/packages/core/src/__tests__/internalEntry.test.ts b/packages/core/src/__tests__/internalEntry.test.ts index 0c26881fd..01e000762 100644 --- a/packages/core/src/__tests__/internalEntry.test.ts +++ b/packages/core/src/__tests__/internalEntry.test.ts @@ -1,5 +1,3 @@ -import { TurboModuleRegistry } from 'react-native'; - describe('./internal entry', () => { it('exports the internals react-native-vega needs', () => { // eslint-disable-next-line global-require, @typescript-eslint/no-var-requires @@ -24,13 +22,5 @@ describe('./internal entry', () => { } }); - it('is import-safe (no native lookup at import)', () => { - const getSpy = jest.spyOn(TurboModuleRegistry, 'get'); - jest.isolateModules(() => { - // eslint-disable-next-line global-require - require('../internal'); - }); - expect(getSpy).not.toHaveBeenCalled(); - getSpy.mockRestore(); - }); + // Import safety of this entry point is covered in wrapperImportSafety.test.ts. }); diff --git a/packages/core/src/__tests__/wrapperImportSafety.test.ts b/packages/core/src/__tests__/wrapperImportSafety.test.ts index 27dac1f57..53d626ed1 100644 --- a/packages/core/src/__tests__/wrapperImportSafety.test.ts +++ b/packages/core/src/__tests__/wrapperImportSafety.test.ts @@ -36,14 +36,36 @@ describe('SDK wrappers are import-safe', () => { }); it.each([ - ['../rum/DdRum', 'DdRum'], - ['../logs/DdLogs', 'DdLogs'], - ['../trace/DdTrace', 'DdTrace'], - ['../flags/DdFlags', 'DdFlags'] + '../specs/NativeDdSdk', + '../specs/NativeDdRum', + '../specs/NativeDdLogs', + '../specs/NativeDdTrace', + '../specs/NativeDdFlags', + '../sdk/DdSdkInternal', + '../rum/DdRum', + '../logs/DdLogs', + '../trace/DdTrace', + '../flags/DdFlags' ])('importing %s performs no native lookup', modulePath => { expect(requireCountingLookups(modulePath).lookups).toEqual([]); }); + it('DdSdkWrapper only resolves the native module when a method is used', () => { + jest.isolateModules(() => { + // eslint-disable-next-line global-require, @typescript-eslint/no-var-requires + const { TurboModuleRegistry } = require('react-native'); + const getSpy = jest.spyOn(TurboModuleRegistry, 'get'); + // eslint-disable-next-line global-require, @typescript-eslint/no-var-requires + const { DdSdkWrapper } = require('../sdk/DdSdkInternal'); + + const sdk = new DdSdkWrapper(); + expect(getSpy).not.toHaveBeenCalled(); + + sdk.getConstants(); + expect(getSpy).toHaveBeenCalledWith('DdSdk'); + }); + }); + it('importing the public barrel performs no native lookup', () => { expect(requireCountingLookups('../index').lookups).toEqual([]); }); diff --git a/packages/core/src/sdk/DatadogInternalBridge/__tests__/DdSdkNativeBridge.test.tsx b/packages/core/src/sdk/DatadogInternalBridge/__tests__/DdSdkNativeBridge.test.tsx index 3dff4e7ce..455f2c30e 100644 --- a/packages/core/src/sdk/DatadogInternalBridge/__tests__/DdSdkNativeBridge.test.tsx +++ b/packages/core/src/sdk/DatadogInternalBridge/__tests__/DdSdkNativeBridge.test.tsx @@ -66,7 +66,9 @@ describe('DdSdkNativeBridge', () => { }) ); - jest.mock('../../../specs/NativeDdSdk', () => mockNativeDdSdkSpec); + jest.mock('../../../specs/NativeDdSdk', () => ({ + getNativeDdSdk: jest.fn(() => mockNativeDdSdkSpec) + })); }); afterEach(() => { @@ -96,8 +98,39 @@ describe('DdSdkNativeBridge', () => { expect(ddBridge.hasNativeBridge()).toBe(true); }); + it('falls back to the spec getter when NativeModules.DdSdk is missing', () => { + const { NativeModules } = require('react-native'); + const legacyModule = NativeModules.DdSdk; + delete NativeModules.DdSdk; + try { + const { + DatadogNativeEventEmitter + } = require('../../DatadogEventEmitter/DatadogNativeEventEmitter'); + const ddBridge = require('../DdSdkInternalNativeBridge'); + const defaultEventEmitter = require('../../DatadogEventEmitter/DatadogDefaultEventEmitter'); + mockNativeEventEmitter.initialize.mockReturnValueOnce(true); + + ddBridge.registerNativeBridge( + new defaultEventEmitter.DatadogDefaultEventEmitter( + mockErrorHandler + ) + ); + + expect(DatadogNativeEventEmitter).toHaveBeenCalledWith( + mockNativeDdSdkSpec, + mockErrorHandler + ); + expect(mockErrorHandler).not.toHaveBeenCalled(); + expect(ddBridge.hasNativeBridge()).toBe(true); + } finally { + NativeModules.DdSdk = legacyModule; + } + }); + it('catches errors when native event emitter init fails', () => { - jest.mock('../../../specs/NativeDdSdk', () => undefined); + jest.mock('../../../specs/NativeDdSdk', () => ({ + getNativeDdSdk: jest.fn(() => null) + })); const ddBridge = require('../DdSdkInternalNativeBridge'); const defaultEventEmitter = require('../../DatadogEventEmitter/DatadogDefaultEventEmitter'); From 70bd1709a3f2b27bc6aa5dc5e4f7f2061c1d10c1 Mon Sep 17 00:00:00 2001 From: "carlos.nogueira" Date: Tue, 3 Nov 2026 16:37:30 +0000 Subject: [PATCH 10/10] Fix lazy flags resolution and Babel plugin test mock --- .../src/__tests__/nativeSpecGetters.test.ts | 44 +++++++++++++++++++ packages/core/src/flags/FlagsClient.ts | 4 +- .../test/handlerArguments.test.ts | 2 +- 3 files changed, 48 insertions(+), 2 deletions(-) diff --git a/packages/core/src/__tests__/nativeSpecGetters.test.ts b/packages/core/src/__tests__/nativeSpecGetters.test.ts index 28e3bb0f5..0f4756ad6 100644 --- a/packages/core/src/__tests__/nativeSpecGetters.test.ts +++ b/packages/core/src/__tests__/nativeSpecGetters.test.ts @@ -38,6 +38,50 @@ describe('native spec getters', () => { } ); + it('FlagsClient retries a missing module and uses it for fetching and tracking', async () => { + await jest.isolateModulesAsync(async () => { + // eslint-disable-next-line global-require, @typescript-eslint/no-var-requires + const { TurboModuleRegistry } = require('react-native'); + const nativeModule = { + setEvaluationContext: jest.fn().mockResolvedValue({ + feature: { key: 'feature', value: true, reason: 'STATIC' } + }), + trackEvaluation: jest.fn().mockResolvedValue(undefined) + }; + const getSpy = jest + .spyOn(TurboModuleRegistry, 'get') + .mockReturnValueOnce(null) + .mockReturnValue(nativeModule); + // eslint-disable-next-line global-require, @typescript-eslint/no-var-requires + const { FlagsClient } = require('../flags/FlagsClient'); + const client = new FlagsClient('late-native'); + const context = { targetingKey: 'user', attributes: {} }; + + expect(getSpy).not.toHaveBeenCalled(); + await expect( + client.setEvaluationContext(context) + ).rejects.toThrow(); + await expect( + client.setEvaluationContext(context) + ).resolves.toBeUndefined(); + expect(nativeModule.setEvaluationContext).toHaveBeenCalledWith( + 'late-native', + 'user', + {} + ); + expect(client.getBooleanValue('feature', false)).toBe(true); + expect(nativeModule.trackEvaluation).toHaveBeenCalledWith( + 'late-native', + 'feature', + expect.objectContaining({ value: true }), + 'user', + {} + ); + expect(getSpy).toHaveBeenCalledTimes(2); + expect(getSpy).toHaveBeenCalledWith('DdFlags'); + }); + }); + it('DdSdkWrapper picks up a module that was missing on first use', () => { jest.isolateModules(() => { // eslint-disable-next-line global-require, @typescript-eslint/no-var-requires diff --git a/packages/core/src/flags/FlagsClient.ts b/packages/core/src/flags/FlagsClient.ts index 7182d316d..5322419ca 100644 --- a/packages/core/src/flags/FlagsClient.ts +++ b/packages/core/src/flags/FlagsClient.ts @@ -83,7 +83,9 @@ interface OfflineFlagsEvaluator { } export class FlagsClient { - private nativeFlags: DdNativeFlagsType = getNativeDdFlags() as DdNativeFlagsType; + private get nativeFlags(): DdNativeFlagsType { + return getNativeDdFlags() as DdNativeFlagsType; + } private clientName: string; diff --git a/packages/react-native-babel-plugin/test/handlerArguments.test.ts b/packages/react-native-babel-plugin/test/handlerArguments.test.ts index 944d128b8..6ca0342c9 100644 --- a/packages/react-native-babel-plugin/test/handlerArguments.test.ts +++ b/packages/react-native-babel-plugin/test/handlerArguments.test.ts @@ -13,7 +13,7 @@ import plugin from '../src'; jest.mock('../../core/src/specs/NativeDdSdk', () => ({ __esModule: true, - default: { sendTelemetryLog: jest.fn() } + getNativeDdSdk: jest.fn().mockReturnValue({ sendTelemetryLog: jest.fn() }) })); const nodeRequire = createRequire(__filename);