diff --git a/packages/core/package.json b/packages/core/package.json index f0b659316..88c05e90b 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__/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(); + }); +}); 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/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 62284cc40..8d89eda06 100644 --- a/packages/core/src/flags/FlagsClient.ts +++ b/packages/core/src/flags/FlagsClient.ts @@ -8,14 +8,14 @@ import { InternalLog } from '../InternalLog'; import { SdkVerbosity } from '../config/types/SdkVerbosity'; import type { DdNativeFlagsType } from '../nativeModulesTypes'; +import { getNativeDdFlags } from '../specs/NativeDdFlags'; + import { processEvaluationContext } from './internal'; import type { FlagCacheEntry } from './internal'; import type { JsonValue, EvaluationContext, FlagDetails } from './types'; 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/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'; 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 9a7f15dae..9865e6b43 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 003d4da27..b36aeedec 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 ad6e57af0..4e69f9688 100644 --- a/packages/core/src/sdk/DdSdkInternal.ts +++ b/packages/core/src/sdk/DdSdkInternal.ts @@ -7,10 +7,30 @@ 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 NativeDdSdk: 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). Methods are bound to the resolved module. +const NativeDdSdk: DdNativeSdkType = new Proxy({} as DdNativeSdkType, { + get: (_target, prop) => { + const resolved = resolveNativeDdSdk() as Record | 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 = { 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 dbe417f44..270be1d4b 100644 --- a/packages/core/src/specs/NativeDdRum.ts +++ b/packages/core/src/specs/NativeDdRum.ts @@ -229,5 +229,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,