From 6251da25a948f8b8266cd10b866b18755a62e167 Mon Sep 17 00:00:00 2001 From: Carlos Nogueira Date: Sat, 27 Jun 2026 20:43:45 +0100 Subject: [PATCH 1/9] 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 4cb146783..f91cb69c2 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'; @@ -123,7 +123,7 @@ class BabelInteractionTracking { ): (...args: any[]) => any { return (...args: any[]) => { if (!this.telemetrySent) { - DdSdk?.sendTelemetryLog( + getNativeDdSdk()?.sendTelemetryLog( BABEL_PLUGIN_TELEMETRY, this.getTelemetryConfig(), { onlyOnce: true } @@ -147,7 +147,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 c74b86ba5..1cdcc6c6b 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 9a119cafb21d52a68b9219b1a0b688fb0d8023c4 Mon Sep 17 00:00:00 2001 From: Carlos Nogueira Date: Sat, 27 Jun 2026 20:44:02 +0100 Subject: [PATCH 2/9] 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 27bb7cfded87d44985e9f9abf11ca97452058531 Mon Sep 17 00:00:00 2001 From: Carlos Nogueira Date: Sat, 27 Jun 2026 20:46:48 +0100 Subject: [PATCH 3/9] 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 4952ad3f6cd421be597e6897468e4a99d74ccd52 Mon Sep 17 00:00:00 2001 From: "carlos.nogueira" Date: Tue, 27 Oct 2026 11:23:31 +0000 Subject: [PATCH 4/9] 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 24ad982c27f6e7a4d2b4c0bf8feb6a0eca9524c2 Mon Sep 17 00:00:00 2001 From: "carlos.nogueira" Date: Tue, 27 Oct 2026 14:31:48 +0000 Subject: [PATCH 5/9] 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 9e5fcfecc6bd493f5d23352de7f8f04957e7b4b2 Mon Sep 17 00:00:00 2001 From: "carlos.nogueira" Date: Tue, 27 Oct 2026 15:12:12 +0000 Subject: [PATCH 6/9] 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 4d30fe7b0990cbef7b6b6049a80f7fbf8dab4108 Mon Sep 17 00:00:00 2001 From: "carlos.nogueira" Date: Tue, 27 Oct 2026 15:12:19 +0000 Subject: [PATCH 7/9] 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 16ab693fb5f46dab798e13cd5d71aee937d0dd0e Mon Sep 17 00:00:00 2001 From: "carlos.nogueira" Date: Tue, 27 Oct 2026 17:39:22 +0000 Subject: [PATCH 8/9] 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 27796fe742b75ed4b9a0c1c54817601445ecc235 Mon Sep 17 00:00:00 2001 From: "carlos.nogueira" Date: Tue, 27 Oct 2026 19:05:03 +0000 Subject: [PATCH 9/9] 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');