From 2be361ca7dba859592dbe991964f43acd61cfbcf Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Mon, 20 Oct 2025 17:47:08 +0300 Subject: [PATCH 01/64] Initial native ios lib setup --- example-new-architecture/App.tsx | 3 + packages/core/ios/Sources/DdFlags.h | 24 ++++++++ packages/core/ios/Sources/DdFlags.mm | 55 +++++++++++++++++++ .../ios/Sources/DdFlagsImplementation.swift | 16 ++++++ packages/core/src/flags/DdFlags.ts | 24 ++++++++ packages/core/src/index.tsx | 2 + packages/core/src/nativeModulesTypes.ts | 6 ++ packages/core/src/specs/NativeDdFlags.ts | 20 +++++++ 8 files changed, 150 insertions(+) create mode 100644 packages/core/ios/Sources/DdFlags.h create mode 100644 packages/core/ios/Sources/DdFlags.mm create mode 100644 packages/core/ios/Sources/DdFlagsImplementation.swift create mode 100644 packages/core/src/flags/DdFlags.ts create mode 100644 packages/core/src/specs/NativeDdFlags.ts diff --git a/example-new-architecture/App.tsx b/example-new-architecture/App.tsx index e830aa345..467e9e837 100644 --- a/example-new-architecture/App.tsx +++ b/example-new-architecture/App.tsx @@ -8,6 +8,7 @@ import { RumActionType, DdLogs, DdTrace, + DdFlags, } from '@datadog/mobile-react-native'; import React from 'react'; import type {PropsWithChildren} from 'react'; @@ -32,6 +33,8 @@ import { import {APPLICATION_ID, CLIENT_TOKEN, ENVIRONMENT} from './ddCredentials'; (async () => { + console.log({constant: await DdFlags.getConstant()}); + const config = new DdSdkReactNativeConfiguration( CLIENT_TOKEN, ENVIRONMENT, diff --git a/packages/core/ios/Sources/DdFlags.h b/packages/core/ios/Sources/DdFlags.h new file mode 100644 index 000000000..d8fdab341 --- /dev/null +++ b/packages/core/ios/Sources/DdFlags.h @@ -0,0 +1,24 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +#import +@class DdFlagsImplementation; + +#ifdef RCT_NEW_ARCH_ENABLED + +#import +@interface DdFlags: NSObject + +#else + +#import +@interface DdFlags : NSObject + +#endif + +@property (nonatomic, strong) DdFlagsImplementation* ddFlagsImplementation; + +@end diff --git a/packages/core/ios/Sources/DdFlags.mm b/packages/core/ios/Sources/DdFlags.mm new file mode 100644 index 000000000..a8724d525 --- /dev/null +++ b/packages/core/ios/Sources/DdFlags.mm @@ -0,0 +1,55 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ +// Import this first to prevent require cycles +#if __has_include("DatadogSDKReactNative-Swift.h") +#import +#else +#import +#endif +#import "DdFlags.h" + + +@implementation DdFlags + +RCT_EXPORT_MODULE() + +// FIXME: This is a temporary method to test whether the native library setup is working. +RCT_REMAP_METHOD(getConstant, withResolve:(RCTPromiseResolveBlock)resolve + withRejecter:(RCTPromiseRejectBlock)reject) +{ + [self getConstant:resolve reject:reject]; +} + +// Thanks to this guard, we won't compile this code when we build for the new architecture. +#ifdef RCT_NEW_ARCH_ENABLED +- (std::shared_ptr)getTurboModule: + (const facebook::react::ObjCTurboModule::InitParams &)params +{ + return std::make_shared(params); +} +#endif + +- (DdFlagsImplementation*)ddFlagsImplementation +{ + if (_ddFlagsImplementation == nil) { + _ddFlagsImplementation = [[DdFlagsImplementation alloc] init]; + } + return _ddFlagsImplementation; +} + ++ (BOOL)requiresMainQueueSetup { + return NO; +} + +- (dispatch_queue_t)methodQueue { + return [RNQueue getSharedQueue]; +} + +- (void)getConstant:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + [self.ddFlagsImplementation getConstant:resolve reject:reject]; +} + +@end diff --git a/packages/core/ios/Sources/DdFlagsImplementation.swift b/packages/core/ios/Sources/DdFlagsImplementation.swift new file mode 100644 index 000000000..6736c8153 --- /dev/null +++ b/packages/core/ios/Sources/DdFlagsImplementation.swift @@ -0,0 +1,16 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +import Foundation + +@objc +public class DdFlagsImplementation: NSObject { + @objc + public func getConstant(_ resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) -> Void { + // FIXME: This is a temporary method to test whether the native library setup is working. + resolve(43) + } +} diff --git a/packages/core/src/flags/DdFlags.ts b/packages/core/src/flags/DdFlags.ts new file mode 100644 index 000000000..8c2680329 --- /dev/null +++ b/packages/core/src/flags/DdFlags.ts @@ -0,0 +1,24 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ +import { InternalLog } from '../InternalLog'; +import { SdkVerbosity } from '../SdkVerbosity'; +import type { DdNativeFlagsType } from '../nativeModulesTypes'; + +class DdFlagsWrapper { + // eslint-disable-next-line global-require, @typescript-eslint/no-var-requires + private nativeFlags: DdNativeFlagsType = require('../specs/NativeDdFlags') + .default; + + getConstant = (): Promise => { + InternalLog.log('Flags.getConstant()', SdkVerbosity.DEBUG); + + return this.nativeFlags.getConstant(); + }; +} + +const DdFlags = new DdFlagsWrapper(); + +export { DdFlags }; diff --git a/packages/core/src/index.tsx b/packages/core/src/index.tsx index 062fecc90..4d9e9695e 100644 --- a/packages/core/src/index.tsx +++ b/packages/core/src/index.tsx @@ -21,6 +21,7 @@ import { InternalLog } from './InternalLog'; import { ProxyConfiguration, ProxyType } from './ProxyConfiguration'; import { SdkVerbosity } from './SdkVerbosity'; import { TrackingConsent } from './TrackingConsent'; +import { DdFlags } from './flags/DdFlags'; import { DdLogs } from './logs/DdLogs'; import { DdRum } from './rum/DdRum'; import { DdBabelInteractionTracking } from './rum/instrumentation/interactionTracking/DdBabelInteractionTracking'; @@ -53,6 +54,7 @@ export { FileBasedConfiguration, InitializationMode, DdLogs, + DdFlags, DdTrace, DdRum, RumActionType, diff --git a/packages/core/src/nativeModulesTypes.ts b/packages/core/src/nativeModulesTypes.ts index b05fb6e95..4a4ee8e9d 100644 --- a/packages/core/src/nativeModulesTypes.ts +++ b/packages/core/src/nativeModulesTypes.ts @@ -4,6 +4,7 @@ * Copyright 2016-Present Datadog, Inc. */ +import type { Spec as NativeDdFlags } from './specs/NativeDdFlags'; import type { Spec as NativeDdLogs } from './specs/NativeDdLogs'; import type { Spec as NativeDdRum } from './specs/NativeDdRum'; import type { Spec as NativeDdSdk } from './specs/NativeDdSdk'; @@ -24,6 +25,11 @@ export type DdNativeLogsType = NativeDdLogs; */ export type DdNativeTraceType = NativeDdTrace; +/** + * The entry point to use Datadog's Flags feature. + */ +export type DdNativeFlagsType = NativeDdFlags; + /** * A configuration object to initialize Datadog's features. */ diff --git a/packages/core/src/specs/NativeDdFlags.ts b/packages/core/src/specs/NativeDdFlags.ts new file mode 100644 index 000000000..3c06e7c96 --- /dev/null +++ b/packages/core/src/specs/NativeDdFlags.ts @@ -0,0 +1,20 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +/* eslint-disable @typescript-eslint/ban-types */ +import type { TurboModule } from 'react-native'; +import { TurboModuleRegistry } from 'react-native'; + +/** + * Do not import this Spec directly, use DdNativeFlagsType instead. + */ +export interface Spec extends TurboModule { + // TODO: This is a temporary method to test whether the native library setup is working. + readonly getConstant: () => Promise; +} + +// eslint-disable-next-line import/no-default-export +export default TurboModuleRegistry.get('DdFlags'); From 7caa72b320940396882b14dfbcea85355bc61f83 Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Mon, 20 Oct 2025 17:53:47 +0300 Subject: [PATCH 02/64] Add DatadogFlags package to the iOS deps --- example-new-architecture/ios/Podfile | 5 + example-new-architecture/ios/Podfile.lock | 120 ++++++++++++-------- packages/core/DatadogSDKReactNative.podspec | 1 + 3 files changed, 79 insertions(+), 47 deletions(-) diff --git a/example-new-architecture/ios/Podfile b/example-new-architecture/ios/Podfile index f2f0fa09f..ead08928f 100644 --- a/example-new-architecture/ios/Podfile +++ b/example-new-architecture/ios/Podfile @@ -19,6 +19,11 @@ end target 'DdSdkReactNativeExample' do pod 'DatadogSDKReactNative', :path => '../../packages/core/DatadogSDKReactNative.podspec', :testspecs => ['Tests'] + # These dependencies are not yet released, so we need to use the branch from the feature/flags branch. + pod 'DatadogRUM', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :branch => 'feature/flags' + pod 'DatadogInternal', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :branch => 'feature/flags' + pod 'DatadogFlags', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :branch => 'feature/flags' + config = use_native_modules! use_react_native!( diff --git a/example-new-architecture/ios/Podfile.lock b/example-new-architecture/ios/Podfile.lock index c0cd90bf6..99ad9a04a 100644 --- a/example-new-architecture/ios/Podfile.lock +++ b/example-new-architecture/ios/Podfile.lock @@ -5,6 +5,8 @@ PODS: - DatadogCrashReporting (3.1.0): - DatadogInternal (= 3.1.0) - PLCrashReporter (~> 1.12.0) + - DatadogFlags (3.1.0): + - DatadogInternal (= 3.1.0) - DatadogInternal (3.1.0) - DatadogLogs (3.1.0): - DatadogInternal (= 3.1.0) @@ -13,6 +15,7 @@ PODS: - DatadogSDKReactNative (2.12.1): - DatadogCore (= 3.1.0) - DatadogCrashReporting (= 3.1.0) + - DatadogFlags (= 3.1.0) - DatadogLogs (= 3.1.0) - DatadogRUM (= 3.1.0) - DatadogTrace (= 3.1.0) @@ -40,6 +43,7 @@ PODS: - DatadogSDKReactNative/Tests (2.12.1): - DatadogCore (= 3.1.0) - DatadogCrashReporting (= 3.1.0) + - DatadogFlags (= 3.1.0) - DatadogLogs (= 3.1.0) - DatadogRUM (= 3.1.0) - DatadogTrace (= 3.1.0) @@ -1634,6 +1638,9 @@ PODS: DEPENDENCIES: - boost (from `../node_modules/react-native/third-party-podspecs/boost.podspec`) + - DatadogFlags (from `https://github.com/DataDog/dd-sdk-ios.git`, branch `feature/flags`) + - DatadogInternal (from `https://github.com/DataDog/dd-sdk-ios.git`, branch `feature/flags`) + - DatadogRUM (from `https://github.com/DataDog/dd-sdk-ios.git`, branch `feature/flags`) - DatadogSDKReactNative (from `../../packages/core/DatadogSDKReactNative.podspec`) - DatadogSDKReactNative/Tests (from `../../packages/core/DatadogSDKReactNative.podspec`) - DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`) @@ -1706,9 +1713,7 @@ SPEC REPOS: https://github.com/CocoaPods/Specs.git: - DatadogCore - DatadogCrashReporting - - DatadogInternal - DatadogLogs - - DatadogRUM - DatadogTrace - DatadogWebViewTracking - OpenTelemetrySwiftApi @@ -1718,6 +1723,15 @@ SPEC REPOS: EXTERNAL SOURCES: boost: :podspec: "../node_modules/react-native/third-party-podspecs/boost.podspec" + DatadogFlags: + :branch: feature/flags + :git: https://github.com/DataDog/dd-sdk-ios.git + DatadogInternal: + :branch: feature/flags + :git: https://github.com/DataDog/dd-sdk-ios.git + DatadogRUM: + :branch: feature/flags + :git: https://github.com/DataDog/dd-sdk-ios.git DatadogSDKReactNative: :path: "../../packages/core/DatadogSDKReactNative.podspec" DoubleConversion: @@ -1848,14 +1862,26 @@ EXTERNAL SOURCES: Yoga: :path: "../node_modules/react-native/ReactCommon/yoga" +CHECKOUT OPTIONS: + DatadogFlags: + :commit: 18ee3ce825a9806c350133179f5e38168d78e0ba + :git: https://github.com/DataDog/dd-sdk-ios.git + DatadogInternal: + :commit: 18ee3ce825a9806c350133179f5e38168d78e0ba + :git: https://github.com/DataDog/dd-sdk-ios.git + DatadogRUM: + :commit: 18ee3ce825a9806c350133179f5e38168d78e0ba + :git: https://github.com/DataDog/dd-sdk-ios.git + SPEC CHECKSUMS: boost: 1dca942403ed9342f98334bf4c3621f011aa7946 DatadogCore: d2f51c7fb4308cf3c25e55e2e7242e5d558ee71d DatadogCrashReporting: f636f1d1c534572c0b0abcdc59df244c884d825d + DatadogFlags: d4237ffb9c06096d1928dbe47aac877739bc6326 DatadogInternal: 7837b2ce3d525d429682532eeda697b181299fdc DatadogLogs: 250894b5a99da5b924a019049c0d0326823cdbd6 DatadogRUM: 0d2a60e1abb8aacfb8827ef84f6d5deb4d5026c8 - DatadogSDKReactNative: 069ea9876220b2d09b0f4b180ce571b1b6ecbb35 + DatadogSDKReactNative: d2a4348b531e604e839ca13026a92728cc2dd5ec DatadogTrace: f59e933074cd285ad7e9f5af991f8fe04b095991 DatadogWebViewTracking: 9bc92b4147aeed47eb1911451f651094aa6dd6c1 DoubleConversion: f16ae600a246532c4020132d54af21d0ddb2a385 @@ -1866,65 +1892,65 @@ SPEC CHECKSUMS: hermes-engine: 9e868dc7be781364296d6ee2f56d0c1a9ef0bb11 OpenTelemetrySwiftApi: aaee576ed961e0c348af78df58b61300e95bd104 PLCrashReporter: db59ef96fa3d25f3650040d02ec2798cffee75f2 - RCT-Folly: ea9d9256ba7f9322ef911169a9f696e5857b9e17 + RCT-Folly: 7b4f73a92ad9571b9dbdb05bb30fad927fa971e1 RCTDeprecation: ebe712bb05077934b16c6bf25228bdec34b64f83 RCTRequired: ca91e5dd26b64f577b528044c962baf171c6b716 RCTTypeSafety: e7678bd60850ca5a41df9b8dc7154638cb66871f React: 4641770499c39f45d4e7cde1eba30e081f9d8a3d React-callinvoker: 4bef67b5c7f3f68db5929ab6a4d44b8a002998ea - React-Core: a68cea3e762814e60ecc3fa521c7f14c36c99245 - React-CoreModules: d81b1eaf8066add66299bab9d23c9f00c9484c7c - React-cxxreact: 984f8b1feeca37181d4e95301fcd6f5f6501c6ab + React-Core: 0a06707a0b34982efc4a556aff5dae4b22863455 + React-CoreModules: 907334e94314189c2e5eed4877f3efe7b26d85b0 + React-cxxreact: 3a1d5e8f4faa5e09be26614e9c8bbcae8d11b73d React-debug: 817160c07dc8d24d020fbd1eac7b3558ffc08964 - React-defaultsnativemodule: 18a684542f82ce1897552a1c4b847be414c9566e - React-domnativemodule: 90bdd4ec3ab38c47cfc3461c1e9283a8507d613f - React-Fabric: f6dade7007533daeb785ba5925039d83f343be4b - React-FabricComponents: b0655cc3e1b5ae12a4a1119aa7d8308f0ad33520 - React-FabricImage: 9b157c4c01ac2bf433f834f0e1e5fe234113a576 + React-defaultsnativemodule: 814830ccbc3fb08d67d0190e63b179ee4098c67b + React-domnativemodule: 270acf94bd0960b026bc3bfb327e703665d27fb4 + React-Fabric: 64586dc191fc1c170372a638b8e722e4f1d0a09b + React-FabricComponents: b0ebd032387468ea700574c581b139f57a7497fb + React-FabricImage: 81f0e0794caf25ad1224fa406d288fbc1986607f React-featureflags: f2792b067a351d86fdc7bec23db3b9a2f2c8d26c - React-featureflagsnativemodule: 742a8325b3c821d2a1ca13a6d2a0fc72d04555e0 - React-graphics: 68969e4e49d73f89da7abef4116c9b5f466aa121 - React-hermes: ac0bcba26a5d288ebc99b500e1097da2d0297ddf - React-idlecallbacksnativemodule: d61d9c9816131bf70d3d80cd04889fc625ee523f - React-ImageManager: e906eec93a9eb6102a06576b89d48d80a4683020 - React-jserrorhandler: ac5dde01104ff444e043cad8f574ca02756e20d6 - React-jsi: 496fa2b9d63b726aeb07d0ac800064617d71211d - React-jsiexecutor: dd22ab48371b80f37a0a30d0e8915b6d0f43a893 - React-jsinspector: 4629ac376f5765e684d19064f2093e55c97fd086 - React-jsitracing: 7a1c9cd484248870cf660733cd3b8114d54c035f - React-logger: c4052eb941cca9a097ef01b59543a656dc088559 - React-Mapbuffer: 33546a3ebefbccb8770c33a1f8a5554fa96a54de - React-microtasksnativemodule: d80ff86c8902872d397d9622f1a97aadcc12cead + React-featureflagsnativemodule: 0d7091ae344d6160c0557048e127897654a5c00f + React-graphics: cbebe910e4a15b65b0bff94a4d3ed278894d6386 + React-hermes: ec18c10f5a69d49fb9b5e17ae95494e9ea13d4d3 + React-idlecallbacksnativemodule: 6b84add48971da9c40403bd1860d4896462590f2 + React-ImageManager: f2a4c01c2ccb2193e60a20c135da74c7ca4d36f2 + React-jserrorhandler: 61d205b5a7cbc57fed3371dd7eed48c97f49fc64 + React-jsi: 95f7676103137861b79b0f319467627bcfa629ee + React-jsiexecutor: 41e0fe87cda9ea3970ffb872ef10f1ff8dbd1932 + React-jsinspector: 15578208796723e5c6f39069b6e8bf36863ef6e2 + React-jsitracing: 3758cdb155ea7711f0e77952572ea62d90c69f0b + React-logger: dbca7bdfd4aa5ef69431362bde6b36d49403cb20 + React-Mapbuffer: 6efad4a606c1fae7e4a93385ee096681ef0300dc + React-microtasksnativemodule: a645237a841d733861c70b69908ab4a1707b52ad React-nativeconfig: 8efdb1ef1e9158c77098a93085438f7e7b463678 - React-NativeModulesApple: cebca2e5320a3d66e123cade23bd90a167ffce5e - React-perflogger: 72e653eb3aba9122f9e57cf012d22d2486f33358 - React-performancetimeline: cd6a9374a72001165995d2ab632f672df04076dc + React-NativeModulesApple: 958d4f6c5c2ace4c0f427cf7ef82e28ae6538a22 + React-perflogger: 9b4f13c0afe56bc7b4a0e93ec74b1150421ee22d + React-performancetimeline: 359db1cb889aa0282fafc5838331b0987c4915a9 React-RCTActionSheet: aacf2375084dea6e7c221f4a727e579f732ff342 - React-RCTAnimation: 395ab53fd064dff81507c15efb781c8684d9a585 - React-RCTAppDelegate: 345a6f1b82abc578437df0ce7e9c48740eca827c - React-RCTBlob: 13311e554c1a367de063c10ee7c5e6573b2dd1d6 - React-RCTFabric: 007b1a98201cc49b5bc6e1417d7fe3f6fc6e2b78 - React-RCTImage: 1b1f914bcc12187c49ba5d949dac38c2eb9f5cc8 - React-RCTLinking: 4ac7c42beb65e36fba0376f3498f3cd8dd0be7fa - React-RCTNetwork: 938902773add4381e84426a7aa17a2414f5f94f7 - React-RCTSettings: e848f1ba17a7a18479cf5a31d28145f567da8223 - React-RCTText: 7e98fafdde7d29e888b80f0b35544e0cb07913cf - React-RCTVibration: cd7d80affd97dc7afa62f9acd491419558b64b78 + React-RCTAnimation: d8c82deebebe3aaf7a843affac1b57cb2dc073d4 + React-RCTAppDelegate: 1774aa421a29a41a704ecaf789811ef73c4634b6 + React-RCTBlob: 70a58c11a6a3500d1a12f2e51ca4f6c99babcff8 + React-RCTFabric: 731cda82aed592aacce2d32ead69d78cde5d9274 + React-RCTImage: 5e9d655ba6a790c31e3176016f9b47fd0978fbf0 + React-RCTLinking: 2a48338252805091f7521eaf92687206401bdf2a + React-RCTNetwork: 0c1282b377257f6b1c81934f72d8a1d0c010e4c3 + React-RCTSettings: f757b679a74e5962be64ea08d7865a7debd67b40 + React-RCTText: e7d20c490b407d3b4a2daa48db4bcd8ec1032af2 + React-RCTVibration: 8228e37144ca3122a91f1de16ba8e0707159cfec React-rendererconsistency: b4917053ecbaa91469c67a4319701c9dc0d40be6 - React-rendererdebug: aa181c36dd6cf5b35511d1ed875d6638fd38f0ec + React-rendererdebug: 81becbc8852b38d9b1b68672aa504556481330d5 React-rncore: 120d21715c9b4ba8f798bffe986cb769b988dd74 - React-RuntimeApple: d033becbbd1eba6f9f6e3af6f1893030ce203edd - React-RuntimeCore: 38af280bb678e66ba000a3c3d42920b2a138eebb + React-RuntimeApple: 52ed0e9e84a7c2607a901149fb13599a3c057655 + React-RuntimeCore: ca6189d2e53d86db826e2673fe8af6571b8be157 React-runtimeexecutor: 877596f82f5632d073e121cba2d2084b76a76899 - React-RuntimeHermes: 37aad735ff21ca6de2d8450a96de1afe9f86c385 - React-runtimescheduler: 8ec34cc885281a34696ea16c4fd86892d631f38d + React-RuntimeHermes: 3b752dc5d8a1661c9d1687391d6d96acfa385549 + React-runtimescheduler: 8321bb09175ace2a4f0b3e3834637eb85bf42ebe React-timing: 331cbf9f2668c67faddfd2e46bb7f41cbd9320b9 - React-utils: ed818f19ab445000d6b5c4efa9d462449326cc9f - ReactCodegen: f853a20cc9125c5521c8766b4b49375fec20648b - ReactCommon: 300d8d9c5cb1a6cd79a67cf5d8f91e4d477195f9 + React-utils: 54df9ada708578c8ad40d92895d6fed03e0e8a9e + ReactCodegen: 21a52ccddc6479448fc91903a437dd23ddc7366c + ReactCommon: bfd3600989d79bc3acbe7704161b171a1480b9fd SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 Yoga: feb4910aba9742cfedc059e2b2902e22ffe9954a -PODFILE CHECKSUM: d9d720c99b6fffec4dd489d565a544a358a52b83 +PODFILE CHECKSUM: a25d6a4a713f5e05063f261c976c29ec22e26585 COCOAPODS: 1.16.2 diff --git a/packages/core/DatadogSDKReactNative.podspec b/packages/core/DatadogSDKReactNative.podspec index 3f5026f5a..b49ded1bd 100644 --- a/packages/core/DatadogSDKReactNative.podspec +++ b/packages/core/DatadogSDKReactNative.podspec @@ -24,6 +24,7 @@ Pod::Spec.new do |s| s.dependency 'DatadogTrace', '3.1.0' s.dependency 'DatadogRUM', '3.1.0' s.dependency 'DatadogCrashReporting', '3.1.0' + s.dependency 'DatadogFlags', '3.1.0' # DatadogWebViewTracking is not available for tvOS s.ios.dependency 'DatadogWebViewTracking', '3.1.0' From dbac6210d101a6a00df35e08e070b66170a058c2 Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Wed, 29 Oct 2025 21:07:51 +0200 Subject: [PATCH 03/64] Working flag assignment evaluation --- example-new-architecture/App.tsx | 16 +++- example-new-architecture/ios/Podfile | 12 ++- example-new-architecture/ios/Podfile.lock | 52 +++++++++---- .../core/ios/Sources/DatadogSDKReactNative.h | 2 + packages/core/ios/Sources/DdFlags.mm | 29 ++++++-- .../ios/Sources/DdFlagsImplementation.swift | 73 ++++++++++++++++++- .../Sources/DdSdkNativeInitialization.swift | 12 +++ .../src/DdSdkReactNativeConfiguration.tsx | 2 + packages/core/src/flags/DatadogFlags.ts | 31 ++++++++ packages/core/src/flags/DdFlags.ts | 24 ------ packages/core/src/flags/FlagsClient.ts | 52 +++++++++++++ packages/core/src/flags/types.ts | 25 +++++++ packages/core/src/index.tsx | 4 +- packages/core/src/specs/NativeDdFlags.ts | 18 ++++- 14 files changed, 294 insertions(+), 58 deletions(-) create mode 100644 packages/core/src/flags/DatadogFlags.ts delete mode 100644 packages/core/src/flags/DdFlags.ts create mode 100644 packages/core/src/flags/FlagsClient.ts create mode 100644 packages/core/src/flags/types.ts diff --git a/example-new-architecture/App.tsx b/example-new-architecture/App.tsx index 467e9e837..67c4ae020 100644 --- a/example-new-architecture/App.tsx +++ b/example-new-architecture/App.tsx @@ -8,7 +8,7 @@ import { RumActionType, DdLogs, DdTrace, - DdFlags, + DatadogFlags, } from '@datadog/mobile-react-native'; import React from 'react'; import type {PropsWithChildren} from 'react'; @@ -33,8 +33,6 @@ import { import {APPLICATION_ID, CLIENT_TOKEN, ENVIRONMENT} from './ddCredentials'; (async () => { - console.log({constant: await DdFlags.getConstant()}); - const config = new DdSdkReactNativeConfiguration( CLIENT_TOKEN, ENVIRONMENT, @@ -57,6 +55,18 @@ import {APPLICATION_ID, CLIENT_TOKEN, ENVIRONMENT} from './ddCredentials'; await DdLogs.info('info log'); const spanId = await DdTrace.startSpan('test span'); await DdTrace.finishSpan(spanId); + + const flagsClient = DatadogFlags.getClient(); + + await flagsClient.setEvaluationContext({ + targetingKey: 'test-user-1', + attributes: { + country: 'US', + }, + }); + + // Feature flag page: https://app.datadoghq.com/feature-flags/c715d5a6-939e-486e-be12-6f96cb36a018?environmentId=d114cd9a-79ed-4c56-bcf3-bcac9293653b + console.log({booleanValue: await flagsClient.getBooleanValue('dp-test-flag', false)}) })(); type SectionProps = PropsWithChildren<{ diff --git a/example-new-architecture/ios/Podfile b/example-new-architecture/ios/Podfile index ead08928f..b77b116d7 100644 --- a/example-new-architecture/ios/Podfile +++ b/example-new-architecture/ios/Podfile @@ -19,10 +19,14 @@ end target 'DdSdkReactNativeExample' do pod 'DatadogSDKReactNative', :path => '../../packages/core/DatadogSDKReactNative.podspec', :testspecs => ['Tests'] - # These dependencies are not yet released, so we need to use the branch from the feature/flags branch. - pod 'DatadogRUM', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :branch => 'feature/flags' - pod 'DatadogInternal', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :branch => 'feature/flags' - pod 'DatadogFlags', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :branch => 'feature/flags' + # Flags don't seem to be released yet so we pull them from the iOS SDK develop branch. + pod 'DatadogInternal', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :branch => 'develop' + pod 'DatadogCore', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :branch => 'develop' + pod 'DatadogLogs', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :branch => 'develop' + pod 'DatadogTrace', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :branch => 'develop' + pod 'DatadogRUM', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :branch => 'develop' + pod 'DatadogCrashReporting', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :branch => 'develop' + pod 'DatadogFlags', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :branch => 'develop' config = use_native_modules! diff --git a/example-new-architecture/ios/Podfile.lock b/example-new-architecture/ios/Podfile.lock index 99ad9a04a..26890694a 100644 --- a/example-new-architecture/ios/Podfile.lock +++ b/example-new-architecture/ios/Podfile.lock @@ -1638,11 +1638,15 @@ PODS: DEPENDENCIES: - boost (from `../node_modules/react-native/third-party-podspecs/boost.podspec`) - - DatadogFlags (from `https://github.com/DataDog/dd-sdk-ios.git`, branch `feature/flags`) - - DatadogInternal (from `https://github.com/DataDog/dd-sdk-ios.git`, branch `feature/flags`) - - DatadogRUM (from `https://github.com/DataDog/dd-sdk-ios.git`, branch `feature/flags`) + - DatadogCore (from `https://github.com/DataDog/dd-sdk-ios.git`, branch `develop`) + - DatadogCrashReporting (from `https://github.com/DataDog/dd-sdk-ios.git`, branch `develop`) + - DatadogFlags (from `https://github.com/DataDog/dd-sdk-ios.git`, branch `develop`) + - DatadogInternal (from `https://github.com/DataDog/dd-sdk-ios.git`, branch `develop`) + - DatadogLogs (from `https://github.com/DataDog/dd-sdk-ios.git`, branch `develop`) + - DatadogRUM (from `https://github.com/DataDog/dd-sdk-ios.git`, branch `develop`) - DatadogSDKReactNative (from `../../packages/core/DatadogSDKReactNative.podspec`) - DatadogSDKReactNative/Tests (from `../../packages/core/DatadogSDKReactNative.podspec`) + - DatadogTrace (from `https://github.com/DataDog/dd-sdk-ios.git`, branch `develop`) - DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`) - fast_float (from `../node_modules/react-native/third-party-podspecs/fast_float.podspec`) - FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`) @@ -1711,10 +1715,6 @@ DEPENDENCIES: SPEC REPOS: https://github.com/CocoaPods/Specs.git: - - DatadogCore - - DatadogCrashReporting - - DatadogLogs - - DatadogTrace - DatadogWebViewTracking - OpenTelemetrySwiftApi - PLCrashReporter @@ -1723,17 +1723,29 @@ SPEC REPOS: EXTERNAL SOURCES: boost: :podspec: "../node_modules/react-native/third-party-podspecs/boost.podspec" + DatadogCore: + :branch: develop + :git: https://github.com/DataDog/dd-sdk-ios.git + DatadogCrashReporting: + :branch: develop + :git: https://github.com/DataDog/dd-sdk-ios.git DatadogFlags: - :branch: feature/flags + :branch: develop :git: https://github.com/DataDog/dd-sdk-ios.git DatadogInternal: - :branch: feature/flags + :branch: develop + :git: https://github.com/DataDog/dd-sdk-ios.git + DatadogLogs: + :branch: develop :git: https://github.com/DataDog/dd-sdk-ios.git DatadogRUM: - :branch: feature/flags + :branch: develop :git: https://github.com/DataDog/dd-sdk-ios.git DatadogSDKReactNative: :path: "../../packages/core/DatadogSDKReactNative.podspec" + DatadogTrace: + :branch: develop + :git: https://github.com/DataDog/dd-sdk-ios.git DoubleConversion: :podspec: "../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec" fast_float: @@ -1863,14 +1875,26 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/ReactCommon/yoga" CHECKOUT OPTIONS: + DatadogCore: + :commit: 00bb35ce9eb9b50b37cf725f29b0a96da9fa46f1 + :git: https://github.com/DataDog/dd-sdk-ios.git + DatadogCrashReporting: + :commit: 00bb35ce9eb9b50b37cf725f29b0a96da9fa46f1 + :git: https://github.com/DataDog/dd-sdk-ios.git DatadogFlags: - :commit: 18ee3ce825a9806c350133179f5e38168d78e0ba + :commit: 00bb35ce9eb9b50b37cf725f29b0a96da9fa46f1 :git: https://github.com/DataDog/dd-sdk-ios.git DatadogInternal: - :commit: 18ee3ce825a9806c350133179f5e38168d78e0ba + :commit: 00bb35ce9eb9b50b37cf725f29b0a96da9fa46f1 + :git: https://github.com/DataDog/dd-sdk-ios.git + DatadogLogs: + :commit: 00bb35ce9eb9b50b37cf725f29b0a96da9fa46f1 :git: https://github.com/DataDog/dd-sdk-ios.git DatadogRUM: - :commit: 18ee3ce825a9806c350133179f5e38168d78e0ba + :commit: 00bb35ce9eb9b50b37cf725f29b0a96da9fa46f1 + :git: https://github.com/DataDog/dd-sdk-ios.git + DatadogTrace: + :commit: 00bb35ce9eb9b50b37cf725f29b0a96da9fa46f1 :git: https://github.com/DataDog/dd-sdk-ios.git SPEC CHECKSUMS: @@ -1951,6 +1975,6 @@ SPEC CHECKSUMS: SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 Yoga: feb4910aba9742cfedc059e2b2902e22ffe9954a -PODFILE CHECKSUM: a25d6a4a713f5e05063f261c976c29ec22e26585 +PODFILE CHECKSUM: 0371740c3ef15f157e2090328ec6d4963ce43cd5 COCOAPODS: 1.16.2 diff --git a/packages/core/ios/Sources/DatadogSDKReactNative.h b/packages/core/ios/Sources/DatadogSDKReactNative.h index bb724670c..0144fd508 100644 --- a/packages/core/ios/Sources/DatadogSDKReactNative.h +++ b/packages/core/ios/Sources/DatadogSDKReactNative.h @@ -6,3 +6,5 @@ // This file is imported in the auto-generated DatadogSDKReactNative-Swift.h header file. // Deleting it could result in iOS builds failing. + +#import \ No newline at end of file diff --git a/packages/core/ios/Sources/DdFlags.mm b/packages/core/ios/Sources/DdFlags.mm index a8724d525..bc5104a92 100644 --- a/packages/core/ios/Sources/DdFlags.mm +++ b/packages/core/ios/Sources/DdFlags.mm @@ -16,11 +16,24 @@ @implementation DdFlags RCT_EXPORT_MODULE() -// FIXME: This is a temporary method to test whether the native library setup is working. -RCT_REMAP_METHOD(getConstant, withResolve:(RCTPromiseResolveBlock)resolve - withRejecter:(RCTPromiseRejectBlock)reject) +RCT_REMAP_METHOD(setEvaluationContext, + withClientName:(NSString *)clientName + withTargetingKey:(NSString *)targetingKey + withAttributes:(NSDictionary *)attributes + withResolve:(RCTPromiseResolveBlock)resolve + withReject:(RCTPromiseRejectBlock)reject) { - [self getConstant:resolve reject:reject]; + [self setEvaluationContext:clientName targetingKey:targetingKey attributes:attributes resolve:resolve reject:reject]; +} + +RCT_REMAP_METHOD(getBooleanValue, + withClientName:(NSString *)clientName + withKey:(NSString *)key + withDefaultValue:(BOOL)defaultValue + withResolve:(RCTPromiseResolveBlock)resolve + withReject:(RCTPromiseRejectBlock)reject) +{ + [self getBooleanValue:clientName key:key defaultValue:defaultValue resolve:resolve reject:reject]; } // Thanks to this guard, we won't compile this code when we build for the new architecture. @@ -48,8 +61,12 @@ - (dispatch_queue_t)methodQueue { return [RNQueue getSharedQueue]; } -- (void)getConstant:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { - [self.ddFlagsImplementation getConstant:resolve reject:reject]; +- (void)setEvaluationContext:(NSString *)clientName targetingKey:(NSString *)targetingKey attributes:(NSDictionary *)attributes resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + [self.ddFlagsImplementation setEvaluationContext:clientName targetingKey:targetingKey attributes:attributes resolve:resolve reject:reject]; +} + +- (void)getBooleanValue:(NSString *)clientName key:(NSString *)key defaultValue:(BOOL)defaultValue resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + [self.ddFlagsImplementation getBooleanValue:clientName key:key defaultValue:defaultValue resolve:resolve reject:reject]; } @end diff --git a/packages/core/ios/Sources/DdFlagsImplementation.swift b/packages/core/ios/Sources/DdFlagsImplementation.swift index 6736c8153..da4f5a914 100644 --- a/packages/core/ios/Sources/DdFlagsImplementation.swift +++ b/packages/core/ios/Sources/DdFlagsImplementation.swift @@ -5,12 +5,79 @@ */ import Foundation +import DatadogFlags @objc public class DdFlagsImplementation: NSObject { + // Store a registry of client providers by name + // Use providers instead of direct clients to ensure lazy initialization + private var clientProviders: [String: () -> FlagsClientProtocol] = [:] + + private func getClient(name: String) -> FlagsClientProtocol { + if let provider = clientProviders[name] { + return provider() + } + + let client = FlagsClient.create(name: name) + + clientProviders[name] = { FlagsClient.shared(named: name) } + + return client + } + + private func parseAttributes(attributes: NSDictionary) -> [String: AnyValue] { + func asAnyValue(value: Any) -> AnyValue { + switch value { + case let s as String: return .string(s) + case let b as Bool: return .bool(b) + case let i as Int: return .int(i) + case let d as Double: return .double(d) + // FIXME: Do we even support nested evaluation contexts? + case let dict as NSDictionary: return .dictionary(parseAttributes(attributes: dict)) + case let arr as NSArray: return .array(arr.compactMap(asAnyValue)) + case is NSNull: return .null + default: return .null + } + } + + var result: [String: AnyValue] = [:] + for (key, value) in attributes { + guard let stringKey = key as? String else { + continue + } + result[stringKey] = asAnyValue(value: value) + } + return result + } + @objc - public func getConstant(_ resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) -> Void { - // FIXME: This is a temporary method to test whether the native library setup is working. - resolve(43) + public func setEvaluationContext(_ clientName: String, targetingKey: String, attributes: NSDictionary, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { + let client = getClient(name: clientName) + + let evaluationContext = FlagsEvaluationContext(targetingKey: targetingKey, attributes: parseAttributes(attributes: attributes)) + + client.setEvaluationContext(evaluationContext) { result in + switch result { + case .success: + resolve(nil) + case .failure(let error): + reject(error.localizedDescription, "", error) + } + } + } + + @objc + public func getBooleanValue( + _ clientName: String, + key: String, + defaultValue: Bool, + resolve: RCTPromiseResolveBlock, + reject: RCTPromiseRejectBlock + ) { + let client = getClient(name: clientName) + + let value = client.getBooleanValue(key: key, defaultValue: defaultValue) + + resolve(value) } } diff --git a/packages/core/ios/Sources/DdSdkNativeInitialization.swift b/packages/core/ios/Sources/DdSdkNativeInitialization.swift index 7b0f1be19..4341fdb4f 100644 --- a/packages/core/ios/Sources/DdSdkNativeInitialization.swift +++ b/packages/core/ios/Sources/DdSdkNativeInitialization.swift @@ -6,6 +6,7 @@ import Foundation import DatadogCore +import DatadogFlags import DatadogRUM import DatadogLogs import DatadogTrace @@ -87,6 +88,9 @@ public class DdSdkNativeInitialization: NSObject { let traceConfig = buildTraceConfiguration(configuration: sdkConfiguration) Trace.enable(with: traceConfig) + let flagsConfig = buildFlagsConfiguration(configuration: sdkConfiguration) + Flags.enable(with: flagsConfig) + if sdkConfiguration.nativeCrashReportEnabled ?? false { CrashReporting.enable() } @@ -215,6 +219,14 @@ public class DdSdkNativeInitialization: NSObject { return Trace.Configuration(customEndpoint: customTraceEndpointURL) } + func buildFlagsConfiguration(configuration: DdSdkConfiguration) -> Flags.Configuration { + // TODO: Handle flags configuration. + + return Flags.Configuration( + gracefulModeEnabled: true + ) + } + func setVerbosityLevel(configuration: DdSdkConfiguration) { switch configuration.verbosity?.lowercased { case "debug": diff --git a/packages/core/src/DdSdkReactNativeConfiguration.tsx b/packages/core/src/DdSdkReactNativeConfiguration.tsx index 44debb2d2..425cd7cd1 100644 --- a/packages/core/src/DdSdkReactNativeConfiguration.tsx +++ b/packages/core/src/DdSdkReactNativeConfiguration.tsx @@ -358,6 +358,8 @@ export class DdSdkReactNativeConfiguration { public customEndpoints: CustomEndpoints = DEFAULTS.getCustomEndpoints(); + // TODO: Handle flags configuration. + constructor( readonly clientToken: string, readonly env: string, diff --git a/packages/core/src/flags/DatadogFlags.ts b/packages/core/src/flags/DatadogFlags.ts new file mode 100644 index 000000000..e97b2974c --- /dev/null +++ b/packages/core/src/flags/DatadogFlags.ts @@ -0,0 +1,31 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ +import { InternalLog } from '../InternalLog'; +import { SdkVerbosity } from '../SdkVerbosity'; + +import { FlagsClient } from './FlagsClient'; +import type { DatadogFlagsConfiguration } from './types'; + +class DatadogFlagsWrapper { + getClient = (clientName: string = 'default'): FlagsClient => { + return new FlagsClient(clientName); + }; + + enable = async ( + _configuration: DatadogFlagsConfiguration + ): Promise => { + InternalLog.log( + 'No-op DatadogFlags.enable() called. Flags are initialized globally by default for now.', + SdkVerbosity.DEBUG + ); + + return Promise.resolve(); + }; +} + +const DatadogFlags = new DatadogFlagsWrapper(); + +export { DatadogFlags }; diff --git a/packages/core/src/flags/DdFlags.ts b/packages/core/src/flags/DdFlags.ts deleted file mode 100644 index 8c2680329..000000000 --- a/packages/core/src/flags/DdFlags.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. - * This product includes software developed at Datadog (https://www.datadoghq.com/). - * Copyright 2016-Present Datadog, Inc. - */ -import { InternalLog } from '../InternalLog'; -import { SdkVerbosity } from '../SdkVerbosity'; -import type { DdNativeFlagsType } from '../nativeModulesTypes'; - -class DdFlagsWrapper { - // eslint-disable-next-line global-require, @typescript-eslint/no-var-requires - private nativeFlags: DdNativeFlagsType = require('../specs/NativeDdFlags') - .default; - - getConstant = (): Promise => { - InternalLog.log('Flags.getConstant()', SdkVerbosity.DEBUG); - - return this.nativeFlags.getConstant(); - }; -} - -const DdFlags = new DdFlagsWrapper(); - -export { DdFlags }; diff --git a/packages/core/src/flags/FlagsClient.ts b/packages/core/src/flags/FlagsClient.ts new file mode 100644 index 000000000..b6f5ea275 --- /dev/null +++ b/packages/core/src/flags/FlagsClient.ts @@ -0,0 +1,52 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ +import { InternalLog } from '../InternalLog'; +import { SdkVerbosity } from '../SdkVerbosity'; +import type { DdNativeFlagsType } from '../nativeModulesTypes'; + +import type { EvaluationContext } from './types'; + +export class FlagsClient { + // eslint-disable-next-line global-require, @typescript-eslint/no-var-requires + private nativeFlags: DdNativeFlagsType = require('../specs/NativeDdFlags') + .default; + + private clientName: string; + + constructor(clientName: string = 'default') { + this.clientName = clientName; + } + + setEvaluationContext = async ( + context: EvaluationContext + ): Promise => { + const { targetingKey, attributes } = context; + + await this.nativeFlags.setEvaluationContext( + this.clientName, + targetingKey, + attributes + ); + }; + + getBooleanValue = async ( + key: string, + defaultValue: boolean + ): Promise => { + InternalLog.log( + `Flags.getBooleanValue(${key}, ${defaultValue})`, + SdkVerbosity.DEBUG + ); + + const value = await this.nativeFlags.getBooleanValue( + this.clientName, + key, + defaultValue + ); + + return value; + }; +} diff --git a/packages/core/src/flags/types.ts b/packages/core/src/flags/types.ts new file mode 100644 index 000000000..2aa5effb4 --- /dev/null +++ b/packages/core/src/flags/types.ts @@ -0,0 +1,25 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +/** + * Evaluation context for flags. + */ +export interface EvaluationContext { + targetingKey: string; + attributes: Record; +} + +/** + * Configuration settings for flags. + */ +export interface DatadogFlagsConfiguration { + gracefulModeEnabled?: boolean; + customFlagsEndpoint?: string; + customFlagsHeaders?: Record; + customExposureEndpoint?: string; + trackExposures?: boolean; + rumIntegrationEnabled?: boolean; +} diff --git a/packages/core/src/index.tsx b/packages/core/src/index.tsx index 4d9e9695e..b95c2c10a 100644 --- a/packages/core/src/index.tsx +++ b/packages/core/src/index.tsx @@ -21,7 +21,7 @@ import { InternalLog } from './InternalLog'; import { ProxyConfiguration, ProxyType } from './ProxyConfiguration'; import { SdkVerbosity } from './SdkVerbosity'; import { TrackingConsent } from './TrackingConsent'; -import { DdFlags } from './flags/DdFlags'; +import { DatadogFlags } from './flags/DatadogFlags'; import { DdLogs } from './logs/DdLogs'; import { DdRum } from './rum/DdRum'; import { DdBabelInteractionTracking } from './rum/instrumentation/interactionTracking/DdBabelInteractionTracking'; @@ -54,7 +54,7 @@ export { FileBasedConfiguration, InitializationMode, DdLogs, - DdFlags, + DatadogFlags, DdTrace, DdRum, RumActionType, diff --git a/packages/core/src/specs/NativeDdFlags.ts b/packages/core/src/specs/NativeDdFlags.ts index 3c06e7c96..130bb8e86 100644 --- a/packages/core/src/specs/NativeDdFlags.ts +++ b/packages/core/src/specs/NativeDdFlags.ts @@ -12,8 +12,22 @@ import { TurboModuleRegistry } from 'react-native'; * Do not import this Spec directly, use DdNativeFlagsType instead. */ export interface Spec extends TurboModule { - // TODO: This is a temporary method to test whether the native library setup is working. - readonly getConstant: () => Promise; + // TODO: Flags and all other features are initialized globally for now. We want to change this in the future. + // readonly enable: ( + // configuration: DatadogFlagsConfiguration + // ) => Promise; + + readonly setEvaluationContext: ( + clientName: string, + targetingKey: string, + attributes: { [key: string]: unknown } + ) => Promise; + + readonly getBooleanValue: ( + clientName: string, + key: string, + defaultValue: boolean + ) => Promise; } // eslint-disable-next-line import/no-default-export From 58d40aef8b242e80bbe5a243511bc256e18bc744 Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Tue, 4 Nov 2025 19:39:48 +0200 Subject: [PATCH 04/64] Pass configuration from RN to Swift SDK --- example-new-architecture/App.tsx | 3 ++ .../core/ios/Sources/DdSdkConfiguration.swift | 6 ++- .../Sources/DdSdkNativeInitialization.swift | 13 ++---- .../ios/Sources/RNDdSdkConfiguration.swift | 42 +++++++++++++++++-- packages/core/src/DdSdkReactNative.tsx | 3 +- .../src/DdSdkReactNativeConfiguration.tsx | 3 +- packages/core/src/flags/types.ts | 1 + packages/core/src/types.tsx | 4 +- 8 files changed, 58 insertions(+), 17 deletions(-) diff --git a/example-new-architecture/App.tsx b/example-new-architecture/App.tsx index 67c4ae020..badd7700d 100644 --- a/example-new-architecture/App.tsx +++ b/example-new-architecture/App.tsx @@ -46,6 +46,9 @@ import {APPLICATION_ID, CLIENT_TOKEN, ENVIRONMENT} from './ddCredentials'; config.telemetrySampleRate = 100; config.uploadFrequency = UploadFrequency.FREQUENT; config.batchSize = BatchSize.SMALL; + config.flagsConfiguration = { + enabled: true, + }; await DdSdkReactNative.initialize(config); await DdRum.startView('main', 'Main'); setTimeout(async () => { diff --git a/packages/core/ios/Sources/DdSdkConfiguration.swift b/packages/core/ios/Sources/DdSdkConfiguration.swift index 7a76cf39d..823783483 100644 --- a/packages/core/ios/Sources/DdSdkConfiguration.swift +++ b/packages/core/ios/Sources/DdSdkConfiguration.swift @@ -6,6 +6,7 @@ import Foundation import DatadogCore +import DatadogFlags import DatadogInternal import DatadogRUM @@ -76,6 +77,7 @@ public class DdSdkConfiguration: NSObject { public var trackWatchdogTerminations: Bool public var batchProcessingLevel: Datadog.Configuration.BatchProcessingLevel public var initialResourceThreshold: Double? = nil + public var configurationForFlags: Flags.Configuration? = nil public init( clientToken: String, @@ -108,7 +110,8 @@ public class DdSdkConfiguration: NSObject { appHangThreshold: Double?, trackWatchdogTerminations: Bool, batchProcessingLevel: Datadog.Configuration.BatchProcessingLevel, - initialResourceThreshold: Double? + initialResourceThreshold: Double?, + configurationForFlags: Flags.Configuration? ) { self.clientToken = clientToken self.env = env @@ -141,6 +144,7 @@ public class DdSdkConfiguration: NSObject { self.trackWatchdogTerminations = trackWatchdogTerminations self.batchProcessingLevel = batchProcessingLevel self.initialResourceThreshold = initialResourceThreshold + self.configurationForFlags = configurationForFlags } } diff --git a/packages/core/ios/Sources/DdSdkNativeInitialization.swift b/packages/core/ios/Sources/DdSdkNativeInitialization.swift index 4341fdb4f..943b40d36 100644 --- a/packages/core/ios/Sources/DdSdkNativeInitialization.swift +++ b/packages/core/ios/Sources/DdSdkNativeInitialization.swift @@ -88,8 +88,9 @@ public class DdSdkNativeInitialization: NSObject { let traceConfig = buildTraceConfiguration(configuration: sdkConfiguration) Trace.enable(with: traceConfig) - let flagsConfig = buildFlagsConfiguration(configuration: sdkConfiguration) - Flags.enable(with: flagsConfig) + if let configurationForFlags = sdkConfiguration.configurationForFlags { + Flags.enable(with: configurationForFlags) + } if sdkConfiguration.nativeCrashReportEnabled ?? false { CrashReporting.enable() @@ -219,14 +220,6 @@ public class DdSdkNativeInitialization: NSObject { return Trace.Configuration(customEndpoint: customTraceEndpointURL) } - func buildFlagsConfiguration(configuration: DdSdkConfiguration) -> Flags.Configuration { - // TODO: Handle flags configuration. - - return Flags.Configuration( - gracefulModeEnabled: true - ) - } - func setVerbosityLevel(configuration: DdSdkConfiguration) { switch configuration.verbosity?.lowercased { case "debug": diff --git a/packages/core/ios/Sources/RNDdSdkConfiguration.swift b/packages/core/ios/Sources/RNDdSdkConfiguration.swift index 66437c481..123b0e535 100644 --- a/packages/core/ios/Sources/RNDdSdkConfiguration.swift +++ b/packages/core/ios/Sources/RNDdSdkConfiguration.swift @@ -5,6 +5,7 @@ */ import DatadogCore +import DatadogFlags import DatadogRUM import DatadogInternal import Foundation @@ -43,6 +44,7 @@ extension NSDictionary { let trackWatchdogTerminations = object(forKey: "trackWatchdogTerminations") as? Bool let batchProcessingLevel = object(forKey: "batchProcessingLevel") as? NSString let initialResourceThreshold = object(forKey: "initialResourceThreshold") as? Double + let configurationForFlags = object(forKey: "configurationForFlags") as? NSDictionary return DdSdkConfiguration( clientToken: (clientToken != nil) ? clientToken! : String(), @@ -75,7 +77,8 @@ extension NSDictionary { appHangThreshold: appHangThreshold, trackWatchdogTerminations: trackWatchdogTerminations ?? DefaultConfiguration.trackWatchdogTerminations, batchProcessingLevel: batchProcessingLevel.asBatchProcessingLevel(), - initialResourceThreshold: initialResourceThreshold + initialResourceThreshold: initialResourceThreshold, + configurationForFlags: configurationForFlags?.asConfigurationForFlags() ) } @@ -96,7 +99,38 @@ extension NSDictionary { reactNativeVersion: reactNativeVersion ) } - + + func asConfigurationForFlags() -> Flags.Configuration? { + let enabled = object(forKey: "enabled") as! Bool + + if !enabled { + return nil + } + + let gracefulModeEnabled = object(forKey: "gracefulModeEnabled") as? Bool + let customFlagsHeaders = object(forKey: "customFlagsHeaders") as? [String: String] + let trackExposures = object(forKey: "trackExposures") as? Bool + let rumIntegrationEnabled = object(forKey: "rumIntegrationEnabled") as? Bool + + var customFlagsEndpointURL: URL? = nil + if let customFlagsEndpoint = object(forKey: "customFlagsEndpoint") as? String { + customFlagsEndpointURL = URL(string: "\(customFlagsEndpoint)/precompute-assignments" as String) + } + var customExposureEndpointURL: URL? = nil + if let customExposureEndpoint = object(forKey: "customExposureEndpoint") as? String { + customExposureEndpointURL = URL(string: "\(customExposureEndpoint)/api/v2/exposures" as String) + } + + return Flags.Configuration( + gracefulModeEnabled: gracefulModeEnabled ?? true, + customFlagsEndpoint: customFlagsEndpointURL, + customFlagsHeaders: customFlagsHeaders, + customExposureEndpoint: customExposureEndpointURL, + trackExposures: trackExposures ?? true, + rumIntegrationEnabled: rumIntegrationEnabled ?? true + ) + } + func asCustomEndpoints() -> CustomEndpoints { let rum = object(forKey: "rum") as? NSString let logs = object(forKey: "logs") as? NSString @@ -244,6 +278,7 @@ extension Dictionary where Key == String, Value == AnyObject { let trackWatchdogTerminations = configuration["trackWatchdogTerminations"] as? Bool let batchProcessingLevel = configuration["batchProcessingLevel"] as? NSString let initialResourceThreshold = configuration["initialResourceThreshold"] as? Double + let configurationForFlags = configuration["configurationForFlags"] as? NSDictionary return DdSdkConfiguration( clientToken: clientToken ?? String(), @@ -279,7 +314,8 @@ extension Dictionary where Key == String, Value == AnyObject { appHangThreshold: appHangThreshold, trackWatchdogTerminations: trackWatchdogTerminations ?? DefaultConfiguration.trackWatchdogTerminations, batchProcessingLevel: batchProcessingLevel.asBatchProcessingLevel(), - initialResourceThreshold: initialResourceThreshold + initialResourceThreshold: initialResourceThreshold, + configurationForFlags: configurationForFlags?.asConfigurationForFlags() ) } } diff --git a/packages/core/src/DdSdkReactNative.tsx b/packages/core/src/DdSdkReactNative.tsx index 1838542df..7dd9aecd7 100644 --- a/packages/core/src/DdSdkReactNative.tsx +++ b/packages/core/src/DdSdkReactNative.tsx @@ -356,7 +356,8 @@ export class DdSdkReactNative { configuration.resourceTracingSamplingRate, configuration.trackWatchdogTerminations, configuration.batchProcessingLevel, - configuration.initialResourceThreshold + configuration.initialResourceThreshold, + configuration.flagsConfiguration ); }; diff --git a/packages/core/src/DdSdkReactNativeConfiguration.tsx b/packages/core/src/DdSdkReactNativeConfiguration.tsx index 425cd7cd1..01db282c8 100644 --- a/packages/core/src/DdSdkReactNativeConfiguration.tsx +++ b/packages/core/src/DdSdkReactNativeConfiguration.tsx @@ -7,6 +7,7 @@ import type { ProxyConfiguration } from './ProxyConfiguration'; import type { SdkVerbosity } from './SdkVerbosity'; import { TrackingConsent } from './TrackingConsent'; +import type { DatadogFlagsConfiguration } from './flags/types'; import type { ActionEventMapper } from './rum/eventMappers/actionEventMapper'; import type { ErrorEventMapper } from './rum/eventMappers/errorEventMapper'; import type { ResourceEventMapper } from './rum/eventMappers/resourceEventMapper'; @@ -358,7 +359,7 @@ export class DdSdkReactNativeConfiguration { public customEndpoints: CustomEndpoints = DEFAULTS.getCustomEndpoints(); - // TODO: Handle flags configuration. + public flagsConfiguration?: DatadogFlagsConfiguration; constructor( readonly clientToken: string, diff --git a/packages/core/src/flags/types.ts b/packages/core/src/flags/types.ts index 2aa5effb4..e7ef4a081 100644 --- a/packages/core/src/flags/types.ts +++ b/packages/core/src/flags/types.ts @@ -16,6 +16,7 @@ export interface EvaluationContext { * Configuration settings for flags. */ export interface DatadogFlagsConfiguration { + enabled: boolean; gracefulModeEnabled?: boolean; customFlagsEndpoint?: string; customFlagsHeaders?: Record; diff --git a/packages/core/src/types.tsx b/packages/core/src/types.tsx index bad19d429..5a0289e21 100644 --- a/packages/core/src/types.tsx +++ b/packages/core/src/types.tsx @@ -5,6 +5,7 @@ */ import type { BatchProcessingLevel } from './DdSdkReactNativeConfiguration'; +import type { DatadogFlagsConfiguration } from './flags/types'; declare global { // eslint-disable-next-line no-var, vars-on-top @@ -69,7 +70,8 @@ export class DdSdkConfiguration { readonly resourceTracingSamplingRate: number, readonly trackWatchdogTerminations: boolean | undefined, readonly batchProcessingLevel: BatchProcessingLevel, // eslint-disable-next-line no-empty-function - readonly initialResourceThreshold: number | undefined + readonly initialResourceThreshold: number | undefined, + readonly configurationForFlags: DatadogFlagsConfiguration | undefined ) {} } From ab48ffe7f10a47a677c1f6e0207a3c3e45f22b81 Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Thu, 13 Nov 2025 16:29:59 +0200 Subject: [PATCH 05/64] Pin Datadog* ios packages to a specific commit --- example-new-architecture/ios/Podfile | 14 +++---- example-new-architecture/ios/Podfile.lock | 46 +++++++++++------------ 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/example-new-architecture/ios/Podfile b/example-new-architecture/ios/Podfile index b77b116d7..3de68ea46 100644 --- a/example-new-architecture/ios/Podfile +++ b/example-new-architecture/ios/Podfile @@ -20,13 +20,13 @@ target 'DdSdkReactNativeExample' do pod 'DatadogSDKReactNative', :path => '../../packages/core/DatadogSDKReactNative.podspec', :testspecs => ['Tests'] # Flags don't seem to be released yet so we pull them from the iOS SDK develop branch. - pod 'DatadogInternal', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :branch => 'develop' - pod 'DatadogCore', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :branch => 'develop' - pod 'DatadogLogs', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :branch => 'develop' - pod 'DatadogTrace', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :branch => 'develop' - pod 'DatadogRUM', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :branch => 'develop' - pod 'DatadogCrashReporting', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :branch => 'develop' - pod 'DatadogFlags', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :branch => 'develop' + pod 'DatadogInternal', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :commit => '00bb35ce9' + pod 'DatadogCore', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :commit => '00bb35ce9' + pod 'DatadogLogs', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :commit => '00bb35ce9' + pod 'DatadogTrace', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :commit => '00bb35ce9' + pod 'DatadogRUM', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :commit => '00bb35ce9' + pod 'DatadogCrashReporting', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :commit => '00bb35ce9' + pod 'DatadogFlags', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :commit => '00bb35ce9' config = use_native_modules! diff --git a/example-new-architecture/ios/Podfile.lock b/example-new-architecture/ios/Podfile.lock index ce0b1f972..0de14a56a 100644 --- a/example-new-architecture/ios/Podfile.lock +++ b/example-new-architecture/ios/Podfile.lock @@ -1638,15 +1638,15 @@ PODS: DEPENDENCIES: - boost (from `../node_modules/react-native/third-party-podspecs/boost.podspec`) - - DatadogCore (from `https://github.com/DataDog/dd-sdk-ios.git`, branch `develop`) - - DatadogCrashReporting (from `https://github.com/DataDog/dd-sdk-ios.git`, branch `develop`) - - DatadogFlags (from `https://github.com/DataDog/dd-sdk-ios.git`, branch `develop`) - - DatadogInternal (from `https://github.com/DataDog/dd-sdk-ios.git`, branch `develop`) - - DatadogLogs (from `https://github.com/DataDog/dd-sdk-ios.git`, branch `develop`) - - DatadogRUM (from `https://github.com/DataDog/dd-sdk-ios.git`, branch `develop`) + - DatadogCore (from `https://github.com/DataDog/dd-sdk-ios.git`, commit `00bb35ce9`) + - DatadogCrashReporting (from `https://github.com/DataDog/dd-sdk-ios.git`, commit `00bb35ce9`) + - DatadogFlags (from `https://github.com/DataDog/dd-sdk-ios.git`, commit `00bb35ce9`) + - DatadogInternal (from `https://github.com/DataDog/dd-sdk-ios.git`, commit `00bb35ce9`) + - DatadogLogs (from `https://github.com/DataDog/dd-sdk-ios.git`, commit `00bb35ce9`) + - DatadogRUM (from `https://github.com/DataDog/dd-sdk-ios.git`, commit `00bb35ce9`) - DatadogSDKReactNative (from `../../packages/core/DatadogSDKReactNative.podspec`) - DatadogSDKReactNative/Tests (from `../../packages/core/DatadogSDKReactNative.podspec`) - - DatadogTrace (from `https://github.com/DataDog/dd-sdk-ios.git`, branch `develop`) + - DatadogTrace (from `https://github.com/DataDog/dd-sdk-ios.git`, commit `00bb35ce9`) - DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`) - fast_float (from `../node_modules/react-native/third-party-podspecs/fast_float.podspec`) - FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`) @@ -1724,27 +1724,27 @@ EXTERNAL SOURCES: boost: :podspec: "../node_modules/react-native/third-party-podspecs/boost.podspec" DatadogCore: - :branch: develop + :commit: 00bb35ce9 :git: https://github.com/DataDog/dd-sdk-ios.git DatadogCrashReporting: - :branch: develop + :commit: 00bb35ce9 :git: https://github.com/DataDog/dd-sdk-ios.git DatadogFlags: - :branch: develop + :commit: 00bb35ce9 :git: https://github.com/DataDog/dd-sdk-ios.git DatadogInternal: - :branch: develop + :commit: 00bb35ce9 :git: https://github.com/DataDog/dd-sdk-ios.git DatadogLogs: - :branch: develop + :commit: 00bb35ce9 :git: https://github.com/DataDog/dd-sdk-ios.git DatadogRUM: - :branch: develop + :commit: 00bb35ce9 :git: https://github.com/DataDog/dd-sdk-ios.git DatadogSDKReactNative: :path: "../../packages/core/DatadogSDKReactNative.podspec" DatadogTrace: - :branch: develop + :commit: 00bb35ce9 :git: https://github.com/DataDog/dd-sdk-ios.git DoubleConversion: :podspec: "../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec" @@ -1876,25 +1876,25 @@ EXTERNAL SOURCES: CHECKOUT OPTIONS: DatadogCore: - :commit: 00bb35ce9eb9b50b37cf725f29b0a96da9fa46f1 + :commit: 00bb35ce9 :git: https://github.com/DataDog/dd-sdk-ios.git DatadogCrashReporting: - :commit: 00bb35ce9eb9b50b37cf725f29b0a96da9fa46f1 + :commit: 00bb35ce9 :git: https://github.com/DataDog/dd-sdk-ios.git DatadogFlags: - :commit: 00bb35ce9eb9b50b37cf725f29b0a96da9fa46f1 + :commit: 00bb35ce9 :git: https://github.com/DataDog/dd-sdk-ios.git DatadogInternal: - :commit: 00bb35ce9eb9b50b37cf725f29b0a96da9fa46f1 + :commit: 00bb35ce9 :git: https://github.com/DataDog/dd-sdk-ios.git DatadogLogs: - :commit: 00bb35ce9eb9b50b37cf725f29b0a96da9fa46f1 + :commit: 00bb35ce9 :git: https://github.com/DataDog/dd-sdk-ios.git DatadogRUM: - :commit: 00bb35ce9eb9b50b37cf725f29b0a96da9fa46f1 + :commit: 00bb35ce9 :git: https://github.com/DataDog/dd-sdk-ios.git DatadogTrace: - :commit: 00bb35ce9eb9b50b37cf725f29b0a96da9fa46f1 + :commit: 00bb35ce9 :git: https://github.com/DataDog/dd-sdk-ios.git SPEC CHECKSUMS: @@ -1905,7 +1905,7 @@ SPEC CHECKSUMS: DatadogInternal: 7837b2ce3d525d429682532eeda697b181299fdc DatadogLogs: 250894b5a99da5b924a019049c0d0326823cdbd6 DatadogRUM: 0d2a60e1abb8aacfb8827ef84f6d5deb4d5026c8 - DatadogSDKReactNative: 2f11191b56e18680f633bfb125ab1832b327d9b4 + DatadogSDKReactNative: e74b171da3b103bf9b2fd372f480fa71c230830d DatadogTrace: f59e933074cd285ad7e9f5af991f8fe04b095991 DatadogWebViewTracking: 9bc92b4147aeed47eb1911451f651094aa6dd6c1 DoubleConversion: f16ae600a246532c4020132d54af21d0ddb2a385 @@ -1975,6 +1975,6 @@ SPEC CHECKSUMS: SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 Yoga: feb4910aba9742cfedc059e2b2902e22ffe9954a -PODFILE CHECKSUM: 0371740c3ef15f157e2090328ec6d4963ce43cd5 +PODFILE CHECKSUM: f3ab84af7c758d164059ea0f336689464122369d COCOAPODS: 1.16.2 From 2fd23d1f64b930b6dcbd80683359ff2d713e05ef Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Thu, 13 Nov 2025 20:30:12 +0200 Subject: [PATCH 06/64] Update deps to 3.2.0 --- example-new-architecture/ios/Podfile | 15 +-- example-new-architecture/ios/Podfile.lock | 128 ++++++++++---------- packages/core/DatadogSDKReactNative.podspec | 14 +-- 3 files changed, 82 insertions(+), 75 deletions(-) diff --git a/example-new-architecture/ios/Podfile b/example-new-architecture/ios/Podfile index 3de68ea46..6a063cedd 100644 --- a/example-new-architecture/ios/Podfile +++ b/example-new-architecture/ios/Podfile @@ -20,13 +20,14 @@ target 'DdSdkReactNativeExample' do pod 'DatadogSDKReactNative', :path => '../../packages/core/DatadogSDKReactNative.podspec', :testspecs => ['Tests'] # Flags don't seem to be released yet so we pull them from the iOS SDK develop branch. - pod 'DatadogInternal', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :commit => '00bb35ce9' - pod 'DatadogCore', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :commit => '00bb35ce9' - pod 'DatadogLogs', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :commit => '00bb35ce9' - pod 'DatadogTrace', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :commit => '00bb35ce9' - pod 'DatadogRUM', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :commit => '00bb35ce9' - pod 'DatadogCrashReporting', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :commit => '00bb35ce9' - pod 'DatadogFlags', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :commit => '00bb35ce9' + pod 'DatadogInternal', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :commit => '2dfe2d0ff' + pod 'DatadogCore', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :commit => '2dfe2d0ff' + pod 'DatadogLogs', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :commit => '2dfe2d0ff' + pod 'DatadogTrace', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :commit => '2dfe2d0ff' + pod 'DatadogRUM', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :commit => '2dfe2d0ff' + pod 'DatadogCrashReporting', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :commit => '2dfe2d0ff' + pod 'DatadogFlags', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :commit => '2dfe2d0ff' + pod 'DatadogWebViewTracking', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :commit => '2dfe2d0ff' config = use_native_modules! diff --git a/example-new-architecture/ios/Podfile.lock b/example-new-architecture/ios/Podfile.lock index 0de14a56a..93fa5e8f1 100644 --- a/example-new-architecture/ios/Podfile.lock +++ b/example-new-architecture/ios/Podfile.lock @@ -1,25 +1,25 @@ PODS: - boost (1.84.0) - - DatadogCore (3.1.0): - - DatadogInternal (= 3.1.0) - - DatadogCrashReporting (3.1.0): - - DatadogInternal (= 3.1.0) + - DatadogCore (3.2.0): + - DatadogInternal (= 3.2.0) + - DatadogCrashReporting (3.2.0): + - DatadogInternal (= 3.2.0) - PLCrashReporter (~> 1.12.0) - - DatadogFlags (3.1.0): - - DatadogInternal (= 3.1.0) - - DatadogInternal (3.1.0) - - DatadogLogs (3.1.0): - - DatadogInternal (= 3.1.0) - - DatadogRUM (3.1.0): - - DatadogInternal (= 3.1.0) + - DatadogFlags (3.2.0): + - DatadogInternal (= 3.2.0) + - DatadogInternal (3.2.0) + - DatadogLogs (3.2.0): + - DatadogInternal (= 3.2.0) + - DatadogRUM (3.2.0): + - DatadogInternal (= 3.2.0) - DatadogSDKReactNative (2.13.0): - - DatadogCore (= 3.1.0) - - DatadogCrashReporting (= 3.1.0) - - DatadogFlags (= 3.1.0) - - DatadogLogs (= 3.1.0) - - DatadogRUM (= 3.1.0) - - DatadogTrace (= 3.1.0) - - DatadogWebViewTracking (= 3.1.0) + - DatadogCore (= 3.2.0) + - DatadogCrashReporting (= 3.2.0) + - DatadogFlags (= 3.2.0) + - DatadogLogs (= 3.2.0) + - DatadogRUM (= 3.2.0) + - DatadogTrace (= 3.2.0) + - DatadogWebViewTracking (= 3.2.0) - DoubleConversion - glog - hermes-engine @@ -41,13 +41,13 @@ PODS: - ReactCommon/turbomodule/core - Yoga - DatadogSDKReactNative/Tests (2.13.0): - - DatadogCore (= 3.1.0) - - DatadogCrashReporting (= 3.1.0) - - DatadogFlags (= 3.1.0) - - DatadogLogs (= 3.1.0) - - DatadogRUM (= 3.1.0) - - DatadogTrace (= 3.1.0) - - DatadogWebViewTracking (= 3.1.0) + - DatadogCore (= 3.2.0) + - DatadogCrashReporting (= 3.2.0) + - DatadogFlags (= 3.2.0) + - DatadogLogs (= 3.2.0) + - DatadogRUM (= 3.2.0) + - DatadogTrace (= 3.2.0) + - DatadogWebViewTracking (= 3.2.0) - DoubleConversion - glog - hermes-engine @@ -68,11 +68,11 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - DatadogTrace (3.1.0): - - DatadogInternal (= 3.1.0) + - DatadogTrace (3.2.0): + - DatadogInternal (= 3.2.0) - OpenTelemetrySwiftApi (= 1.13.1) - - DatadogWebViewTracking (3.1.0): - - DatadogInternal (= 3.1.0) + - DatadogWebViewTracking (3.2.0): + - DatadogInternal (= 3.2.0) - DoubleConversion (1.1.6) - fast_float (6.1.4) - FBLazyVector (0.76.9) @@ -1638,15 +1638,16 @@ PODS: DEPENDENCIES: - boost (from `../node_modules/react-native/third-party-podspecs/boost.podspec`) - - DatadogCore (from `https://github.com/DataDog/dd-sdk-ios.git`, commit `00bb35ce9`) - - DatadogCrashReporting (from `https://github.com/DataDog/dd-sdk-ios.git`, commit `00bb35ce9`) - - DatadogFlags (from `https://github.com/DataDog/dd-sdk-ios.git`, commit `00bb35ce9`) - - DatadogInternal (from `https://github.com/DataDog/dd-sdk-ios.git`, commit `00bb35ce9`) - - DatadogLogs (from `https://github.com/DataDog/dd-sdk-ios.git`, commit `00bb35ce9`) - - DatadogRUM (from `https://github.com/DataDog/dd-sdk-ios.git`, commit `00bb35ce9`) + - DatadogCore (from `https://github.com/DataDog/dd-sdk-ios.git`, commit `2dfe2d0ff`) + - DatadogCrashReporting (from `https://github.com/DataDog/dd-sdk-ios.git`, commit `2dfe2d0ff`) + - DatadogFlags (from `https://github.com/DataDog/dd-sdk-ios.git`, commit `2dfe2d0ff`) + - DatadogInternal (from `https://github.com/DataDog/dd-sdk-ios.git`, commit `2dfe2d0ff`) + - DatadogLogs (from `https://github.com/DataDog/dd-sdk-ios.git`, commit `2dfe2d0ff`) + - DatadogRUM (from `https://github.com/DataDog/dd-sdk-ios.git`, commit `2dfe2d0ff`) - DatadogSDKReactNative (from `../../packages/core/DatadogSDKReactNative.podspec`) - DatadogSDKReactNative/Tests (from `../../packages/core/DatadogSDKReactNative.podspec`) - - DatadogTrace (from `https://github.com/DataDog/dd-sdk-ios.git`, commit `00bb35ce9`) + - DatadogTrace (from `https://github.com/DataDog/dd-sdk-ios.git`, commit `2dfe2d0ff`) + - DatadogWebViewTracking (from `https://github.com/DataDog/dd-sdk-ios.git`, commit `2dfe2d0ff`) - DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`) - fast_float (from `../node_modules/react-native/third-party-podspecs/fast_float.podspec`) - FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`) @@ -1715,7 +1716,6 @@ DEPENDENCIES: SPEC REPOS: https://github.com/CocoaPods/Specs.git: - - DatadogWebViewTracking - OpenTelemetrySwiftApi - PLCrashReporter - SocketRocket @@ -1724,27 +1724,30 @@ EXTERNAL SOURCES: boost: :podspec: "../node_modules/react-native/third-party-podspecs/boost.podspec" DatadogCore: - :commit: 00bb35ce9 + :commit: 2dfe2d0ff :git: https://github.com/DataDog/dd-sdk-ios.git DatadogCrashReporting: - :commit: 00bb35ce9 + :commit: 2dfe2d0ff :git: https://github.com/DataDog/dd-sdk-ios.git DatadogFlags: - :commit: 00bb35ce9 + :commit: 2dfe2d0ff :git: https://github.com/DataDog/dd-sdk-ios.git DatadogInternal: - :commit: 00bb35ce9 + :commit: 2dfe2d0ff :git: https://github.com/DataDog/dd-sdk-ios.git DatadogLogs: - :commit: 00bb35ce9 + :commit: 2dfe2d0ff :git: https://github.com/DataDog/dd-sdk-ios.git DatadogRUM: - :commit: 00bb35ce9 + :commit: 2dfe2d0ff :git: https://github.com/DataDog/dd-sdk-ios.git DatadogSDKReactNative: :path: "../../packages/core/DatadogSDKReactNative.podspec" DatadogTrace: - :commit: 00bb35ce9 + :commit: 2dfe2d0ff + :git: https://github.com/DataDog/dd-sdk-ios.git + DatadogWebViewTracking: + :commit: 2dfe2d0ff :git: https://github.com/DataDog/dd-sdk-ios.git DoubleConversion: :podspec: "../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec" @@ -1876,38 +1879,41 @@ EXTERNAL SOURCES: CHECKOUT OPTIONS: DatadogCore: - :commit: 00bb35ce9 + :commit: 2dfe2d0ff :git: https://github.com/DataDog/dd-sdk-ios.git DatadogCrashReporting: - :commit: 00bb35ce9 + :commit: 2dfe2d0ff :git: https://github.com/DataDog/dd-sdk-ios.git DatadogFlags: - :commit: 00bb35ce9 + :commit: 2dfe2d0ff :git: https://github.com/DataDog/dd-sdk-ios.git DatadogInternal: - :commit: 00bb35ce9 + :commit: 2dfe2d0ff :git: https://github.com/DataDog/dd-sdk-ios.git DatadogLogs: - :commit: 00bb35ce9 + :commit: 2dfe2d0ff :git: https://github.com/DataDog/dd-sdk-ios.git DatadogRUM: - :commit: 00bb35ce9 + :commit: 2dfe2d0ff :git: https://github.com/DataDog/dd-sdk-ios.git DatadogTrace: - :commit: 00bb35ce9 + :commit: 2dfe2d0ff + :git: https://github.com/DataDog/dd-sdk-ios.git + DatadogWebViewTracking: + :commit: 2dfe2d0ff :git: https://github.com/DataDog/dd-sdk-ios.git SPEC CHECKSUMS: boost: 1dca942403ed9342f98334bf4c3621f011aa7946 - DatadogCore: d2f51c7fb4308cf3c25e55e2e7242e5d558ee71d - DatadogCrashReporting: f636f1d1c534572c0b0abcdc59df244c884d825d - DatadogFlags: d4237ffb9c06096d1928dbe47aac877739bc6326 - DatadogInternal: 7837b2ce3d525d429682532eeda697b181299fdc - DatadogLogs: 250894b5a99da5b924a019049c0d0326823cdbd6 - DatadogRUM: 0d2a60e1abb8aacfb8827ef84f6d5deb4d5026c8 - DatadogSDKReactNative: e74b171da3b103bf9b2fd372f480fa71c230830d - DatadogTrace: f59e933074cd285ad7e9f5af991f8fe04b095991 - DatadogWebViewTracking: 9bc92b4147aeed47eb1911451f651094aa6dd6c1 + DatadogCore: 8f360d91ec79c8799e753ec1abe3169911ee50fa + DatadogCrashReporting: 0a392c47eaf0294df7e04c9ae113e97612af63b3 + DatadogFlags: 2a06a1258e78686246e5383ef90ee71f53dc6bff + DatadogInternal: 291b5ad0142280a651ab196ebd30dcd1d37cf6ff + DatadogLogs: 3ce559b785c8013911be4777845e46202046e618 + DatadogRUM: f26899d603f1797b4a41e00b5ee1aa0f6c972ef2 + DatadogSDKReactNative: 8b0b92cb7ec31e34c97ec6671908662d9203f0ca + DatadogTrace: 95e5fb69876ce3ab223dd8ef3036605c89db3cdf + DatadogWebViewTracking: 6b2d5e5ab1e85481e458f9d0130294078549558b DoubleConversion: f16ae600a246532c4020132d54af21d0ddb2a385 fast_float: 06eeec4fe712a76acc9376682e4808b05ce978b6 FBLazyVector: 7605ea4810e0e10ae4815292433c09bf4324ba45 @@ -1975,6 +1981,6 @@ SPEC CHECKSUMS: SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 Yoga: feb4910aba9742cfedc059e2b2902e22ffe9954a -PODFILE CHECKSUM: f3ab84af7c758d164059ea0f336689464122369d +PODFILE CHECKSUM: bda441670d020698768356464e7ec5c2b0573608 COCOAPODS: 1.16.2 diff --git a/packages/core/DatadogSDKReactNative.podspec b/packages/core/DatadogSDKReactNative.podspec index ccdda06c4..2a65176b0 100644 --- a/packages/core/DatadogSDKReactNative.podspec +++ b/packages/core/DatadogSDKReactNative.podspec @@ -19,15 +19,15 @@ Pod::Spec.new do |s| s.dependency "React-Core" # /!\ Remember to keep the versions in sync with DatadogSDKReactNativeSessionReplay.podspec - s.dependency 'DatadogCore', '3.1.0' - s.dependency 'DatadogLogs', '3.1.0' - s.dependency 'DatadogTrace', '3.1.0' - s.dependency 'DatadogRUM', '3.1.0' - s.dependency 'DatadogCrashReporting', '3.1.0' - s.dependency 'DatadogFlags', '3.1.0' + s.dependency 'DatadogCore', '3.2.0' + s.dependency 'DatadogLogs', '3.2.0' + s.dependency 'DatadogTrace', '3.2.0' + s.dependency 'DatadogRUM', '3.2.0' + s.dependency 'DatadogCrashReporting', '3.2.0' + s.dependency 'DatadogFlags', '3.2.0' # DatadogWebViewTracking is not available for tvOS - s.ios.dependency 'DatadogWebViewTracking', '3.1.0' + s.ios.dependency 'DatadogWebViewTracking', '3.2.0' s.test_spec 'Tests' do |test_spec| test_spec.source_files = 'ios/Tests/**/*.{swift,json}' From cb038d08bc35ac0868ae1264ba4239e2186f5549 Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Thu, 13 Nov 2025 22:27:12 +0200 Subject: [PATCH 07/64] Support other feature flag types --- example-new-architecture/App.tsx | 34 +++++++- packages/core/ios/Sources/DdFlags.mm | 43 +++++++++- .../ios/Sources/DdFlagsImplementation.swift | 81 +++++++++++++++---- packages/core/src/flags/FlagsClient.ts | 42 ++++++++-- packages/core/src/specs/NativeDdFlags.ts | 18 +++++ 5 files changed, 193 insertions(+), 25 deletions(-) diff --git a/example-new-architecture/App.tsx b/example-new-architecture/App.tsx index badd7700d..4e64b7c91 100644 --- a/example-new-architecture/App.tsx +++ b/example-new-architecture/App.tsx @@ -68,8 +68,6 @@ import {APPLICATION_ID, CLIENT_TOKEN, ENVIRONMENT} from './ddCredentials'; }, }); - // Feature flag page: https://app.datadoghq.com/feature-flags/c715d5a6-939e-486e-be12-6f96cb36a018?environmentId=d114cd9a-79ed-4c56-bcf3-bcac9293653b - console.log({booleanValue: await flagsClient.getBooleanValue('dp-test-flag', false)}) })(); type SectionProps = PropsWithChildren<{ @@ -103,6 +101,35 @@ function Section({children, title}: SectionProps): React.JSX.Element { } function App(): React.JSX.Element { + const [flagValues, setFlagValues] = React.useState>({}); + + React.useEffect(() => { + (async () => { + const flagsClient = DatadogFlags.getClient(); + + const [booleanValue, stringValue, jsonValue, integerValue, numberValue] = await Promise.all([ + flagsClient.getBooleanValue('rn-sdk-test-boolean-flag', false), // https://app.datadoghq.com/feature-flags/046d0e70-626d-41e1-8314-3f009fb79b7a?environmentId=d114cd9a-79ed-4c56-bcf3-bcac9293653b + flagsClient.getStringValue('rn-sdk-test-string-flag', 'default-value'), // https://app.datadoghq.com/feature-flags/80756d8f-a375-437a-a023-b490c91cd506?environmentId=d114cd9a-79ed-4c56-bcf3-bcac9293653b + flagsClient.getObjectValue('rn-sdk-test-json-flag', {default: 'value'}), // https://app.datadoghq.com/feature-flags/bcf75cd6-96d8-4182-8871-0b66ad76127a?environmentId=d114cd9a-79ed-4c56-bcf3-bcac9293653b + flagsClient.getNumberValue('rn-sdk-test-integer-flag', 0), // https://app.datadoghq.com/feature-flags/5cd5a154-65ef-4c15-b539-e68c93eaa7f1?environmentId=d114cd9a-79ed-4c56-bcf3-bcac9293653b + flagsClient.getNumberValue('rn-sdk-test-number-flag', 0.7), // https://app.datadoghq.com/feature-flags/62b3129a-f9fa-49c0-b8a2-1a772b183bf7?environmentId=d114cd9a-79ed-4c56-bcf3-bcac9293653b + ]); + + const newValues = { + boolean: booleanValue, + json: jsonValue, + integer: integerValue, + string: stringValue, + number: numberValue, + }; + + console.log({newValues}); + + setFlagValues(newValues); + })().catch(console.error); + }, []); + + const isDarkMode = useColorScheme() === 'dark'; const backgroundStyle = { @@ -123,6 +150,9 @@ function App(): React.JSX.Element { style={{ backgroundColor: isDarkMode ? Colors.black : Colors.white, }}> + + {JSON.stringify(flagValues, (key, value) => value === undefined ? '' : value, 2)} +
Edit App.tsx to change this screen and then come back to see your edits. diff --git a/packages/core/ios/Sources/DdFlags.mm b/packages/core/ios/Sources/DdFlags.mm index bc5104a92..39086bee5 100644 --- a/packages/core/ios/Sources/DdFlags.mm +++ b/packages/core/ios/Sources/DdFlags.mm @@ -27,7 +27,7 @@ @implementation DdFlags } RCT_REMAP_METHOD(getBooleanValue, - withClientName:(NSString *)clientName + getBooleanValueWithClientName:(NSString *)clientName withKey:(NSString *)key withDefaultValue:(BOOL)defaultValue withResolve:(RCTPromiseResolveBlock)resolve @@ -36,6 +36,36 @@ @implementation DdFlags [self getBooleanValue:clientName key:key defaultValue:defaultValue resolve:resolve reject:reject]; } +RCT_REMAP_METHOD(getStringValue, + getStringValueWithClientName:(NSString *)clientName + withKey:(NSString *)key + withDefaultValue:(NSString *)defaultValue + withResolve:(RCTPromiseResolveBlock)resolve + withReject:(RCTPromiseRejectBlock)reject) +{ + [self getStringValue:clientName key:key defaultValue:defaultValue resolve:resolve reject:reject]; +} + +RCT_REMAP_METHOD(getNumberValue, + getNumberValueWithClientName:(NSString *)clientName + withKey:(NSString *)key + withDefaultValue:(double)defaultValue + withResolve:(RCTPromiseResolveBlock)resolve + withReject:(RCTPromiseRejectBlock)reject) +{ + [self getNumberValue:clientName key:key defaultValue:defaultValue resolve:resolve reject:reject]; +} + +RCT_REMAP_METHOD(getObjectValue, + getObjectValueWithClientName:(NSString *)clientName + withKey:(NSString *)key + withDefaultValue:(NSDictionary *)defaultValue + withResolve:(RCTPromiseResolveBlock)resolve + withReject:(RCTPromiseRejectBlock)reject) +{ + [self getObjectValue:clientName key:key defaultValue:defaultValue resolve:resolve reject:reject]; +} + // Thanks to this guard, we won't compile this code when we build for the new architecture. #ifdef RCT_NEW_ARCH_ENABLED - (std::shared_ptr)getTurboModule: @@ -69,4 +99,15 @@ - (void)getBooleanValue:(NSString *)clientName key:(NSString *)key defaultValue: [self.ddFlagsImplementation getBooleanValue:clientName key:key defaultValue:defaultValue resolve:resolve reject:reject]; } +- (void)getStringValue:(NSString *)clientName key:(NSString *)key defaultValue:(NSString *)defaultValue resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + [self.ddFlagsImplementation getStringValue:clientName key:key defaultValue:defaultValue resolve:resolve reject:reject]; +} + +- (void)getNumberValue:(NSString *)clientName key:(NSString *)key defaultValue:(double)defaultValue resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + [self.ddFlagsImplementation getNumberValue:clientName key:key defaultValue:defaultValue resolve:resolve reject:reject]; +} + +- (void)getObjectValue:(NSString *)clientName key:(NSString *)key defaultValue:(NSDictionary *)defaultValue resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + [self.ddFlagsImplementation getObjectValue:clientName key:key defaultValue:defaultValue resolve:resolve reject:reject]; +} @end diff --git a/packages/core/ios/Sources/DdFlagsImplementation.swift b/packages/core/ios/Sources/DdFlagsImplementation.swift index da4f5a914..c76ac3793 100644 --- a/packages/core/ios/Sources/DdFlagsImplementation.swift +++ b/packages/core/ios/Sources/DdFlagsImplementation.swift @@ -25,27 +25,35 @@ public class DdFlagsImplementation: NSObject { return client } - private func parseAttributes(attributes: NSDictionary) -> [String: AnyValue] { - func asAnyValue(value: Any) -> AnyValue { - switch value { - case let s as String: return .string(s) - case let b as Bool: return .bool(b) - case let i as Int: return .int(i) - case let d as Double: return .double(d) - // FIXME: Do we even support nested evaluation contexts? - case let dict as NSDictionary: return .dictionary(parseAttributes(attributes: dict)) - case let arr as NSArray: return .array(arr.compactMap(asAnyValue)) - case is NSNull: return .null - default: return .null - } + private func asAnyValue(_ value: Any) -> AnyValue { + if value is NSNull { + return .null } - + + if let value = value as? String { + return .string(value) + } else if let value = value as? Bool { + return .bool(value) + } else if let value = value as? Int { + return .int(value) + } else if let value = value as? Double { + return .double(value) + } else if let value = value as? NSDictionary { + return .dictionary(parseAttributes(attributes: value)) + } else if let value = value as? NSArray { + return .array(value.compactMap(asAnyValue)) + } else { + return .null + } + } + + private func parseAttributes(attributes: NSDictionary) -> [String: AnyValue] { var result: [String: AnyValue] = [:] for (key, value) in attributes { guard let stringKey = key as? String else { continue } - result[stringKey] = asAnyValue(value: value) + result[stringKey] = asAnyValue(value) } return result } @@ -75,9 +83,50 @@ public class DdFlagsImplementation: NSObject { reject: RCTPromiseRejectBlock ) { let client = getClient(name: clientName) - let value = client.getBooleanValue(key: key, defaultValue: defaultValue) + resolve(value) + } + + @objc + public func getStringValue( + _ clientName: String, + key: String, + defaultValue: String, + resolve: RCTPromiseResolveBlock, + reject: RCTPromiseRejectBlock + ) { + let client = getClient(name: clientName) + let value = client.getStringValue(key: key, defaultValue: defaultValue) + resolve(value) + } + + @objc + public func getNumberValue( + _ clientName: String, + key: String, + defaultValue: Double, + resolve: RCTPromiseResolveBlock, + reject: RCTPromiseRejectBlock + ) { + let client = getClient(name: clientName) + // TODO: Handle Integer flag values... + let value = client.getDoubleValue(key: key, defaultValue: defaultValue) + resolve(value) + } + + @objc + public func getObjectValue( + _ clientName: String, + key: String, + defaultValue: NSDictionary, + resolve: RCTPromiseResolveBlock, + reject: RCTPromiseRejectBlock + ) { + let client = getClient(name: clientName) + let val = asAnyValue(defaultValue) + let value = client.getObjectValue(key: key, defaultValue: val) + // TODO: Convert to Dictionary. resolve(value) } } diff --git a/packages/core/src/flags/FlagsClient.ts b/packages/core/src/flags/FlagsClient.ts index b6f5ea275..0bbcb9948 100644 --- a/packages/core/src/flags/FlagsClient.ts +++ b/packages/core/src/flags/FlagsClient.ts @@ -3,8 +3,7 @@ * This product includes software developed at Datadog (https://www.datadoghq.com/). * Copyright 2016-Present Datadog, Inc. */ -import { InternalLog } from '../InternalLog'; -import { SdkVerbosity } from '../SdkVerbosity'; + import type { DdNativeFlagsType } from '../nativeModulesTypes'; import type { EvaluationContext } from './types'; @@ -36,17 +35,48 @@ export class FlagsClient { key: string, defaultValue: boolean ): Promise => { - InternalLog.log( - `Flags.getBooleanValue(${key}, ${defaultValue})`, - SdkVerbosity.DEBUG + const value = await this.nativeFlags.getBooleanValue( + this.clientName, + key, + defaultValue ); + return value; + }; - const value = await this.nativeFlags.getBooleanValue( + getStringValue = async ( + key: string, + defaultValue: string + ): Promise => { + const value = await this.nativeFlags.getStringValue( + this.clientName, + key, + defaultValue + ); + return value; + }; + + getNumberValue = async ( + key: string, + defaultValue: number + ): Promise => { + const value = await this.nativeFlags.getNumberValue( this.clientName, key, defaultValue ); + return value; + }; + getObjectValue = async ( + key: string, + defaultValue: { [key: string]: unknown } + ): Promise<{ [key: string]: unknown }> => { + // FIXME: This is broken at the moment due to issues with JSON parsing on native iOS SDK side. + const value = await this.nativeFlags.getObjectValue( + this.clientName, + key, + defaultValue + ); return value; }; } diff --git a/packages/core/src/specs/NativeDdFlags.ts b/packages/core/src/specs/NativeDdFlags.ts index 130bb8e86..5074191ae 100644 --- a/packages/core/src/specs/NativeDdFlags.ts +++ b/packages/core/src/specs/NativeDdFlags.ts @@ -28,6 +28,24 @@ export interface Spec extends TurboModule { key: string, defaultValue: boolean ) => Promise; + + readonly getStringValue: ( + clientName: string, + key: string, + defaultValue: string + ) => Promise; + + readonly getNumberValue: ( + clientName: string, + key: string, + defaultValue: number + ) => Promise; + + readonly getObjectValue: ( + clientName: string, + key: string, + defaultValue: { [key: string]: unknown } + ) => Promise<{ [key: string]: unknown }>; } // eslint-disable-next-line import/no-default-export From 9d51b6d1053cd1e39c5899baa9b5ef99f4813483 Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Fri, 14 Nov 2025 18:30:41 +0200 Subject: [PATCH 08/64] Add a get*Details method --- example-new-architecture/App.tsx | 2 +- packages/core/ios/Sources/DdFlags.mm | 14 ++ .../ios/Sources/DdFlagsImplementation.swift | 135 ++++++++++++++---- packages/core/src/flags/DatadogFlags.ts | 1 + packages/core/src/flags/FlagsClient.ts | 14 +- packages/core/src/flags/types.ts | 29 ++-- packages/core/src/specs/NativeDdFlags.ts | 8 ++ 7 files changed, 165 insertions(+), 38 deletions(-) diff --git a/example-new-architecture/App.tsx b/example-new-architecture/App.tsx index 4e64b7c91..84d7ed687 100644 --- a/example-new-architecture/App.tsx +++ b/example-new-architecture/App.tsx @@ -108,7 +108,7 @@ function App(): React.JSX.Element { const flagsClient = DatadogFlags.getClient(); const [booleanValue, stringValue, jsonValue, integerValue, numberValue] = await Promise.all([ - flagsClient.getBooleanValue('rn-sdk-test-boolean-flag', false), // https://app.datadoghq.com/feature-flags/046d0e70-626d-41e1-8314-3f009fb79b7a?environmentId=d114cd9a-79ed-4c56-bcf3-bcac9293653b + flagsClient.getBooleanDetails('rn-sdk-test-boolean-flag', false), // https://app.datadoghq.com/feature-flags/046d0e70-626d-41e1-8314-3f009fb79b7a?environmentId=d114cd9a-79ed-4c56-bcf3-bcac9293653b flagsClient.getStringValue('rn-sdk-test-string-flag', 'default-value'), // https://app.datadoghq.com/feature-flags/80756d8f-a375-437a-a023-b490c91cd506?environmentId=d114cd9a-79ed-4c56-bcf3-bcac9293653b flagsClient.getObjectValue('rn-sdk-test-json-flag', {default: 'value'}), // https://app.datadoghq.com/feature-flags/bcf75cd6-96d8-4182-8871-0b66ad76127a?environmentId=d114cd9a-79ed-4c56-bcf3-bcac9293653b flagsClient.getNumberValue('rn-sdk-test-integer-flag', 0), // https://app.datadoghq.com/feature-flags/5cd5a154-65ef-4c15-b539-e68c93eaa7f1?environmentId=d114cd9a-79ed-4c56-bcf3-bcac9293653b diff --git a/packages/core/ios/Sources/DdFlags.mm b/packages/core/ios/Sources/DdFlags.mm index 39086bee5..d7c294f0b 100644 --- a/packages/core/ios/Sources/DdFlags.mm +++ b/packages/core/ios/Sources/DdFlags.mm @@ -36,6 +36,16 @@ @implementation DdFlags [self getBooleanValue:clientName key:key defaultValue:defaultValue resolve:resolve reject:reject]; } +RCT_REMAP_METHOD(getBooleanDetails, + getBooleanDetailsWithClientName:(NSString *)clientName + withKey:(NSString *)key + withDefaultValue:(BOOL)defaultValue + withResolve:(RCTPromiseResolveBlock)resolve + withReject:(RCTPromiseRejectBlock)reject) +{ + [self getBooleanDetails:clientName key:key defaultValue:defaultValue resolve:resolve reject:reject]; +} + RCT_REMAP_METHOD(getStringValue, getStringValueWithClientName:(NSString *)clientName withKey:(NSString *)key @@ -99,6 +109,10 @@ - (void)getBooleanValue:(NSString *)clientName key:(NSString *)key defaultValue: [self.ddFlagsImplementation getBooleanValue:clientName key:key defaultValue:defaultValue resolve:resolve reject:reject]; } +- (void)getBooleanDetails:(NSString *)clientName key:(NSString *)key defaultValue:(BOOL)defaultValue resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + [self.ddFlagsImplementation getBooleanDetails:clientName key:key defaultValue:defaultValue resolve:resolve reject:reject]; +} + - (void)getStringValue:(NSString *)clientName key:(NSString *)key defaultValue:(NSString *)defaultValue resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { [self.ddFlagsImplementation getStringValue:clientName key:key defaultValue:defaultValue resolve:resolve reject:reject]; } diff --git a/packages/core/ios/Sources/DdFlagsImplementation.swift b/packages/core/ios/Sources/DdFlagsImplementation.swift index c76ac3793..8e632967a 100644 --- a/packages/core/ios/Sources/DdFlagsImplementation.swift +++ b/packages/core/ios/Sources/DdFlagsImplementation.swift @@ -25,35 +25,13 @@ public class DdFlagsImplementation: NSObject { return client } - private func asAnyValue(_ value: Any) -> AnyValue { - if value is NSNull { - return .null - } - - if let value = value as? String { - return .string(value) - } else if let value = value as? Bool { - return .bool(value) - } else if let value = value as? Int { - return .int(value) - } else if let value = value as? Double { - return .double(value) - } else if let value = value as? NSDictionary { - return .dictionary(parseAttributes(attributes: value)) - } else if let value = value as? NSArray { - return .array(value.compactMap(asAnyValue)) - } else { - return .null - } - } - private func parseAttributes(attributes: NSDictionary) -> [String: AnyValue] { var result: [String: AnyValue] = [:] for (key, value) in attributes { guard let stringKey = key as? String else { continue } - result[stringKey] = asAnyValue(value) + result[stringKey] = AnyValue.wrap(value) } return result } @@ -62,7 +40,8 @@ public class DdFlagsImplementation: NSObject { public func setEvaluationContext(_ clientName: String, targetingKey: String, attributes: NSDictionary, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { let client = getClient(name: clientName) - let evaluationContext = FlagsEvaluationContext(targetingKey: targetingKey, attributes: parseAttributes(attributes: attributes)) + let parsedAttributes = parseAttributes(attributes: attributes) + let evaluationContext = FlagsEvaluationContext(targetingKey: targetingKey, attributes: parsedAttributes) client.setEvaluationContext(evaluationContext) { result in switch result { @@ -87,6 +66,20 @@ public class DdFlagsImplementation: NSObject { resolve(value) } + @objc + public func getBooleanDetails( + _ clientName: String, + key: String, + defaultValue: Bool, + resolve: RCTPromiseResolveBlock, + reject: RCTPromiseRejectBlock + ) { + let client = getClient(name: clientName) + let details = client.getBooleanDetails(key: key, defaultValue: defaultValue) + let serializedDetails = details.toSerializedDictionary() + resolve(serializedDetails) + } + @objc public func getStringValue( _ clientName: String, @@ -123,10 +116,96 @@ public class DdFlagsImplementation: NSObject { reject: RCTPromiseRejectBlock ) { let client = getClient(name: clientName) - - let val = asAnyValue(defaultValue) - let value = client.getObjectValue(key: key, defaultValue: val) - // TODO: Convert to Dictionary. + let value = client.getObjectValue(key: key, defaultValue: AnyValue.wrap(defaultValue)) resolve(value) } } + +extension AnyValue { + static func wrap(_ value: Any) -> AnyValue { + if value is NSNull { + return .null + } + + if let value = value as? String { + return .string(value) + } else if let value = value as? Bool { + return .bool(value) + } else if let value = value as? Int { + return .int(value) + } else if let value = value as? Double { + return .double(value) + } else if let value = value as? [String: Any] { + return .dictionary(value.mapValues(AnyValue.wrap)) + } else if let value = value as? [Any] { + return .array(value.map(AnyValue.wrap)) + } else { + return .null + } + } + + func unwrap() -> Any { + switch self { + case .string(let value): + return value + case .bool(let value): + return value + case .int(let value): + return value + case .double(let value): + return value + case .dictionary(let dict): + return dict.mapValues { $0.unwrap() } + case .array(let array): + return array.map { $0.unwrap() } + case .null: + return NSNull() + } + } +} + +extension FlagDetails { + func toSerializedDictionary() -> [String: Any?] { + let dict: [String: Any?] = [ + "key": key, + "value": getSerializedValue(), + "variant": variant as Any?, + "reason": reason as Any?, + "error": getSerializedError() + ] + + return dict + } + + private func getSerializedValue() -> Any { + if let boolValue = value as? Bool { + return boolValue + } else if let stringValue = value as? String { + return stringValue + } else if let intValue = value as? Int { + return intValue + } else if let doubleValue = value as? Double { + return doubleValue + } else if let anyValue = value as? AnyValue { + return anyValue.unwrap() + } + + // Fallback for unexpected types. + return NSNull() + } + + private func getSerializedError() -> String? { + guard let error = error else { + return nil + } + + switch error { + case .providerNotReady: + return "PROVIDER_NOT_READY" + case .flagNotFound: + return "FLAG_NOT_FOUND" + case .typeMismatch: + return "TYPE_MISMATCH" + } + } +} diff --git a/packages/core/src/flags/DatadogFlags.ts b/packages/core/src/flags/DatadogFlags.ts index e97b2974c..e1f18bae9 100644 --- a/packages/core/src/flags/DatadogFlags.ts +++ b/packages/core/src/flags/DatadogFlags.ts @@ -3,6 +3,7 @@ * This product includes software developed at Datadog (https://www.datadoghq.com/). * Copyright 2016-Present Datadog, Inc. */ + import { InternalLog } from '../InternalLog'; import { SdkVerbosity } from '../SdkVerbosity'; diff --git a/packages/core/src/flags/FlagsClient.ts b/packages/core/src/flags/FlagsClient.ts index 0bbcb9948..b6264638e 100644 --- a/packages/core/src/flags/FlagsClient.ts +++ b/packages/core/src/flags/FlagsClient.ts @@ -6,7 +6,7 @@ import type { DdNativeFlagsType } from '../nativeModulesTypes'; -import type { EvaluationContext } from './types'; +import type { EvaluationContext, FlagDetails } from './types'; export class FlagsClient { // eslint-disable-next-line global-require, @typescript-eslint/no-var-requires @@ -43,6 +43,18 @@ export class FlagsClient { return value; }; + getBooleanDetails = async ( + key: string, + defaultValue: boolean + ): Promise> => { + const details = await this.nativeFlags.getBooleanDetails( + this.clientName, + key, + defaultValue + ); + return details; + }; + getStringValue = async ( key: string, defaultValue: string diff --git a/packages/core/src/flags/types.ts b/packages/core/src/flags/types.ts index e7ef4a081..0f652ecf1 100644 --- a/packages/core/src/flags/types.ts +++ b/packages/core/src/flags/types.ts @@ -4,14 +4,6 @@ * Copyright 2016-Present Datadog, Inc. */ -/** - * Evaluation context for flags. - */ -export interface EvaluationContext { - targetingKey: string; - attributes: Record; -} - /** * Configuration settings for flags. */ @@ -24,3 +16,24 @@ export interface DatadogFlagsConfiguration { trackExposures?: boolean; rumIntegrationEnabled?: boolean; } + +/** + * Evaluation context for flags. + */ +export interface EvaluationContext { + targetingKey: string; + attributes: Record; +} + +export type FlagEvaluationError = + | 'PROVIDER_NOT_READY' + | 'FLAG_NOT_FOUND' + | 'TYPE_MISMATCH'; + +export interface FlagDetails { + key: string; + value: T; + variant: string | null; + reason: string | null; + error: FlagEvaluationError | null; +} diff --git a/packages/core/src/specs/NativeDdFlags.ts b/packages/core/src/specs/NativeDdFlags.ts index 5074191ae..fa05e8c68 100644 --- a/packages/core/src/specs/NativeDdFlags.ts +++ b/packages/core/src/specs/NativeDdFlags.ts @@ -8,6 +8,8 @@ import type { TurboModule } from 'react-native'; import { TurboModuleRegistry } from 'react-native'; +import type { FlagDetails } from '../flags/types'; + /** * Do not import this Spec directly, use DdNativeFlagsType instead. */ @@ -46,6 +48,12 @@ export interface Spec extends TurboModule { key: string, defaultValue: { [key: string]: unknown } ) => Promise<{ [key: string]: unknown }>; + + readonly getBooleanDetails: ( + clientName: string, + key: string, + defaultValue: boolean + ) => Promise>; } // eslint-disable-next-line import/no-default-export From a1740655ea1161e338f3f87570f3d94a27b9fdc6 Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Fri, 14 Nov 2025 19:25:43 +0200 Subject: [PATCH 09/64] Switch to only passing details methods from Swift --- example-new-architecture/App.tsx | 8 +-- packages/core/ios/Sources/DdFlags.mm | 44 ++++-------- .../ios/Sources/DdFlagsImplementation.swift | 36 ++++------ packages/core/src/flags/FlagsClient.ts | 68 ++++++++++++------- packages/core/src/specs/NativeDdFlags.ts | 22 +++--- 5 files changed, 84 insertions(+), 94 deletions(-) diff --git a/example-new-architecture/App.tsx b/example-new-architecture/App.tsx index 84d7ed687..42be78f5e 100644 --- a/example-new-architecture/App.tsx +++ b/example-new-architecture/App.tsx @@ -109,10 +109,10 @@ function App(): React.JSX.Element { const [booleanValue, stringValue, jsonValue, integerValue, numberValue] = await Promise.all([ flagsClient.getBooleanDetails('rn-sdk-test-boolean-flag', false), // https://app.datadoghq.com/feature-flags/046d0e70-626d-41e1-8314-3f009fb79b7a?environmentId=d114cd9a-79ed-4c56-bcf3-bcac9293653b - flagsClient.getStringValue('rn-sdk-test-string-flag', 'default-value'), // https://app.datadoghq.com/feature-flags/80756d8f-a375-437a-a023-b490c91cd506?environmentId=d114cd9a-79ed-4c56-bcf3-bcac9293653b - flagsClient.getObjectValue('rn-sdk-test-json-flag', {default: 'value'}), // https://app.datadoghq.com/feature-flags/bcf75cd6-96d8-4182-8871-0b66ad76127a?environmentId=d114cd9a-79ed-4c56-bcf3-bcac9293653b - flagsClient.getNumberValue('rn-sdk-test-integer-flag', 0), // https://app.datadoghq.com/feature-flags/5cd5a154-65ef-4c15-b539-e68c93eaa7f1?environmentId=d114cd9a-79ed-4c56-bcf3-bcac9293653b - flagsClient.getNumberValue('rn-sdk-test-number-flag', 0.7), // https://app.datadoghq.com/feature-flags/62b3129a-f9fa-49c0-b8a2-1a772b183bf7?environmentId=d114cd9a-79ed-4c56-bcf3-bcac9293653b + flagsClient.getStringDetails('rn-sdk-test-string-flag', 'default-value'), // https://app.datadoghq.com/feature-flags/80756d8f-a375-437a-a023-b490c91cd506?environmentId=d114cd9a-79ed-4c56-bcf3-bcac9293653b + flagsClient.getObjectDetails('rn-sdk-test-json-flag', {default: 'value'}), // https://app.datadoghq.com/feature-flags/bcf75cd6-96d8-4182-8871-0b66ad76127a?environmentId=d114cd9a-79ed-4c56-bcf3-bcac9293653b + flagsClient.getNumberDetails('rn-sdk-test-integer-flag', 0), // https://app.datadoghq.com/feature-flags/5cd5a154-65ef-4c15-b539-e68c93eaa7f1?environmentId=d114cd9a-79ed-4c56-bcf3-bcac9293653b + flagsClient.getNumberDetails('rn-sdk-test-number-flag', 0.7), // https://app.datadoghq.com/feature-flags/62b3129a-f9fa-49c0-b8a2-1a772b183bf7?environmentId=d114cd9a-79ed-4c56-bcf3-bcac9293653b ]); const newValues = { diff --git a/packages/core/ios/Sources/DdFlags.mm b/packages/core/ios/Sources/DdFlags.mm index d7c294f0b..59939d45b 100644 --- a/packages/core/ios/Sources/DdFlags.mm +++ b/packages/core/ios/Sources/DdFlags.mm @@ -26,16 +26,6 @@ @implementation DdFlags [self setEvaluationContext:clientName targetingKey:targetingKey attributes:attributes resolve:resolve reject:reject]; } -RCT_REMAP_METHOD(getBooleanValue, - getBooleanValueWithClientName:(NSString *)clientName - withKey:(NSString *)key - withDefaultValue:(BOOL)defaultValue - withResolve:(RCTPromiseResolveBlock)resolve - withReject:(RCTPromiseRejectBlock)reject) -{ - [self getBooleanValue:clientName key:key defaultValue:defaultValue resolve:resolve reject:reject]; -} - RCT_REMAP_METHOD(getBooleanDetails, getBooleanDetailsWithClientName:(NSString *)clientName withKey:(NSString *)key @@ -46,34 +36,34 @@ @implementation DdFlags [self getBooleanDetails:clientName key:key defaultValue:defaultValue resolve:resolve reject:reject]; } -RCT_REMAP_METHOD(getStringValue, - getStringValueWithClientName:(NSString *)clientName +RCT_REMAP_METHOD(getStringDetails, + getStringDetailsWithClientName:(NSString *)clientName withKey:(NSString *)key withDefaultValue:(NSString *)defaultValue withResolve:(RCTPromiseResolveBlock)resolve withReject:(RCTPromiseRejectBlock)reject) { - [self getStringValue:clientName key:key defaultValue:defaultValue resolve:resolve reject:reject]; + [self getStringDetails:clientName key:key defaultValue:defaultValue resolve:resolve reject:reject]; } -RCT_REMAP_METHOD(getNumberValue, - getNumberValueWithClientName:(NSString *)clientName +RCT_REMAP_METHOD(getNumberDetails, + getNumberDetailsWithClientName:(NSString *)clientName withKey:(NSString *)key withDefaultValue:(double)defaultValue withResolve:(RCTPromiseResolveBlock)resolve withReject:(RCTPromiseRejectBlock)reject) { - [self getNumberValue:clientName key:key defaultValue:defaultValue resolve:resolve reject:reject]; + [self getNumberDetails:clientName key:key defaultValue:defaultValue resolve:resolve reject:reject]; } -RCT_REMAP_METHOD(getObjectValue, - getObjectValueWithClientName:(NSString *)clientName +RCT_REMAP_METHOD(getObjectDetails, + getObjectDetailsWithClientName:(NSString *)clientName withKey:(NSString *)key withDefaultValue:(NSDictionary *)defaultValue withResolve:(RCTPromiseResolveBlock)resolve withReject:(RCTPromiseRejectBlock)reject) { - [self getObjectValue:clientName key:key defaultValue:defaultValue resolve:resolve reject:reject]; + [self getObjectDetails:clientName key:key defaultValue:defaultValue resolve:resolve reject:reject]; } // Thanks to this guard, we won't compile this code when we build for the new architecture. @@ -105,23 +95,19 @@ - (void)setEvaluationContext:(NSString *)clientName targetingKey:(NSString *)tar [self.ddFlagsImplementation setEvaluationContext:clientName targetingKey:targetingKey attributes:attributes resolve:resolve reject:reject]; } -- (void)getBooleanValue:(NSString *)clientName key:(NSString *)key defaultValue:(BOOL)defaultValue resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { - [self.ddFlagsImplementation getBooleanValue:clientName key:key defaultValue:defaultValue resolve:resolve reject:reject]; -} - - (void)getBooleanDetails:(NSString *)clientName key:(NSString *)key defaultValue:(BOOL)defaultValue resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { [self.ddFlagsImplementation getBooleanDetails:clientName key:key defaultValue:defaultValue resolve:resolve reject:reject]; } -- (void)getStringValue:(NSString *)clientName key:(NSString *)key defaultValue:(NSString *)defaultValue resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { - [self.ddFlagsImplementation getStringValue:clientName key:key defaultValue:defaultValue resolve:resolve reject:reject]; +- (void)getStringDetails:(NSString *)clientName key:(NSString *)key defaultValue:(NSString *)defaultValue resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + [self.ddFlagsImplementation getStringDetails:clientName key:key defaultValue:defaultValue resolve:resolve reject:reject]; } -- (void)getNumberValue:(NSString *)clientName key:(NSString *)key defaultValue:(double)defaultValue resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { - [self.ddFlagsImplementation getNumberValue:clientName key:key defaultValue:defaultValue resolve:resolve reject:reject]; +- (void)getNumberDetails:(NSString *)clientName key:(NSString *)key defaultValue:(double)defaultValue resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + [self.ddFlagsImplementation getNumberDetails:clientName key:key defaultValue:defaultValue resolve:resolve reject:reject]; } -- (void)getObjectValue:(NSString *)clientName key:(NSString *)key defaultValue:(NSDictionary *)defaultValue resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { - [self.ddFlagsImplementation getObjectValue:clientName key:key defaultValue:defaultValue resolve:resolve reject:reject]; +- (void)getObjectDetails:(NSString *)clientName key:(NSString *)key defaultValue:(NSDictionary *)defaultValue resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + [self.ddFlagsImplementation getObjectDetails:clientName key:key defaultValue:defaultValue resolve:resolve reject:reject]; } @end diff --git a/packages/core/ios/Sources/DdFlagsImplementation.swift b/packages/core/ios/Sources/DdFlagsImplementation.swift index 8e632967a..9e3e41058 100644 --- a/packages/core/ios/Sources/DdFlagsImplementation.swift +++ b/packages/core/ios/Sources/DdFlagsImplementation.swift @@ -53,19 +53,6 @@ public class DdFlagsImplementation: NSObject { } } - @objc - public func getBooleanValue( - _ clientName: String, - key: String, - defaultValue: Bool, - resolve: RCTPromiseResolveBlock, - reject: RCTPromiseRejectBlock - ) { - let client = getClient(name: clientName) - let value = client.getBooleanValue(key: key, defaultValue: defaultValue) - resolve(value) - } - @objc public func getBooleanDetails( _ clientName: String, @@ -81,7 +68,7 @@ public class DdFlagsImplementation: NSObject { } @objc - public func getStringValue( + public func getStringDetails( _ clientName: String, key: String, defaultValue: String, @@ -89,12 +76,13 @@ public class DdFlagsImplementation: NSObject { reject: RCTPromiseRejectBlock ) { let client = getClient(name: clientName) - let value = client.getStringValue(key: key, defaultValue: defaultValue) - resolve(value) + let details = client.getStringDetails(key: key, defaultValue: defaultValue) + let serializedDetails = details.toSerializedDictionary() + resolve(serializedDetails) } @objc - public func getNumberValue( + public func getNumberDetails( _ clientName: String, key: String, defaultValue: Double, @@ -103,21 +91,23 @@ public class DdFlagsImplementation: NSObject { ) { let client = getClient(name: clientName) // TODO: Handle Integer flag values... - let value = client.getDoubleValue(key: key, defaultValue: defaultValue) - resolve(value) + let details = client.getDoubleDetails(key: key, defaultValue: defaultValue) + let serializedDetails = details.toSerializedDictionary() + resolve(serializedDetails) } @objc - public func getObjectValue( + public func getObjectDetails( _ clientName: String, key: String, - defaultValue: NSDictionary, + defaultValue: [String: Any], resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock ) { let client = getClient(name: clientName) - let value = client.getObjectValue(key: key, defaultValue: AnyValue.wrap(defaultValue)) - resolve(value) + let details = client.getObjectDetails(key: key, defaultValue: AnyValue.wrap(defaultValue)) + let serializedDetails = details.toSerializedDictionary() + resolve(serializedDetails) } } diff --git a/packages/core/src/flags/FlagsClient.ts b/packages/core/src/flags/FlagsClient.ts index b6264638e..502bb3a69 100644 --- a/packages/core/src/flags/FlagsClient.ts +++ b/packages/core/src/flags/FlagsClient.ts @@ -31,23 +31,23 @@ export class FlagsClient { ); }; - getBooleanValue = async ( + getBooleanDetails = async ( key: string, defaultValue: boolean - ): Promise => { - const value = await this.nativeFlags.getBooleanValue( + ): Promise> => { + const details = await this.nativeFlags.getBooleanDetails( this.clientName, key, defaultValue ); - return value; + return details; }; - getBooleanDetails = async ( + getStringDetails = async ( key: string, - defaultValue: boolean - ): Promise> => { - const details = await this.nativeFlags.getBooleanDetails( + defaultValue: string + ): Promise> => { + const details = await this.nativeFlags.getStringDetails( this.clientName, key, defaultValue @@ -55,28 +55,52 @@ export class FlagsClient { return details; }; - getStringValue = async ( + getNumberDetails = async ( key: string, - defaultValue: string - ): Promise => { - const value = await this.nativeFlags.getStringValue( + defaultValue: number + ): Promise> => { + const details = await this.nativeFlags.getNumberDetails( this.clientName, key, defaultValue ); - return value; + return details; }; - getNumberValue = async ( + getObjectDetails = async ( key: string, - defaultValue: number - ): Promise => { - const value = await this.nativeFlags.getNumberValue( + defaultValue: { [key: string]: unknown } + ): Promise> => { + const details = await this.nativeFlags.getObjectDetails( this.clientName, key, defaultValue ); - return value; + return details; + }; + + getBooleanValue = async ( + key: string, + defaultValue: boolean + ): Promise => { + const details = await this.getBooleanDetails(key, defaultValue); + return details.value; + }; + + getStringValue = async ( + key: string, + defaultValue: string + ): Promise => { + const details = await this.getStringDetails(key, defaultValue); + return details.value; + }; + + getNumberValue = async ( + key: string, + defaultValue: number + ): Promise => { + const details = await this.getNumberDetails(key, defaultValue); + return details.value; }; getObjectValue = async ( @@ -84,11 +108,7 @@ export class FlagsClient { defaultValue: { [key: string]: unknown } ): Promise<{ [key: string]: unknown }> => { // FIXME: This is broken at the moment due to issues with JSON parsing on native iOS SDK side. - const value = await this.nativeFlags.getObjectValue( - this.clientName, - key, - defaultValue - ); - return value; + const details = await this.getObjectDetails(key, defaultValue); + return details.value; }; } diff --git a/packages/core/src/specs/NativeDdFlags.ts b/packages/core/src/specs/NativeDdFlags.ts index fa05e8c68..52461c660 100644 --- a/packages/core/src/specs/NativeDdFlags.ts +++ b/packages/core/src/specs/NativeDdFlags.ts @@ -25,35 +25,29 @@ export interface Spec extends TurboModule { attributes: { [key: string]: unknown } ) => Promise; - readonly getBooleanValue: ( + readonly getBooleanDetails: ( clientName: string, key: string, defaultValue: boolean - ) => Promise; + ) => Promise>; - readonly getStringValue: ( + readonly getStringDetails: ( clientName: string, key: string, defaultValue: string - ) => Promise; + ) => Promise>; - readonly getNumberValue: ( + readonly getNumberDetails: ( clientName: string, key: string, defaultValue: number - ) => Promise; + ) => Promise>; - readonly getObjectValue: ( + readonly getObjectDetails: ( clientName: string, key: string, defaultValue: { [key: string]: unknown } - ) => Promise<{ [key: string]: unknown }>; - - readonly getBooleanDetails: ( - clientName: string, - key: string, - defaultValue: boolean - ) => Promise>; + ) => Promise>; } // eslint-disable-next-line import/no-default-export From 9882b316dd90f1b69da171900c40386beb99af3b Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Fri, 14 Nov 2025 19:27:49 +0200 Subject: [PATCH 10/64] Properly handle Integer flag type --- .../ios/Sources/DdFlagsImplementation.swift | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/packages/core/ios/Sources/DdFlagsImplementation.swift b/packages/core/ios/Sources/DdFlagsImplementation.swift index 9e3e41058..0c70d7d1f 100644 --- a/packages/core/ios/Sources/DdFlagsImplementation.swift +++ b/packages/core/ios/Sources/DdFlagsImplementation.swift @@ -90,10 +90,23 @@ public class DdFlagsImplementation: NSObject { reject: RCTPromiseRejectBlock ) { let client = getClient(name: clientName) - // TODO: Handle Integer flag values... - let details = client.getDoubleDetails(key: key, defaultValue: defaultValue) - let serializedDetails = details.toSerializedDictionary() - resolve(serializedDetails) + + let doubleDetails = client.getDoubleDetails(key: key, defaultValue: defaultValue) + + // Try to retrieve this flag as Integer, not a Number flag type. + if doubleDetails.error == .typeMismatch { + if let safeInt = Int(exactly: defaultValue) { + let intDetails = client.getIntegerDetails(key: key, defaultValue: safeInt) + + // If resolved correctly, return Integer details. + if intDetails.error == nil { + resolve(intDetails.toSerializedDictionary()) + return + } + } + } + + resolve(doubleDetails.toSerializedDictionary()) } @objc From 11dd28789b7d1a004591ae4982cbf3e8eb4c1f51 Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Fri, 14 Nov 2025 19:52:35 +0200 Subject: [PATCH 11/64] Add default value type validation --- packages/core/src/flags/FlagsClient.ts | 40 ++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/packages/core/src/flags/FlagsClient.ts b/packages/core/src/flags/FlagsClient.ts index 502bb3a69..3e1606a63 100644 --- a/packages/core/src/flags/FlagsClient.ts +++ b/packages/core/src/flags/FlagsClient.ts @@ -35,6 +35,16 @@ export class FlagsClient { key: string, defaultValue: boolean ): Promise> => { + if (typeof defaultValue !== 'boolean') { + return { + key, + value: defaultValue, + variant: null, + reason: null, + error: 'TYPE_MISMATCH' + }; + } + const details = await this.nativeFlags.getBooleanDetails( this.clientName, key, @@ -47,6 +57,16 @@ export class FlagsClient { key: string, defaultValue: string ): Promise> => { + if (typeof defaultValue !== 'string') { + return { + key, + value: defaultValue, + variant: null, + reason: null, + error: 'TYPE_MISMATCH' + }; + } + const details = await this.nativeFlags.getStringDetails( this.clientName, key, @@ -59,6 +79,16 @@ export class FlagsClient { key: string, defaultValue: number ): Promise> => { + if (typeof defaultValue !== 'number') { + return { + key, + value: defaultValue, + variant: null, + reason: null, + error: 'TYPE_MISMATCH' + }; + } + const details = await this.nativeFlags.getNumberDetails( this.clientName, key, @@ -71,6 +101,16 @@ export class FlagsClient { key: string, defaultValue: { [key: string]: unknown } ): Promise> => { + if (typeof defaultValue !== 'object') { + return { + key, + value: defaultValue, + variant: null, + reason: null, + error: 'TYPE_MISMATCH' + }; + } + const details = await this.nativeFlags.getObjectDetails( this.clientName, key, From 591282602cf5276433a655c0d629da9ea8a0618f Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Fri, 14 Nov 2025 20:36:34 +0200 Subject: [PATCH 12/64] Add JSDoc documentation to public stuff --- packages/core/src/flags/DatadogFlags.ts | 15 ++- packages/core/src/flags/types.ts | 170 +++++++++++++++++++++++- 2 files changed, 178 insertions(+), 7 deletions(-) diff --git a/packages/core/src/flags/DatadogFlags.ts b/packages/core/src/flags/DatadogFlags.ts index e1f18bae9..e5e65fb5c 100644 --- a/packages/core/src/flags/DatadogFlags.ts +++ b/packages/core/src/flags/DatadogFlags.ts @@ -6,12 +6,16 @@ import { InternalLog } from '../InternalLog'; import { SdkVerbosity } from '../SdkVerbosity'; +import { getGlobalInstance } from '../utils/singletonUtils'; import { FlagsClient } from './FlagsClient'; -import type { DatadogFlagsConfiguration } from './types'; +import type { DatadogFlagsType, DatadogFlagsConfiguration } from './types'; -class DatadogFlagsWrapper { +const FLAGS_MODULE = 'com.datadog.reactnative.flags'; + +class DatadogFlagsWrapper implements DatadogFlagsType { getClient = (clientName: string = 'default'): FlagsClient => { + // TODO: Do we have to track whether .enabled() was called before .getClient() could be called? return new FlagsClient(clientName); }; @@ -27,6 +31,7 @@ class DatadogFlagsWrapper { }; } -const DatadogFlags = new DatadogFlagsWrapper(); - -export { DatadogFlags }; +export const DatadogFlags: DatadogFlagsType = getGlobalInstance( + FLAGS_MODULE, + () => new DatadogFlagsWrapper() +); diff --git a/packages/core/src/flags/types.ts b/packages/core/src/flags/types.ts index 0f652ecf1..e5964d5b5 100644 --- a/packages/core/src/flags/types.ts +++ b/packages/core/src/flags/types.ts @@ -4,36 +4,202 @@ * Copyright 2016-Present Datadog, Inc. */ +import type { FlagsClient } from './FlagsClient'; + +export type DatadogFlagsType = { + /** + * Returns a `FlagsClient` instance for further feature flag evaluation. + * + * If client name is not provided, the `'default'` client is returned. + */ + getClient: (clientName?: string) => FlagsClient; + /** + * Enables the Datadog Flags feature. + * + * TODO: This method is no-op for now, as flags are initialized globally by default. + */ + enable: (configuration: DatadogFlagsConfiguration) => Promise; +}; + /** - * Configuration settings for flags. + * Configuration options for the Datadog Flags feature. + * + * Use this type to customize the behavior of feature flag evaluation, including custom endpoints, + * exposure tracking, and error handling modes. */ export interface DatadogFlagsConfiguration { + /** + * Controls whether the feature flag evaluation feature is enabled. + */ enabled: boolean; + /** + * Controls error handling behavior for `FlagsClient` API misuse. + * + * This setting determines how the SDK responds to incorrect usage, such as: + * - Creating a `FlagsClient` before calling `Flags.enable()` + * + * Error handling is selected based on the build configuration and this setting: + * - Release builds use safe defaults with SDK level-based logging + * - Debug builds with `gracefulModeEnabled = true` log warnings to console instead of crashing + * - Debug builds with `gracefulModeEnabled = false` crashes with fatal errors for fail-fast development + * + * Recommended usage: + * - Set to `false` in development, test, and QA builds for immediate error detection + * - Set to `true` (default) in dogfooding and staging environments for visible warnings without crashes + * - Production builds always handle errors gracefully regardless of this setting + * + * @default true + */ gracefulModeEnabled?: boolean; + /** + * Custom server URL for retrieving flag assignments. + * + * If not set, the SDK uses the default Datadog Flags endpoint for the configured site. + * + * @default undefined + */ customFlagsEndpoint?: string; + /** + * Additional HTTP headers to attach to requests made to `customFlagsEndpoint`. + * + * Useful for authentication or routing when using your own Flags service. Ignored when using the default Datadog endpoint. + * + * @default undefined + */ customFlagsHeaders?: Record; + /** + * Custom server URL for sending Flags exposure data. + * + * If not set, the SDK uses the default Datadog Flags exposure endpoint. + * + * @default undefined + */ customExposureEndpoint?: string; + /** + * Enables exposure logging via the dedicated exposures intake endpoint. + * + * When enabled, flag evaluation events are sent to the exposures endpoint for analytics and monitoring. + * + * @default true + */ trackExposures?: boolean; + /** + * Enables the RUM integration. + * + * When enabled, flag evaluation events are sent to RUM for correlation with user sessions. + * + * @default true + */ rumIntegrationEnabled?: boolean; } /** - * Evaluation context for flags. + * Context information used for feature flag targeting and evaluation. + * + * The evaluation context contains user or session information that determines which flag + * variations are returned. This typically includes a unique identifier (targeting key) and + * optional custom attributes for more granular targeting. + * + * You can create an evaluation context and set it on the client before evaluating flags: + * + * ```ts + * const context: EvaluationContext = { + * targetingKey: "user-123", + * attributes: { + * "email": "user@example.com", + * "plan": "premium", + * "age": 25, + * "beta_tester": true + * } + * }; + * + * await client.setEvaluationContext(context); + * ``` */ export interface EvaluationContext { + /** + * The unique identifier used for targeting this user or session. + * + * This is typically a user ID, session ID, or device ID. The targeting key is used + * by the feature flag service to determine which variation to serve. + */ targetingKey: string; + + /** + * Custom attributes for more granular targeting. + * + * Attributes can include user properties, session data, or any other contextual information + * needed for flag evaluation rules. + */ attributes: Record; } +/** + * An error tha occurs during feature flag evaluation. + * + * Indicates why a flag evaluation may have failed or returned a default value. + */ export type FlagEvaluationError = | 'PROVIDER_NOT_READY' | 'FLAG_NOT_FOUND' | 'TYPE_MISMATCH'; +/** + * Detailed information about a feature flag evaluation. + * + * `FlagDetails` contains both the evaluated flag value and metadata about the evaluation, + * including the variant served, evaluation reason, and any errors that occurred. + * + * Use this type when you need access to evaluation metadata beyond just the flag value: + * + * ```ts + * const details = await flagsClient.getBooleanDetails('new-feature', false); + * + * if (details.value) { + * // Feature is enabled + * console.log(`Using variant: ${details.variant ?? 'default'}`); + * } + * + * if (details.error) { + * console.log(`Evaluation error: ${details.error}`); + * } + * ``` + */ export interface FlagDetails { + /** + * The feature flag key that was evaluated. + */ key: string; + /** + * The evaluated flag value. + * + * This is either the flag's assigned value or the default value if evaluation failed. + */ value: T; + /** + * The variant key for the evaluated flag. + * + * Variants identify which version of the flag was served. Returns `null` if the flag + * was not found or if the default value was used. + * + * ```ts + * const details = await flagsClient.getBooleanDetails('new-feature', false); + * console.log(`Served variant: ${details.variant ?? 'default'}`); + * ``` + */ variant: string | null; + /** + * The reason why this evaluation result was returned. + * + * Provides context about how the flag was evaluated, such as "TARGETING_MATCH" or "DEFAULT". + * Returns `null` if the flag was not found. + */ reason: string | null; + /** + * The error that occurred during evaluation, if any. + * + * Returns `null` if evaluation succeeded. Check this property to determine if the returned + * value is from a successful evaluation or a fallback to the default value. + */ error: FlagEvaluationError | null; } From 257f683f9aeaecc7a1b428330d4ea4738677b295 Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Mon, 17 Nov 2025 14:58:37 +0200 Subject: [PATCH 13/64] Hard set `gracefulModeEnabled` to true, add a warning for misconfigured DatadogFlags --- example-new-architecture/App.tsx | 21 ++++++++----------- .../ios/Sources/DdFlagsImplementation.swift | 12 +++++++---- .../ios/Sources/RNDdSdkConfiguration.swift | 6 ++++-- packages/core/src/DdSdkReactNative.tsx | 8 +++++++ packages/core/src/flags/DatadogFlags.ts | 18 +++++++++++----- packages/core/src/flags/types.ts | 19 ----------------- 6 files changed, 42 insertions(+), 42 deletions(-) diff --git a/example-new-architecture/App.tsx b/example-new-architecture/App.tsx index 42be78f5e..f8c91d870 100644 --- a/example-new-architecture/App.tsx +++ b/example-new-architecture/App.tsx @@ -50,6 +50,7 @@ import {APPLICATION_ID, CLIENT_TOKEN, ENVIRONMENT} from './ddCredentials'; enabled: true, }; await DdSdkReactNative.initialize(config); + await DatadogFlags.enable(config.flagsConfiguration); await DdRum.startView('main', 'Main'); setTimeout(async () => { await DdRum.addTiming('one_second'); @@ -58,16 +59,6 @@ import {APPLICATION_ID, CLIENT_TOKEN, ENVIRONMENT} from './ddCredentials'; await DdLogs.info('info log'); const spanId = await DdTrace.startSpan('test span'); await DdTrace.finishSpan(spanId); - - const flagsClient = DatadogFlags.getClient(); - - await flagsClient.setEvaluationContext({ - targetingKey: 'test-user-1', - attributes: { - country: 'US', - }, - }); - })(); type SectionProps = PropsWithChildren<{ @@ -107,6 +98,14 @@ function App(): React.JSX.Element { (async () => { const flagsClient = DatadogFlags.getClient(); + // Set flag evaluation context. + await flagsClient.setEvaluationContext({ + targetingKey: 'test-user-1', + attributes: { + country: 'US', + }, + }); + const [booleanValue, stringValue, jsonValue, integerValue, numberValue] = await Promise.all([ flagsClient.getBooleanDetails('rn-sdk-test-boolean-flag', false), // https://app.datadoghq.com/feature-flags/046d0e70-626d-41e1-8314-3f009fb79b7a?environmentId=d114cd9a-79ed-4c56-bcf3-bcac9293653b flagsClient.getStringDetails('rn-sdk-test-string-flag', 'default-value'), // https://app.datadoghq.com/feature-flags/80756d8f-a375-437a-a023-b490c91cd506?environmentId=d114cd9a-79ed-4c56-bcf3-bcac9293653b @@ -123,8 +122,6 @@ function App(): React.JSX.Element { number: numberValue, }; - console.log({newValues}); - setFlagValues(newValues); })().catch(console.error); }, []); diff --git a/packages/core/ios/Sources/DdFlagsImplementation.swift b/packages/core/ios/Sources/DdFlagsImplementation.swift index 0c70d7d1f..7c943d0e0 100644 --- a/packages/core/ios/Sources/DdFlagsImplementation.swift +++ b/packages/core/ios/Sources/DdFlagsImplementation.swift @@ -5,23 +5,27 @@ */ import Foundation +import DatadogInternal import DatadogFlags @objc public class DdFlagsImplementation: NSObject { - // Store a registry of client providers by name - // Use providers instead of direct clients to ensure lazy initialization private var clientProviders: [String: () -> FlagsClientProtocol] = [:] + /// Retrieve a `FlagsClient` instance in a non-interruptive way for usage in methods bridged to React Native. + /// + /// We create a simple registry of client providers by client name holding closures for retrieving a client since client references are kept internally in the flagging SDK. + /// This is motivated by the fact that it is impossible to create a bridged synchronous `FlagsClient` creation; thus, we create a client instance dynamically on-demand. + /// + /// - Important: Due to specifics of React Native hot reloading, this registry is destroyed upon JS bundle refresh. This leads to`FlagsClient.create` being called several times during development process for the same client. + /// This should not be a problem because `gracefulModeEnabled` is hard set to `true` for the RN SDK. private func getClient(name: String) -> FlagsClientProtocol { if let provider = clientProviders[name] { return provider() } let client = FlagsClient.create(name: name) - clientProviders[name] = { FlagsClient.shared(named: name) } - return client } diff --git a/packages/core/ios/Sources/RNDdSdkConfiguration.swift b/packages/core/ios/Sources/RNDdSdkConfiguration.swift index cfdb4d89a..ff0a46807 100644 --- a/packages/core/ios/Sources/RNDdSdkConfiguration.swift +++ b/packages/core/ios/Sources/RNDdSdkConfiguration.swift @@ -109,7 +109,9 @@ extension NSDictionary { return nil } - let gracefulModeEnabled = object(forKey: "gracefulModeEnabled") as? Bool + // Hard set `gracefulModeEnabled` to `true` because this misconfiguration is handled on JS side. + let gracefulModeEnabled = true + let customFlagsHeaders = object(forKey: "customFlagsHeaders") as? [String: String] let trackExposures = object(forKey: "trackExposures") as? Bool let rumIntegrationEnabled = object(forKey: "rumIntegrationEnabled") as? Bool @@ -124,7 +126,7 @@ extension NSDictionary { } return Flags.Configuration( - gracefulModeEnabled: gracefulModeEnabled ?? true, + gracefulModeEnabled: gracefulModeEnabled, customFlagsEndpoint: customFlagsEndpointURL, customFlagsHeaders: customFlagsHeaders, customExposureEndpoint: customExposureEndpointURL, diff --git a/packages/core/src/DdSdkReactNative.tsx b/packages/core/src/DdSdkReactNative.tsx index b9907f93d..b7305d67a 100644 --- a/packages/core/src/DdSdkReactNative.tsx +++ b/packages/core/src/DdSdkReactNative.tsx @@ -353,6 +353,14 @@ export class DdSdkReactNative { ] = `${reactNativeVersion}`; } + // Hard set `gracefulModeEnabled` to `true` because crashing an app on misconfiguration + // is not the usual workflow for React Native. + if (configuration.flagsConfiguration) { + Object.assign(configuration.flagsConfiguration, { + gracefulModeEnabled: true + }); + } + return new DdSdkConfiguration( configuration.clientToken, configuration.env, diff --git a/packages/core/src/flags/DatadogFlags.ts b/packages/core/src/flags/DatadogFlags.ts index e5e65fb5c..8e8d2e98e 100644 --- a/packages/core/src/flags/DatadogFlags.ts +++ b/packages/core/src/flags/DatadogFlags.ts @@ -14,18 +14,26 @@ import type { DatadogFlagsType, DatadogFlagsConfiguration } from './types'; const FLAGS_MODULE = 'com.datadog.reactnative.flags'; class DatadogFlagsWrapper implements DatadogFlagsType { + private _isEnabled = false; + getClient = (clientName: string = 'default'): FlagsClient => { - // TODO: Do we have to track whether .enabled() was called before .getClient() could be called? + if (__DEV__) { + if (!this._isEnabled) { + InternalLog.log( + 'DatadogFlags.getClient() called before DatadogFlags have been initialized. Flag evaluations will resolve to default values.', + SdkVerbosity.ERROR + ); + } + } + return new FlagsClient(clientName); }; enable = async ( _configuration: DatadogFlagsConfiguration ): Promise => { - InternalLog.log( - 'No-op DatadogFlags.enable() called. Flags are initialized globally by default for now.', - SdkVerbosity.DEBUG - ); + // Feature Flags are initialized globally by default for now. + this._isEnabled = _configuration.enabled; return Promise.resolve(); }; diff --git a/packages/core/src/flags/types.ts b/packages/core/src/flags/types.ts index e5964d5b5..6ecb0880f 100644 --- a/packages/core/src/flags/types.ts +++ b/packages/core/src/flags/types.ts @@ -32,25 +32,6 @@ export interface DatadogFlagsConfiguration { * Controls whether the feature flag evaluation feature is enabled. */ enabled: boolean; - /** - * Controls error handling behavior for `FlagsClient` API misuse. - * - * This setting determines how the SDK responds to incorrect usage, such as: - * - Creating a `FlagsClient` before calling `Flags.enable()` - * - * Error handling is selected based on the build configuration and this setting: - * - Release builds use safe defaults with SDK level-based logging - * - Debug builds with `gracefulModeEnabled = true` log warnings to console instead of crashing - * - Debug builds with `gracefulModeEnabled = false` crashes with fatal errors for fail-fast development - * - * Recommended usage: - * - Set to `false` in development, test, and QA builds for immediate error detection - * - Set to `true` (default) in dogfooding and staging environments for visible warnings without crashes - * - Production builds always handle errors gracefully regardless of this setting - * - * @default true - */ - gracefulModeEnabled?: boolean; /** * Custom server URL for retrieving flag assignments. * From 67c8284bfc4c97b5240a31d6704a17f71acd4c82 Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Mon, 17 Nov 2025 17:03:43 +0200 Subject: [PATCH 14/64] Remove stale FIXME --- packages/core/src/flags/FlagsClient.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/core/src/flags/FlagsClient.ts b/packages/core/src/flags/FlagsClient.ts index 3e1606a63..b07c9b731 100644 --- a/packages/core/src/flags/FlagsClient.ts +++ b/packages/core/src/flags/FlagsClient.ts @@ -147,7 +147,6 @@ export class FlagsClient { key: string, defaultValue: { [key: string]: unknown } ): Promise<{ [key: string]: unknown }> => { - // FIXME: This is broken at the moment due to issues with JSON parsing on native iOS SDK side. const details = await this.getObjectDetails(key, defaultValue); return details.value; }; From 70484e6d6249ca86f2b784838f9c941dbb398d18 Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Mon, 17 Nov 2025 19:42:19 +0200 Subject: [PATCH 15/64] Minor improvements to error handling --- example-new-architecture/App.tsx | 2 +- .../ios/Sources/DdFlagsImplementation.swift | 13 ++++++++++- packages/core/src/flags/DatadogFlags.ts | 16 ++++++------- packages/core/src/flags/FlagsClient.ts | 23 ++++++++++++++----- 4 files changed, 37 insertions(+), 17 deletions(-) diff --git a/example-new-architecture/App.tsx b/example-new-architecture/App.tsx index f8c91d870..454026708 100644 --- a/example-new-architecture/App.tsx +++ b/example-new-architecture/App.tsx @@ -123,7 +123,7 @@ function App(): React.JSX.Element { }; setFlagValues(newValues); - })().catch(console.error); + })().catch(error => console.error(error.message)); }, []); diff --git a/packages/core/ios/Sources/DdFlagsImplementation.swift b/packages/core/ios/Sources/DdFlagsImplementation.swift index 7c943d0e0..f0eab15a3 100644 --- a/packages/core/ios/Sources/DdFlagsImplementation.swift +++ b/packages/core/ios/Sources/DdFlagsImplementation.swift @@ -52,7 +52,18 @@ public class DdFlagsImplementation: NSObject { case .success: resolve(nil) case .failure(let error): - reject(error.localizedDescription, "", error) + var errorCode: String + switch (error) { + case .clientNotInitialized: + errorCode = "CLIENT_NOT_INITIALIZED" + case .invalidConfiguration: + errorCode = "INVALID_CONFIGURATION" + case .invalidResponse: + errorCode = "INVALID_RESPONSE" + case .networkError: + errorCode = "NETWORK_ERROR" + } + reject(nil, errorCode, error) } } } diff --git a/packages/core/src/flags/DatadogFlags.ts b/packages/core/src/flags/DatadogFlags.ts index 8e8d2e98e..195881112 100644 --- a/packages/core/src/flags/DatadogFlags.ts +++ b/packages/core/src/flags/DatadogFlags.ts @@ -14,16 +14,14 @@ import type { DatadogFlagsType, DatadogFlagsConfiguration } from './types'; const FLAGS_MODULE = 'com.datadog.reactnative.flags'; class DatadogFlagsWrapper implements DatadogFlagsType { - private _isEnabled = false; + private isFeatureEnabled = false; getClient = (clientName: string = 'default'): FlagsClient => { - if (__DEV__) { - if (!this._isEnabled) { - InternalLog.log( - 'DatadogFlags.getClient() called before DatadogFlags have been initialized. Flag evaluations will resolve to default values.', - SdkVerbosity.ERROR - ); - } + if (!this.isFeatureEnabled) { + InternalLog.log( + 'DatadogFlags.getClient() called before DatadogFlags have been initialized. Flag evaluations will resolve to default values.', + SdkVerbosity.ERROR + ); } return new FlagsClient(clientName); @@ -33,7 +31,7 @@ class DatadogFlagsWrapper implements DatadogFlagsType { _configuration: DatadogFlagsConfiguration ): Promise => { // Feature Flags are initialized globally by default for now. - this._isEnabled = _configuration.enabled; + this.isFeatureEnabled = _configuration.enabled; return Promise.resolve(); }; diff --git a/packages/core/src/flags/FlagsClient.ts b/packages/core/src/flags/FlagsClient.ts index b07c9b731..e9ccf00f4 100644 --- a/packages/core/src/flags/FlagsClient.ts +++ b/packages/core/src/flags/FlagsClient.ts @@ -4,6 +4,8 @@ * Copyright 2016-Present Datadog, Inc. */ +import { InternalLog } from '../InternalLog'; +import { SdkVerbosity } from '../SdkVerbosity'; import type { DdNativeFlagsType } from '../nativeModulesTypes'; import type { EvaluationContext, FlagDetails } from './types'; @@ -24,11 +26,20 @@ export class FlagsClient { ): Promise => { const { targetingKey, attributes } = context; - await this.nativeFlags.setEvaluationContext( - this.clientName, - targetingKey, - attributes - ); + try { + await this.nativeFlags.setEvaluationContext( + this.clientName, + targetingKey, + attributes + ); + } catch (error) { + if (error instanceof Error) { + InternalLog.log( + `Error setting flag evaluation context: ${error.message}`, + SdkVerbosity.ERROR + ); + } + } }; getBooleanDetails = async ( @@ -101,7 +112,7 @@ export class FlagsClient { key: string, defaultValue: { [key: string]: unknown } ): Promise> => { - if (typeof defaultValue !== 'object') { + if (typeof defaultValue !== 'object' || defaultValue === null) { return { key, value: defaultValue, From c7fea30cd20b277577149b3446eb20697f93cb73 Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Mon, 17 Nov 2025 20:27:49 +0200 Subject: [PATCH 16/64] Revert Datadog deps to 3.1.0, add them to old arch app --- example-new-architecture/ios/Podfile | 18 +- example-new-architecture/ios/Podfile.lock | 126 +++--- example/ios/Podfile | 10 + example/ios/Podfile.lock | 411 +++++++++++++++++--- packages/core/DatadogSDKReactNative.podspec | 14 +- 5 files changed, 439 insertions(+), 140 deletions(-) diff --git a/example-new-architecture/ios/Podfile b/example-new-architecture/ios/Podfile index 6a063cedd..5810da564 100644 --- a/example-new-architecture/ios/Podfile +++ b/example-new-architecture/ios/Podfile @@ -19,15 +19,15 @@ end target 'DdSdkReactNativeExample' do pod 'DatadogSDKReactNative', :path => '../../packages/core/DatadogSDKReactNative.podspec', :testspecs => ['Tests'] - # Flags don't seem to be released yet so we pull them from the iOS SDK develop branch. - pod 'DatadogInternal', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :commit => '2dfe2d0ff' - pod 'DatadogCore', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :commit => '2dfe2d0ff' - pod 'DatadogLogs', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :commit => '2dfe2d0ff' - pod 'DatadogTrace', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :commit => '2dfe2d0ff' - pod 'DatadogRUM', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :commit => '2dfe2d0ff' - pod 'DatadogCrashReporting', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :commit => '2dfe2d0ff' - pod 'DatadogFlags', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :commit => '2dfe2d0ff' - pod 'DatadogWebViewTracking', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :commit => '2dfe2d0ff' + # Pin Datadog* dependencies to a specific reference until they are updated in feature/v3. + pod 'DatadogInternal', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :branch => 'feature/flags' + pod 'DatadogCore', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :branch => 'feature/flags' + pod 'DatadogLogs', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :branch => 'feature/flags' + pod 'DatadogTrace', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :branch => 'feature/flags' + pod 'DatadogRUM', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :branch => 'feature/flags' + pod 'DatadogCrashReporting', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :branch => 'feature/flags' + pod 'DatadogFlags', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :branch => 'feature/flags' + pod 'DatadogWebViewTracking', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :branch => 'feature/flags' config = use_native_modules! diff --git a/example-new-architecture/ios/Podfile.lock b/example-new-architecture/ios/Podfile.lock index 93fa5e8f1..4a49e5325 100644 --- a/example-new-architecture/ios/Podfile.lock +++ b/example-new-architecture/ios/Podfile.lock @@ -1,25 +1,25 @@ PODS: - boost (1.84.0) - - DatadogCore (3.2.0): - - DatadogInternal (= 3.2.0) - - DatadogCrashReporting (3.2.0): - - DatadogInternal (= 3.2.0) + - DatadogCore (3.1.0): + - DatadogInternal (= 3.1.0) + - DatadogCrashReporting (3.1.0): + - DatadogInternal (= 3.1.0) - PLCrashReporter (~> 1.12.0) - - DatadogFlags (3.2.0): - - DatadogInternal (= 3.2.0) - - DatadogInternal (3.2.0) - - DatadogLogs (3.2.0): - - DatadogInternal (= 3.2.0) - - DatadogRUM (3.2.0): - - DatadogInternal (= 3.2.0) + - DatadogFlags (3.1.0): + - DatadogInternal (= 3.1.0) + - DatadogInternal (3.1.0) + - DatadogLogs (3.1.0): + - DatadogInternal (= 3.1.0) + - DatadogRUM (3.1.0): + - DatadogInternal (= 3.1.0) - DatadogSDKReactNative (2.13.0): - - DatadogCore (= 3.2.0) - - DatadogCrashReporting (= 3.2.0) - - DatadogFlags (= 3.2.0) - - DatadogLogs (= 3.2.0) - - DatadogRUM (= 3.2.0) - - DatadogTrace (= 3.2.0) - - DatadogWebViewTracking (= 3.2.0) + - DatadogCore (= 3.1.0) + - DatadogCrashReporting (= 3.1.0) + - DatadogFlags (= 3.1.0) + - DatadogLogs (= 3.1.0) + - DatadogRUM (= 3.1.0) + - DatadogTrace (= 3.1.0) + - DatadogWebViewTracking (= 3.1.0) - DoubleConversion - glog - hermes-engine @@ -41,13 +41,13 @@ PODS: - ReactCommon/turbomodule/core - Yoga - DatadogSDKReactNative/Tests (2.13.0): - - DatadogCore (= 3.2.0) - - DatadogCrashReporting (= 3.2.0) - - DatadogFlags (= 3.2.0) - - DatadogLogs (= 3.2.0) - - DatadogRUM (= 3.2.0) - - DatadogTrace (= 3.2.0) - - DatadogWebViewTracking (= 3.2.0) + - DatadogCore (= 3.1.0) + - DatadogCrashReporting (= 3.1.0) + - DatadogFlags (= 3.1.0) + - DatadogLogs (= 3.1.0) + - DatadogRUM (= 3.1.0) + - DatadogTrace (= 3.1.0) + - DatadogWebViewTracking (= 3.1.0) - DoubleConversion - glog - hermes-engine @@ -68,11 +68,11 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - DatadogTrace (3.2.0): - - DatadogInternal (= 3.2.0) + - DatadogTrace (3.1.0): + - DatadogInternal (= 3.1.0) - OpenTelemetrySwiftApi (= 1.13.1) - - DatadogWebViewTracking (3.2.0): - - DatadogInternal (= 3.2.0) + - DatadogWebViewTracking (3.1.0): + - DatadogInternal (= 3.1.0) - DoubleConversion (1.1.6) - fast_float (6.1.4) - FBLazyVector (0.76.9) @@ -1638,16 +1638,16 @@ PODS: DEPENDENCIES: - boost (from `../node_modules/react-native/third-party-podspecs/boost.podspec`) - - DatadogCore (from `https://github.com/DataDog/dd-sdk-ios.git`, commit `2dfe2d0ff`) - - DatadogCrashReporting (from `https://github.com/DataDog/dd-sdk-ios.git`, commit `2dfe2d0ff`) - - DatadogFlags (from `https://github.com/DataDog/dd-sdk-ios.git`, commit `2dfe2d0ff`) - - DatadogInternal (from `https://github.com/DataDog/dd-sdk-ios.git`, commit `2dfe2d0ff`) - - DatadogLogs (from `https://github.com/DataDog/dd-sdk-ios.git`, commit `2dfe2d0ff`) - - DatadogRUM (from `https://github.com/DataDog/dd-sdk-ios.git`, commit `2dfe2d0ff`) + - DatadogCore (from `https://github.com/DataDog/dd-sdk-ios.git`, branch `feature/flags`) + - DatadogCrashReporting (from `https://github.com/DataDog/dd-sdk-ios.git`, branch `feature/flags`) + - DatadogFlags (from `https://github.com/DataDog/dd-sdk-ios.git`, branch `feature/flags`) + - DatadogInternal (from `https://github.com/DataDog/dd-sdk-ios.git`, branch `feature/flags`) + - DatadogLogs (from `https://github.com/DataDog/dd-sdk-ios.git`, branch `feature/flags`) + - DatadogRUM (from `https://github.com/DataDog/dd-sdk-ios.git`, branch `feature/flags`) - DatadogSDKReactNative (from `../../packages/core/DatadogSDKReactNative.podspec`) - DatadogSDKReactNative/Tests (from `../../packages/core/DatadogSDKReactNative.podspec`) - - DatadogTrace (from `https://github.com/DataDog/dd-sdk-ios.git`, commit `2dfe2d0ff`) - - DatadogWebViewTracking (from `https://github.com/DataDog/dd-sdk-ios.git`, commit `2dfe2d0ff`) + - DatadogTrace (from `https://github.com/DataDog/dd-sdk-ios.git`, branch `feature/flags`) + - DatadogWebViewTracking (from `https://github.com/DataDog/dd-sdk-ios.git`, branch `feature/flags`) - DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`) - fast_float (from `../node_modules/react-native/third-party-podspecs/fast_float.podspec`) - FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`) @@ -1724,30 +1724,30 @@ EXTERNAL SOURCES: boost: :podspec: "../node_modules/react-native/third-party-podspecs/boost.podspec" DatadogCore: - :commit: 2dfe2d0ff + :branch: feature/flags :git: https://github.com/DataDog/dd-sdk-ios.git DatadogCrashReporting: - :commit: 2dfe2d0ff + :branch: feature/flags :git: https://github.com/DataDog/dd-sdk-ios.git DatadogFlags: - :commit: 2dfe2d0ff + :branch: feature/flags :git: https://github.com/DataDog/dd-sdk-ios.git DatadogInternal: - :commit: 2dfe2d0ff + :branch: feature/flags :git: https://github.com/DataDog/dd-sdk-ios.git DatadogLogs: - :commit: 2dfe2d0ff + :branch: feature/flags :git: https://github.com/DataDog/dd-sdk-ios.git DatadogRUM: - :commit: 2dfe2d0ff + :branch: feature/flags :git: https://github.com/DataDog/dd-sdk-ios.git DatadogSDKReactNative: :path: "../../packages/core/DatadogSDKReactNative.podspec" DatadogTrace: - :commit: 2dfe2d0ff + :branch: feature/flags :git: https://github.com/DataDog/dd-sdk-ios.git DatadogWebViewTracking: - :commit: 2dfe2d0ff + :branch: feature/flags :git: https://github.com/DataDog/dd-sdk-ios.git DoubleConversion: :podspec: "../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec" @@ -1879,41 +1879,41 @@ EXTERNAL SOURCES: CHECKOUT OPTIONS: DatadogCore: - :commit: 2dfe2d0ff + :commit: 18ee3ce825a9806c350133179f5e38168d78e0ba :git: https://github.com/DataDog/dd-sdk-ios.git DatadogCrashReporting: - :commit: 2dfe2d0ff + :commit: 18ee3ce825a9806c350133179f5e38168d78e0ba :git: https://github.com/DataDog/dd-sdk-ios.git DatadogFlags: - :commit: 2dfe2d0ff + :commit: 18ee3ce825a9806c350133179f5e38168d78e0ba :git: https://github.com/DataDog/dd-sdk-ios.git DatadogInternal: - :commit: 2dfe2d0ff + :commit: 18ee3ce825a9806c350133179f5e38168d78e0ba :git: https://github.com/DataDog/dd-sdk-ios.git DatadogLogs: - :commit: 2dfe2d0ff + :commit: 18ee3ce825a9806c350133179f5e38168d78e0ba :git: https://github.com/DataDog/dd-sdk-ios.git DatadogRUM: - :commit: 2dfe2d0ff + :commit: 18ee3ce825a9806c350133179f5e38168d78e0ba :git: https://github.com/DataDog/dd-sdk-ios.git DatadogTrace: - :commit: 2dfe2d0ff + :commit: 18ee3ce825a9806c350133179f5e38168d78e0ba :git: https://github.com/DataDog/dd-sdk-ios.git DatadogWebViewTracking: - :commit: 2dfe2d0ff + :commit: 18ee3ce825a9806c350133179f5e38168d78e0ba :git: https://github.com/DataDog/dd-sdk-ios.git SPEC CHECKSUMS: boost: 1dca942403ed9342f98334bf4c3621f011aa7946 - DatadogCore: 8f360d91ec79c8799e753ec1abe3169911ee50fa - DatadogCrashReporting: 0a392c47eaf0294df7e04c9ae113e97612af63b3 - DatadogFlags: 2a06a1258e78686246e5383ef90ee71f53dc6bff - DatadogInternal: 291b5ad0142280a651ab196ebd30dcd1d37cf6ff - DatadogLogs: 3ce559b785c8013911be4777845e46202046e618 - DatadogRUM: f26899d603f1797b4a41e00b5ee1aa0f6c972ef2 - DatadogSDKReactNative: 8b0b92cb7ec31e34c97ec6671908662d9203f0ca - DatadogTrace: 95e5fb69876ce3ab223dd8ef3036605c89db3cdf - DatadogWebViewTracking: 6b2d5e5ab1e85481e458f9d0130294078549558b + DatadogCore: d2f51c7fb4308cf3c25e55e2e7242e5d558ee71d + DatadogCrashReporting: f636f1d1c534572c0b0abcdc59df244c884d825d + DatadogFlags: d4237ffb9c06096d1928dbe47aac877739bc6326 + DatadogInternal: 7837b2ce3d525d429682532eeda697b181299fdc + DatadogLogs: 250894b5a99da5b924a019049c0d0326823cdbd6 + DatadogRUM: 0d2a60e1abb8aacfb8827ef84f6d5deb4d5026c8 + DatadogSDKReactNative: e74b171da3b103bf9b2fd372f480fa71c230830d + DatadogTrace: f59e933074cd285ad7e9f5af991f8fe04b095991 + DatadogWebViewTracking: 9bc92b4147aeed47eb1911451f651094aa6dd6c1 DoubleConversion: f16ae600a246532c4020132d54af21d0ddb2a385 fast_float: 06eeec4fe712a76acc9376682e4808b05ce978b6 FBLazyVector: 7605ea4810e0e10ae4815292433c09bf4324ba45 @@ -1981,6 +1981,6 @@ SPEC CHECKSUMS: SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 Yoga: feb4910aba9742cfedc059e2b2902e22ffe9954a -PODFILE CHECKSUM: bda441670d020698768356464e7ec5c2b0573608 +PODFILE CHECKSUM: c0a8ceabe25801227323f2a415c464bfca5e3f73 COCOAPODS: 1.16.2 diff --git a/example/ios/Podfile b/example/ios/Podfile index 1736870c9..da3575afe 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -21,6 +21,16 @@ target 'ddSdkReactnativeExample' do pod 'DatadogSDKReactNativeSessionReplay', :path => '../../packages/react-native-session-replay/DatadogSDKReactNativeSessionReplay.podspec', :testspecs => ['Tests'] pod 'DatadogSDKReactNativeWebView', :path => '../../packages/react-native-webview/DatadogSDKReactNativeWebView.podspec', :testspecs => ['Tests'] + # Pin Datadog* dependencies to a specific reference until they are updated in feature/v3. + pod 'DatadogInternal', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :branch => 'feature/flags' + pod 'DatadogCore', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :branch => 'feature/flags' + pod 'DatadogLogs', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :branch => 'feature/flags' + pod 'DatadogTrace', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :branch => 'feature/flags' + pod 'DatadogRUM', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :branch => 'feature/flags' + pod 'DatadogCrashReporting', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :branch => 'feature/flags' + pod 'DatadogFlags', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :branch => 'feature/flags' + pod 'DatadogWebViewTracking', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :branch => 'feature/flags' + config = use_native_modules! use_react_native!( diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index ca237868f..bac047afc 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -5,6 +5,8 @@ PODS: - DatadogCrashReporting (3.1.0): - DatadogInternal (= 3.1.0) - PLCrashReporter (~> 1.12.0) + - DatadogFlags (3.1.0): + - DatadogInternal (= 3.1.0) - DatadogInternal (3.1.0) - DatadogLogs (3.1.0): - DatadogInternal (= 3.1.0) @@ -13,19 +15,59 @@ PODS: - DatadogSDKReactNative (2.13.0): - DatadogCore (= 3.1.0) - DatadogCrashReporting (= 3.1.0) + - DatadogFlags (= 3.1.0) - DatadogLogs (= 3.1.0) - DatadogRUM (= 3.1.0) - DatadogTrace (= 3.1.0) - DatadogWebViewTracking (= 3.1.0) + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.10.14.00) + - RCTRequired + - RCTTypeSafety - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga - DatadogSDKReactNative/Tests (2.13.0): - DatadogCore (= 3.1.0) - DatadogCrashReporting (= 3.1.0) + - DatadogFlags (= 3.1.0) - DatadogLogs (= 3.1.0) - DatadogRUM (= 3.1.0) - DatadogTrace (= 3.1.0) - DatadogWebViewTracking (= 3.1.0) + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.10.14.00) + - RCTRequired + - RCTTypeSafety - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga - DatadogSDKReactNativeSessionReplay (2.13.0): - DatadogSDKReactNative - DatadogSessionReplay (= 3.1.0) @@ -77,14 +119,52 @@ PODS: - DatadogInternal (= 3.1.0) - DatadogSDKReactNative - DatadogWebViewTracking (= 3.1.0) + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.10.14.00) + - RCTRequired + - RCTTypeSafety - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga - DatadogSDKReactNativeWebView/Tests (2.13.0): - DatadogInternal (= 3.1.0) - DatadogSDKReactNative - DatadogWebViewTracking (= 3.1.0) + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.10.14.00) + - RCTRequired + - RCTTypeSafety - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager - react-native-webview + - React-NativeModulesApple + - React-RCTFabric - React-RCTText + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga - DatadogSessionReplay (3.1.0): - DatadogInternal (= 3.1.0) - DatadogTrace (3.1.0): @@ -142,6 +222,7 @@ PODS: - React-RCTText (= 0.76.9) - React-RCTVibration (= 0.76.9) - React-callinvoker (0.76.9) + - React-Codegen (0.1.0) - React-Core (0.76.9): - glog - hermes-engine @@ -1384,7 +1465,71 @@ PODS: - react-native-crash-tester (0.2.3): - React-Core - react-native-safe-area-context (5.1.0): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.10.14.00) + - RCTRequired + - RCTTypeSafety - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - react-native-safe-area-context/common (= 5.1.0) + - react-native-safe-area-context/fabric (= 5.1.0) + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - react-native-safe-area-context/common (5.1.0): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.10.14.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - react-native-safe-area-context/fabric (5.1.0): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.10.14.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - react-native-safe-area-context/common + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga - react-native-webview (13.14.2): - DoubleConversion - glog @@ -1679,20 +1824,87 @@ PODS: - React-perflogger - React-utils (= 0.76.9) - ReactNativeNavigation (8.0.0-snapshot.1658): + - DoubleConversion + - glog + - hermes-engine - HMSegmentedControl + - RCT-Folly + - RCTRequired + - RCTTypeSafety + - React + - React-Codegen - React-Core - React-CoreModules + - React-cxxreact + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric - React-RCTImage - React-RCTText + - React-rendererdebug + - React-rncore + - React-runtimeexecutor + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core - ReactNativeNavigation/Core (= 8.0.0-snapshot.1658) + - Yoga - ReactNativeNavigation/Core (8.0.0-snapshot.1658): + - DoubleConversion + - glog + - hermes-engine - HMSegmentedControl + - RCT-Folly + - RCTRequired + - RCTTypeSafety + - React + - React-Codegen - React-Core - React-CoreModules + - React-cxxreact + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric - React-RCTImage - React-RCTText + - React-rendererdebug + - React-rncore + - React-runtimeexecutor + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga - RNCAsyncStorage (2.2.0): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.10.14.00) + - RCTRequired + - RCTTypeSafety - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga - RNGestureHandler (2.24.0): - DoubleConversion - glog @@ -1715,6 +1927,29 @@ PODS: - ReactCommon/turbomodule/core - Yoga - RNScreens (4.5.0): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.10.14.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric + - React-RCTImage + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - RNScreens/common (= 4.5.0) + - Yoga + - RNScreens/common (4.5.0): - DoubleConversion - glog - hermes-engine @@ -1741,12 +1976,20 @@ PODS: DEPENDENCIES: - boost (from `../node_modules/react-native/third-party-podspecs/boost.podspec`) + - DatadogCore (from `https://github.com/DataDog/dd-sdk-ios.git`, branch `feature/flags`) + - DatadogCrashReporting (from `https://github.com/DataDog/dd-sdk-ios.git`, branch `feature/flags`) + - DatadogFlags (from `https://github.com/DataDog/dd-sdk-ios.git`, branch `feature/flags`) + - DatadogInternal (from `https://github.com/DataDog/dd-sdk-ios.git`, branch `feature/flags`) + - DatadogLogs (from `https://github.com/DataDog/dd-sdk-ios.git`, branch `feature/flags`) + - DatadogRUM (from `https://github.com/DataDog/dd-sdk-ios.git`, branch `feature/flags`) - DatadogSDKReactNative (from `../../packages/core/DatadogSDKReactNative.podspec`) - DatadogSDKReactNative/Tests (from `../../packages/core/DatadogSDKReactNative.podspec`) - DatadogSDKReactNativeSessionReplay (from `../../packages/react-native-session-replay/DatadogSDKReactNativeSessionReplay.podspec`) - DatadogSDKReactNativeSessionReplay/Tests (from `../../packages/react-native-session-replay/DatadogSDKReactNativeSessionReplay.podspec`) - DatadogSDKReactNativeWebView (from `../../packages/react-native-webview/DatadogSDKReactNativeWebView.podspec`) - DatadogSDKReactNativeWebView/Tests (from `../../packages/react-native-webview/DatadogSDKReactNativeWebView.podspec`) + - DatadogTrace (from `https://github.com/DataDog/dd-sdk-ios.git`, branch `feature/flags`) + - DatadogWebViewTracking (from `https://github.com/DataDog/dd-sdk-ios.git`, branch `feature/flags`) - DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`) - fast_float (from `../node_modules/react-native/third-party-podspecs/fast_float.podspec`) - FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`) @@ -1822,28 +2065,46 @@ DEPENDENCIES: SPEC REPOS: https://github.com/CocoaPods/Specs.git: - - DatadogCore - - DatadogCrashReporting - - DatadogInternal - - DatadogLogs - - DatadogRUM - DatadogSessionReplay - - DatadogTrace - - DatadogWebViewTracking - HMSegmentedControl - OpenTelemetrySwiftApi - PLCrashReporter + - React-Codegen - SocketRocket EXTERNAL SOURCES: boost: :podspec: "../node_modules/react-native/third-party-podspecs/boost.podspec" + DatadogCore: + :branch: feature/flags + :git: https://github.com/DataDog/dd-sdk-ios.git + DatadogCrashReporting: + :branch: feature/flags + :git: https://github.com/DataDog/dd-sdk-ios.git + DatadogFlags: + :branch: feature/flags + :git: https://github.com/DataDog/dd-sdk-ios.git + DatadogInternal: + :branch: feature/flags + :git: https://github.com/DataDog/dd-sdk-ios.git + DatadogLogs: + :branch: feature/flags + :git: https://github.com/DataDog/dd-sdk-ios.git + DatadogRUM: + :branch: feature/flags + :git: https://github.com/DataDog/dd-sdk-ios.git DatadogSDKReactNative: :path: "../../packages/core/DatadogSDKReactNative.podspec" DatadogSDKReactNativeSessionReplay: :path: "../../packages/react-native-session-replay/DatadogSDKReactNativeSessionReplay.podspec" DatadogSDKReactNativeWebView: :path: "../../packages/react-native-webview/DatadogSDKReactNativeWebView.podspec" + DatadogTrace: + :branch: feature/flags + :git: https://github.com/DataDog/dd-sdk-ios.git + DatadogWebViewTracking: + :branch: feature/flags + :git: https://github.com/DataDog/dd-sdk-ios.git DoubleConversion: :podspec: "../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec" fast_float: @@ -1986,16 +2247,43 @@ EXTERNAL SOURCES: Yoga: :path: "../node_modules/react-native/ReactCommon/yoga" +CHECKOUT OPTIONS: + DatadogCore: + :commit: 18ee3ce825a9806c350133179f5e38168d78e0ba + :git: https://github.com/DataDog/dd-sdk-ios.git + DatadogCrashReporting: + :commit: 18ee3ce825a9806c350133179f5e38168d78e0ba + :git: https://github.com/DataDog/dd-sdk-ios.git + DatadogFlags: + :commit: 18ee3ce825a9806c350133179f5e38168d78e0ba + :git: https://github.com/DataDog/dd-sdk-ios.git + DatadogInternal: + :commit: 18ee3ce825a9806c350133179f5e38168d78e0ba + :git: https://github.com/DataDog/dd-sdk-ios.git + DatadogLogs: + :commit: 18ee3ce825a9806c350133179f5e38168d78e0ba + :git: https://github.com/DataDog/dd-sdk-ios.git + DatadogRUM: + :commit: 18ee3ce825a9806c350133179f5e38168d78e0ba + :git: https://github.com/DataDog/dd-sdk-ios.git + DatadogTrace: + :commit: 18ee3ce825a9806c350133179f5e38168d78e0ba + :git: https://github.com/DataDog/dd-sdk-ios.git + DatadogWebViewTracking: + :commit: 18ee3ce825a9806c350133179f5e38168d78e0ba + :git: https://github.com/DataDog/dd-sdk-ios.git + SPEC CHECKSUMS: boost: 1dca942403ed9342f98334bf4c3621f011aa7946 DatadogCore: d2f51c7fb4308cf3c25e55e2e7242e5d558ee71d DatadogCrashReporting: f636f1d1c534572c0b0abcdc59df244c884d825d + DatadogFlags: d4237ffb9c06096d1928dbe47aac877739bc6326 DatadogInternal: 7837b2ce3d525d429682532eeda697b181299fdc DatadogLogs: 250894b5a99da5b924a019049c0d0326823cdbd6 DatadogRUM: 0d2a60e1abb8aacfb8827ef84f6d5deb4d5026c8 - DatadogSDKReactNative: 822ff8092666172584d4d5e56f79c3799887d408 - DatadogSDKReactNativeSessionReplay: afc4e2b1db34ba8af3a442b0691359faaf5e586e - DatadogSDKReactNativeWebView: 00affefdaca0cf2375e669fa03925d8fa75263d0 + DatadogSDKReactNative: e74b171da3b103bf9b2fd372f480fa71c230830d + DatadogSDKReactNativeSessionReplay: fcd758a85e16ef29e0a56c56bcee0d4895fc0c64 + DatadogSDKReactNativeWebView: b4aefc87771441e1598254a2f890b8877613656e DatadogSessionReplay: 6bc71888e2b41dd0de3325f06f0c0b3cee0e6df4 DatadogTrace: f59e933074cd285ad7e9f5af991f8fe04b095991 DatadogWebViewTracking: 9bc92b4147aeed47eb1911451f651094aa6dd6c1 @@ -2008,72 +2296,73 @@ SPEC CHECKSUMS: HMSegmentedControl: 34c1f54d822d8308e7b24f5d901ec674dfa31352 OpenTelemetrySwiftApi: aaee576ed961e0c348af78df58b61300e95bd104 PLCrashReporter: db59ef96fa3d25f3650040d02ec2798cffee75f2 - RCT-Folly: ea9d9256ba7f9322ef911169a9f696e5857b9e17 + RCT-Folly: 7b4f73a92ad9571b9dbdb05bb30fad927fa971e1 RCTDeprecation: ebe712bb05077934b16c6bf25228bdec34b64f83 RCTRequired: ca91e5dd26b64f577b528044c962baf171c6b716 RCTTypeSafety: e7678bd60850ca5a41df9b8dc7154638cb66871f React: 4641770499c39f45d4e7cde1eba30e081f9d8a3d React-callinvoker: 4bef67b5c7f3f68db5929ab6a4d44b8a002998ea - React-Core: a68cea3e762814e60ecc3fa521c7f14c36c99245 - React-CoreModules: d81b1eaf8066add66299bab9d23c9f00c9484c7c - React-cxxreact: 984f8b1feeca37181d4e95301fcd6f5f6501c6ab + React-Codegen: 4b8b4817cea7a54b83851d4c1f91f79aa73de30a + React-Core: 0a06707a0b34982efc4a556aff5dae4b22863455 + React-CoreModules: 907334e94314189c2e5eed4877f3efe7b26d85b0 + React-cxxreact: 3a1d5e8f4faa5e09be26614e9c8bbcae8d11b73d React-debug: 817160c07dc8d24d020fbd1eac7b3558ffc08964 - React-defaultsnativemodule: 21f216e8db975897eb32b5f13247f5bbfaa97f41 - React-domnativemodule: 19270ad4b8d33312838d257f24731a0026809d49 - React-Fabric: f6dade7007533daeb785ba5925039d83f343be4b - React-FabricComponents: b0655cc3e1b5ae12a4a1119aa7d8308f0ad33520 - React-FabricImage: 9b157c4c01ac2bf433f834f0e1e5fe234113a576 + React-defaultsnativemodule: 814830ccbc3fb08d67d0190e63b179ee4098c67b + React-domnativemodule: 270acf94bd0960b026bc3bfb327e703665d27fb4 + React-Fabric: 64586dc191fc1c170372a638b8e722e4f1d0a09b + React-FabricComponents: b0ebd032387468ea700574c581b139f57a7497fb + React-FabricImage: 81f0e0794caf25ad1224fa406d288fbc1986607f React-featureflags: f2792b067a351d86fdc7bec23db3b9a2f2c8d26c - React-featureflagsnativemodule: 3a8731d8fd9f755be57e00d9fa8a7f92aa77e87d - React-graphics: 68969e4e49d73f89da7abef4116c9b5f466aa121 - React-hermes: ac0bcba26a5d288ebc99b500e1097da2d0297ddf - React-idlecallbacksnativemodule: 9a2c5b5c174c0c476f039bedc1b9497a8272133e - React-ImageManager: e906eec93a9eb6102a06576b89d48d80a4683020 - React-jserrorhandler: ac5dde01104ff444e043cad8f574ca02756e20d6 - React-jsi: 496fa2b9d63b726aeb07d0ac800064617d71211d - React-jsiexecutor: dd22ab48371b80f37a0a30d0e8915b6d0f43a893 - React-jsinspector: 4629ac376f5765e684d19064f2093e55c97fd086 - React-jsitracing: 7a1c9cd484248870cf660733cd3b8114d54c035f - React-logger: c4052eb941cca9a097ef01b59543a656dc088559 - React-Mapbuffer: 33546a3ebefbccb8770c33a1f8a5554fa96a54de - React-microtasksnativemodule: 5c3d795318c22ab8df55100e50b151384a4a60b3 - react-native-crash-tester: 48bde9d6f5256c61ef2e0c52dfc74256b26e55eb - react-native-safe-area-context: e134b241010ebe2aacdcea013565963d13826faa - react-native-webview: 2ea635bc43fd8a4b89de61133e8cc0607084e9f8 + React-featureflagsnativemodule: 0d7091ae344d6160c0557048e127897654a5c00f + React-graphics: cbebe910e4a15b65b0bff94a4d3ed278894d6386 + React-hermes: ec18c10f5a69d49fb9b5e17ae95494e9ea13d4d3 + React-idlecallbacksnativemodule: 6b84add48971da9c40403bd1860d4896462590f2 + React-ImageManager: f2a4c01c2ccb2193e60a20c135da74c7ca4d36f2 + React-jserrorhandler: 61d205b5a7cbc57fed3371dd7eed48c97f49fc64 + React-jsi: 95f7676103137861b79b0f319467627bcfa629ee + React-jsiexecutor: 41e0fe87cda9ea3970ffb872ef10f1ff8dbd1932 + React-jsinspector: 15578208796723e5c6f39069b6e8bf36863ef6e2 + React-jsitracing: 3758cdb155ea7711f0e77952572ea62d90c69f0b + React-logger: dbca7bdfd4aa5ef69431362bde6b36d49403cb20 + React-Mapbuffer: 6efad4a606c1fae7e4a93385ee096681ef0300dc + React-microtasksnativemodule: a645237a841d733861c70b69908ab4a1707b52ad + react-native-crash-tester: 3ffaa64141427ca362079cb53559fe9a532487ae + react-native-safe-area-context: 511649fbcdb7b88d29660aa5c0936b3cd03c935d + react-native-webview: 7e0507e49529e26404dd5a37db31f2f8ad3d19b9 React-nativeconfig: 8efdb1ef1e9158c77098a93085438f7e7b463678 - React-NativeModulesApple: cebca2e5320a3d66e123cade23bd90a167ffce5e - React-perflogger: 72e653eb3aba9122f9e57cf012d22d2486f33358 - React-performancetimeline: cd6a9374a72001165995d2ab632f672df04076dc + React-NativeModulesApple: 958d4f6c5c2ace4c0f427cf7ef82e28ae6538a22 + React-perflogger: 9b4f13c0afe56bc7b4a0e93ec74b1150421ee22d + React-performancetimeline: 359db1cb889aa0282fafc5838331b0987c4915a9 React-RCTActionSheet: aacf2375084dea6e7c221f4a727e579f732ff342 - React-RCTAnimation: 395ab53fd064dff81507c15efb781c8684d9a585 - React-RCTAppDelegate: 1e5b43833e3e36e9fa34eec20be98174bc0e14a2 - React-RCTBlob: 13311e554c1a367de063c10ee7c5e6573b2dd1d6 - React-RCTFabric: bd906861a4e971e21d8df496c2d8f3ca6956f840 - React-RCTImage: 1b1f914bcc12187c49ba5d949dac38c2eb9f5cc8 - React-RCTLinking: 4ac7c42beb65e36fba0376f3498f3cd8dd0be7fa - React-RCTNetwork: 938902773add4381e84426a7aa17a2414f5f94f7 - React-RCTSettings: e848f1ba17a7a18479cf5a31d28145f567da8223 - React-RCTText: 7e98fafdde7d29e888b80f0b35544e0cb07913cf - React-RCTVibration: cd7d80affd97dc7afa62f9acd491419558b64b78 + React-RCTAnimation: d8c82deebebe3aaf7a843affac1b57cb2dc073d4 + React-RCTAppDelegate: 1774aa421a29a41a704ecaf789811ef73c4634b6 + React-RCTBlob: 70a58c11a6a3500d1a12f2e51ca4f6c99babcff8 + React-RCTFabric: 731cda82aed592aacce2d32ead69d78cde5d9274 + React-RCTImage: 5e9d655ba6a790c31e3176016f9b47fd0978fbf0 + React-RCTLinking: 2a48338252805091f7521eaf92687206401bdf2a + React-RCTNetwork: 0c1282b377257f6b1c81934f72d8a1d0c010e4c3 + React-RCTSettings: f757b679a74e5962be64ea08d7865a7debd67b40 + React-RCTText: e7d20c490b407d3b4a2daa48db4bcd8ec1032af2 + React-RCTVibration: 8228e37144ca3122a91f1de16ba8e0707159cfec React-rendererconsistency: b4917053ecbaa91469c67a4319701c9dc0d40be6 - React-rendererdebug: aa181c36dd6cf5b35511d1ed875d6638fd38f0ec + React-rendererdebug: 81becbc8852b38d9b1b68672aa504556481330d5 React-rncore: 120d21715c9b4ba8f798bffe986cb769b988dd74 - React-RuntimeApple: d033becbbd1eba6f9f6e3af6f1893030ce203edd - React-RuntimeCore: 38af280bb678e66ba000a3c3d42920b2a138eebb + React-RuntimeApple: 52ed0e9e84a7c2607a901149fb13599a3c057655 + React-RuntimeCore: ca6189d2e53d86db826e2673fe8af6571b8be157 React-runtimeexecutor: 877596f82f5632d073e121cba2d2084b76a76899 - React-RuntimeHermes: 37aad735ff21ca6de2d8450a96de1afe9f86c385 - React-runtimescheduler: 8ec34cc885281a34696ea16c4fd86892d631f38d + React-RuntimeHermes: 3b752dc5d8a1661c9d1687391d6d96acfa385549 + React-runtimescheduler: 8321bb09175ace2a4f0b3e3834637eb85bf42ebe React-timing: 331cbf9f2668c67faddfd2e46bb7f41cbd9320b9 - React-utils: ed818f19ab445000d6b5c4efa9d462449326cc9f - ReactCodegen: f853a20cc9125c5521c8766b4b49375fec20648b - ReactCommon: 300d8d9c5cb1a6cd79a67cf5d8f91e4d477195f9 - ReactNativeNavigation: 445f86273eb245d15b14023ee4ef9d6e4f891ad6 - RNCAsyncStorage: b44e8a4e798c3e1f56bffccd0f591f674fb9198f - RNGestureHandler: cb711d56ee3b03a5adea1d38324d4459ab55653f - RNScreens: f75b26fd4777848c216e27b0a09e1bf9c9f4760a + React-utils: 54df9ada708578c8ad40d92895d6fed03e0e8a9e + ReactCodegen: 21a52ccddc6479448fc91903a437dd23ddc7366c + ReactCommon: bfd3600989d79bc3acbe7704161b171a1480b9fd + ReactNativeNavigation: 3ddd319cc013c5e2b3bbe90b1f7fadaa5e8aa66f + RNCAsyncStorage: 8724c3be379a3d5a02ba83e276d79c2899f8d53c + RNGestureHandler: db5f279b66899cb96c8b9773c0eaac2117fe0e8a + RNScreens: 638e0b7f980df30dbb57e18d42c3b07b9fb4b92e SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 Yoga: feb4910aba9742cfedc059e2b2902e22ffe9954a -PODFILE CHECKSUM: 2be76f6ff2a88869ff51bdbf48edb79d7d863c79 +PODFILE CHECKSUM: 9b10c7cbb4e8f376b26065bb47f577130e22bc52 COCOAPODS: 1.16.2 diff --git a/packages/core/DatadogSDKReactNative.podspec b/packages/core/DatadogSDKReactNative.podspec index 2a65176b0..ccdda06c4 100644 --- a/packages/core/DatadogSDKReactNative.podspec +++ b/packages/core/DatadogSDKReactNative.podspec @@ -19,15 +19,15 @@ Pod::Spec.new do |s| s.dependency "React-Core" # /!\ Remember to keep the versions in sync with DatadogSDKReactNativeSessionReplay.podspec - s.dependency 'DatadogCore', '3.2.0' - s.dependency 'DatadogLogs', '3.2.0' - s.dependency 'DatadogTrace', '3.2.0' - s.dependency 'DatadogRUM', '3.2.0' - s.dependency 'DatadogCrashReporting', '3.2.0' - s.dependency 'DatadogFlags', '3.2.0' + s.dependency 'DatadogCore', '3.1.0' + s.dependency 'DatadogLogs', '3.1.0' + s.dependency 'DatadogTrace', '3.1.0' + s.dependency 'DatadogRUM', '3.1.0' + s.dependency 'DatadogCrashReporting', '3.1.0' + s.dependency 'DatadogFlags', '3.1.0' # DatadogWebViewTracking is not available for tvOS - s.ios.dependency 'DatadogWebViewTracking', '3.2.0' + s.ios.dependency 'DatadogWebViewTracking', '3.1.0' s.test_spec 'Tests' do |test_spec| test_spec.source_files = 'ios/Tests/**/*.{swift,json}' From 91d3ffb1069710785923e78a5491a7ea425549d5 Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Mon, 17 Nov 2025 20:40:25 +0200 Subject: [PATCH 17/64] Revert an auto-formatter change --- .../core/ios/Sources/DdSdkConfiguration.swift | 69 ++++++++++--------- 1 file changed, 36 insertions(+), 33 deletions(-) diff --git a/packages/core/ios/Sources/DdSdkConfiguration.swift b/packages/core/ios/Sources/DdSdkConfiguration.swift index 84d107a78..8f852834e 100644 --- a/packages/core/ios/Sources/DdSdkConfiguration.swift +++ b/packages/core/ios/Sources/DdSdkConfiguration.swift @@ -4,44 +4,47 @@ * Copyright 2016-Present Datadog, Inc. */ +import Foundation import DatadogCore import DatadogFlags import DatadogInternal import DatadogRUM -import Foundation -/// A configuration object to initialize Datadog's features. -/// - Parameters: -/// - clientToken: A valid Datadog client token. -/// - env: The application’s environment, for example: prod, pre-prod, staging, etc. -/// - applicationId: The RUM application ID. -/// - nativeCrashReportEnabled: Whether the SDK should track native (pure iOS or pure Android) crashes (default is false). -/// - nativeLongTaskThresholdMs: The threshold for native long tasks reporting in milliseconds. -/// - longTaskThresholdMs: The threshold for javascript long tasks reporting in milliseconds. -/// - sampleRate: The sample rate (between 0 and 100) of RUM sessions kept. -/// - site: The Datadog site of your organization (can be 'US1', 'US1_FED', 'US3', 'US5', or 'EU1', default is 'US1'). -/// - trackingConsent: Consent, which can take one of the following values: 'pending', 'granted', 'not_granted'. -/// - telemetrySampleRate: The sample rate (between 0 and 100) of telemetry events. -/// - vitalsUpdateFrequency: The frequency at which to measure vitals performance metrics. -/// - uploadFrequency: The frequency at which batches of data are sent. -/// - batchSize: The preferred size of batched data uploaded to Datadog. -/// - trackFrustrations: Whether to track frustration signals or not. -/// - trackBackgroundEvents: Enables/Disables tracking RUM event when no RUM View is active. Might increase number of sessions and billing. -/// - customEndpoints: Custom endpoints for RUM/Logs/Trace features. -/// - additionalConfig: Additional configuration parameters. -/// - configurationForTelemetry: Additional configuration paramters only used for telemetry purposes. -/// - nativeViewTracking: Enables/Disables tracking RUM Views on the native level. -/// - nativeInteractionTracking: Enables/Disables tracking RUM Actions on the native level. -/// - verbosity: Verbosity level of the SDK. -/// - proxyConfig: Configuration for proxying SDK data. -/// - serviceName: Custom service name. -/// - firstPartyHosts: List of backend hosts to enable tracing with. -/// - bundleLogsWithRum: Correlates logs with RUM. -/// - bundleLogsWithTraces: Correlates logs with traces. -/// - appHangThreshold: The threshold for non-fatal app hangs reporting in seconds. -/// - trackWatchdogTerminations: Whether the SDK should track application termination by the watchdog -/// - batchProcessingLevel: Maximum number of batches processed sequentially without a delay -/// - initialResourceThreshold: The amount of time after a view starts where a Resource should be considered when calculating Time to Network-Settled (TNS) +/** + A configuration object to initialize Datadog's features. + - Parameters: + - clientToken: A valid Datadog client token. + - env: The application’s environment, for example: prod, pre-prod, staging, etc. + - applicationId: The RUM application ID. + - nativeCrashReportEnabled: Whether the SDK should track native (pure iOS or pure Android) crashes (default is false). + - nativeLongTaskThresholdMs: The threshold for native long tasks reporting in milliseconds. + - longTaskThresholdMs: The threshold for javascript long tasks reporting in milliseconds. + - sampleRate: The sample rate (between 0 and 100) of RUM sessions kept. + - site: The Datadog site of your organization (can be 'US1', 'US1_FED', 'US3', 'US5', or 'EU1', default is 'US1'). + - trackingConsent: Consent, which can take one of the following values: 'pending', 'granted', 'not_granted'. + - telemetrySampleRate: The sample rate (between 0 and 100) of telemetry events. + - vitalsUpdateFrequency: The frequency at which to measure vitals performance metrics. + - uploadFrequency: The frequency at which batches of data are sent. + - batchSize: The preferred size of batched data uploaded to Datadog. + - trackFrustrations: Whether to track frustration signals or not. + - trackBackgroundEvents: Enables/Disables tracking RUM event when no RUM View is active. Might increase number of sessions and billing. + - customEndpoints: Custom endpoints for RUM/Logs/Trace features. + - additionalConfig: Additional configuration parameters. + - configurationForTelemetry: Additional configuration paramters only used for telemetry purposes. + - nativeViewTracking: Enables/Disables tracking RUM Views on the native level. + - nativeInteractionTracking: Enables/Disables tracking RUM Actions on the native level. + - verbosity: Verbosity level of the SDK. + - proxyConfig: Configuration for proxying SDK data. + - serviceName: Custom service name. + - firstPartyHosts: List of backend hosts to enable tracing with. + - bundleLogsWithRum: Correlates logs with RUM. + - bundleLogsWithTraces: Correlates logs with traces. + - appHangThreshold: The threshold for non-fatal app hangs reporting in seconds. + - trackWatchdogTerminations: Whether the SDK should track application termination by the watchdog + - batchProcessingLevel: Maximum number of batches processed sequentially without a delay + - initialResourceThreshold: The amount of time after a view starts where a Resource should be considered when calculating Time to Network-Settled (TNS) + - configurationForFlags: Configuration for the feature flags feature. + */ @objc(DdSdkConfiguration) public class DdSdkConfiguration: NSObject { public var clientToken: String = "" From 7d91c9ca4f8b28f8a488cedee6209f667bba1bf1 Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Tue, 18 Nov 2025 17:53:29 +0200 Subject: [PATCH 18/64] Add flagging SDK demo to the old arch example app, update the new arch app demo --- example-new-architecture/App.tsx | 49 +--- example/ios/Podfile.lock | 265 ++---------------- example/src/WixApp.tsx | 21 +- example/src/ddUtils.tsx | 16 +- example/src/screens/MainScreen.tsx | 23 +- packages/core/src/DdSdkReactNative.tsx | 5 + .../src/DdSdkReactNativeConfiguration.tsx | 1 + packages/core/src/index.tsx | 4 +- 8 files changed, 94 insertions(+), 290 deletions(-) diff --git a/example-new-architecture/App.tsx b/example-new-architecture/App.tsx index 454026708..f6aecaac5 100644 --- a/example-new-architecture/App.tsx +++ b/example-new-architecture/App.tsx @@ -92,41 +92,22 @@ function Section({children, title}: SectionProps): React.JSX.Element { } function App(): React.JSX.Element { - const [flagValues, setFlagValues] = React.useState>({}); - + const [testFlagValue, setTestFlagValue] = React.useState(false); React.useEffect(() => { - (async () => { - const flagsClient = DatadogFlags.getClient(); - - // Set flag evaluation context. - await flagsClient.setEvaluationContext({ - targetingKey: 'test-user-1', - attributes: { - country: 'US', - }, - }); - - const [booleanValue, stringValue, jsonValue, integerValue, numberValue] = await Promise.all([ - flagsClient.getBooleanDetails('rn-sdk-test-boolean-flag', false), // https://app.datadoghq.com/feature-flags/046d0e70-626d-41e1-8314-3f009fb79b7a?environmentId=d114cd9a-79ed-4c56-bcf3-bcac9293653b - flagsClient.getStringDetails('rn-sdk-test-string-flag', 'default-value'), // https://app.datadoghq.com/feature-flags/80756d8f-a375-437a-a023-b490c91cd506?environmentId=d114cd9a-79ed-4c56-bcf3-bcac9293653b - flagsClient.getObjectDetails('rn-sdk-test-json-flag', {default: 'value'}), // https://app.datadoghq.com/feature-flags/bcf75cd6-96d8-4182-8871-0b66ad76127a?environmentId=d114cd9a-79ed-4c56-bcf3-bcac9293653b - flagsClient.getNumberDetails('rn-sdk-test-integer-flag', 0), // https://app.datadoghq.com/feature-flags/5cd5a154-65ef-4c15-b539-e68c93eaa7f1?environmentId=d114cd9a-79ed-4c56-bcf3-bcac9293653b - flagsClient.getNumberDetails('rn-sdk-test-number-flag', 0.7), // https://app.datadoghq.com/feature-flags/62b3129a-f9fa-49c0-b8a2-1a772b183bf7?environmentId=d114cd9a-79ed-4c56-bcf3-bcac9293653b - ]); - - const newValues = { - boolean: booleanValue, - json: jsonValue, - integer: integerValue, - string: stringValue, - number: numberValue, - }; - - setFlagValues(newValues); - })().catch(error => console.error(error.message)); + (async () => { + const flagsClient = DatadogFlags.getClient(); + await flagsClient.setEvaluationContext({ + targetingKey: 'test-user-1', + attributes: { + country: 'US', + }, + }); + const flag = await flagsClient.getBooleanDetails('rn-sdk-test-boolean-flag', false); // https://app.datadoghq.com/feature-flags/046d0e70-626d-41e1-8314-3f009fb79b7a?environmentId=d114cd9a-79ed-4c56-bcf3-bcac9293653b + console.log({flag}) + setTestFlagValue(flag.value); + })(); }, []); - const isDarkMode = useColorScheme() === 'dark'; const backgroundStyle = { @@ -143,13 +124,11 @@ function App(): React.JSX.Element { contentInsetAdjustmentBehavior="automatic" style={backgroundStyle}>
+ rn-sdk-test-boolean-flag: {String(testFlagValue)} - - {JSON.stringify(flagValues, (key, value) => value === undefined ? '' : value, 2)} -
Edit App.tsx to change this screen and then come back to see your edits. diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index bac047afc..fb21484ba 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -20,26 +20,7 @@ PODS: - DatadogRUM (= 3.1.0) - DatadogTrace (= 3.1.0) - DatadogWebViewTracking (= 3.1.0) - - DoubleConversion - - glog - - hermes-engine - - RCT-Folly (= 2024.10.14.00) - - RCTRequired - - RCTTypeSafety - React-Core - - React-debug - - React-Fabric - - React-featureflags - - React-graphics - - React-ImageManager - - React-NativeModulesApple - - React-RCTFabric - - React-rendererdebug - - React-utils - - ReactCodegen - - ReactCommon/turbomodule/bridging - - ReactCommon/turbomodule/core - - Yoga - DatadogSDKReactNative/Tests (2.13.0): - DatadogCore (= 3.1.0) - DatadogCrashReporting (= 3.1.0) @@ -48,26 +29,7 @@ PODS: - DatadogRUM (= 3.1.0) - DatadogTrace (= 3.1.0) - DatadogWebViewTracking (= 3.1.0) - - DoubleConversion - - glog - - hermes-engine - - RCT-Folly (= 2024.10.14.00) - - RCTRequired - - RCTTypeSafety - React-Core - - React-debug - - React-Fabric - - React-featureflags - - React-graphics - - React-ImageManager - - React-NativeModulesApple - - React-RCTFabric - - React-rendererdebug - - React-utils - - ReactCodegen - - ReactCommon/turbomodule/bridging - - ReactCommon/turbomodule/core - - Yoga - DatadogSDKReactNativeSessionReplay (2.13.0): - DatadogSDKReactNative - DatadogSessionReplay (= 3.1.0) @@ -119,52 +81,14 @@ PODS: - DatadogInternal (= 3.1.0) - DatadogSDKReactNative - DatadogWebViewTracking (= 3.1.0) - - DoubleConversion - - glog - - hermes-engine - - RCT-Folly (= 2024.10.14.00) - - RCTRequired - - RCTTypeSafety - React-Core - - React-debug - - React-Fabric - - React-featureflags - - React-graphics - - React-ImageManager - - React-NativeModulesApple - - React-RCTFabric - - React-rendererdebug - - React-utils - - ReactCodegen - - ReactCommon/turbomodule/bridging - - ReactCommon/turbomodule/core - - Yoga - DatadogSDKReactNativeWebView/Tests (2.13.0): - DatadogInternal (= 3.1.0) - DatadogSDKReactNative - DatadogWebViewTracking (= 3.1.0) - - DoubleConversion - - glog - - hermes-engine - - RCT-Folly (= 2024.10.14.00) - - RCTRequired - - RCTTypeSafety - React-Core - - React-debug - - React-Fabric - - React-featureflags - - React-graphics - - React-ImageManager - react-native-webview - - React-NativeModulesApple - - React-RCTFabric - React-RCTText - - React-rendererdebug - - React-utils - - ReactCodegen - - ReactCommon/turbomodule/bridging - - ReactCommon/turbomodule/core - - Yoga - DatadogSessionReplay (3.1.0): - DatadogInternal (= 3.1.0) - DatadogTrace (3.1.0): @@ -222,7 +146,6 @@ PODS: - React-RCTText (= 0.76.9) - React-RCTVibration (= 0.76.9) - React-callinvoker (0.76.9) - - React-Codegen (0.1.0) - React-Core (0.76.9): - glog - hermes-engine @@ -1465,71 +1388,7 @@ PODS: - react-native-crash-tester (0.2.3): - React-Core - react-native-safe-area-context (5.1.0): - - DoubleConversion - - glog - - hermes-engine - - RCT-Folly (= 2024.10.14.00) - - RCTRequired - - RCTTypeSafety - React-Core - - React-debug - - React-Fabric - - React-featureflags - - React-graphics - - React-ImageManager - - react-native-safe-area-context/common (= 5.1.0) - - react-native-safe-area-context/fabric (= 5.1.0) - - React-NativeModulesApple - - React-RCTFabric - - React-rendererdebug - - React-utils - - ReactCodegen - - ReactCommon/turbomodule/bridging - - ReactCommon/turbomodule/core - - Yoga - - react-native-safe-area-context/common (5.1.0): - - DoubleConversion - - glog - - hermes-engine - - RCT-Folly (= 2024.10.14.00) - - RCTRequired - - RCTTypeSafety - - React-Core - - React-debug - - React-Fabric - - React-featureflags - - React-graphics - - React-ImageManager - - React-NativeModulesApple - - React-RCTFabric - - React-rendererdebug - - React-utils - - ReactCodegen - - ReactCommon/turbomodule/bridging - - ReactCommon/turbomodule/core - - Yoga - - react-native-safe-area-context/fabric (5.1.0): - - DoubleConversion - - glog - - hermes-engine - - RCT-Folly (= 2024.10.14.00) - - RCTRequired - - RCTTypeSafety - - React-Core - - React-debug - - React-Fabric - - React-featureflags - - React-graphics - - React-ImageManager - - react-native-safe-area-context/common - - React-NativeModulesApple - - React-RCTFabric - - React-rendererdebug - - React-utils - - ReactCodegen - - ReactCommon/turbomodule/bridging - - ReactCommon/turbomodule/core - - Yoga - react-native-webview (13.14.2): - DoubleConversion - glog @@ -1824,87 +1683,20 @@ PODS: - React-perflogger - React-utils (= 0.76.9) - ReactNativeNavigation (8.0.0-snapshot.1658): - - DoubleConversion - - glog - - hermes-engine - HMSegmentedControl - - RCT-Folly - - RCTRequired - - RCTTypeSafety - - React - - React-Codegen - React-Core - React-CoreModules - - React-cxxreact - - React-debug - - React-Fabric - - React-featureflags - - React-graphics - - React-ImageManager - - React-NativeModulesApple - - React-RCTFabric - React-RCTImage - React-RCTText - - React-rendererdebug - - React-rncore - - React-runtimeexecutor - - React-utils - - ReactCodegen - - ReactCommon/turbomodule/bridging - - ReactCommon/turbomodule/core - ReactNativeNavigation/Core (= 8.0.0-snapshot.1658) - - Yoga - ReactNativeNavigation/Core (8.0.0-snapshot.1658): - - DoubleConversion - - glog - - hermes-engine - HMSegmentedControl - - RCT-Folly - - RCTRequired - - RCTTypeSafety - - React - - React-Codegen - React-Core - React-CoreModules - - React-cxxreact - - React-debug - - React-Fabric - - React-featureflags - - React-graphics - - React-ImageManager - - React-NativeModulesApple - - React-RCTFabric - React-RCTImage - React-RCTText - - React-rendererdebug - - React-rncore - - React-runtimeexecutor - - React-utils - - ReactCodegen - - ReactCommon/turbomodule/bridging - - ReactCommon/turbomodule/core - - Yoga - RNCAsyncStorage (2.2.0): - - DoubleConversion - - glog - - hermes-engine - - RCT-Folly (= 2024.10.14.00) - - RCTRequired - - RCTTypeSafety - React-Core - - React-debug - - React-Fabric - - React-featureflags - - React-graphics - - React-ImageManager - - React-NativeModulesApple - - React-RCTFabric - - React-rendererdebug - - React-utils - - ReactCodegen - - ReactCommon/turbomodule/bridging - - ReactCommon/turbomodule/core - - Yoga - RNGestureHandler (2.24.0): - DoubleConversion - glog @@ -1927,29 +1719,6 @@ PODS: - ReactCommon/turbomodule/core - Yoga - RNScreens (4.5.0): - - DoubleConversion - - glog - - hermes-engine - - RCT-Folly (= 2024.10.14.00) - - RCTRequired - - RCTTypeSafety - - React-Core - - React-debug - - React-Fabric - - React-featureflags - - React-graphics - - React-ImageManager - - React-NativeModulesApple - - React-RCTFabric - - React-RCTImage - - React-rendererdebug - - React-utils - - ReactCodegen - - ReactCommon/turbomodule/bridging - - ReactCommon/turbomodule/core - - RNScreens/common (= 4.5.0) - - Yoga - - RNScreens/common (4.5.0): - DoubleConversion - glog - hermes-engine @@ -2069,7 +1838,6 @@ SPEC REPOS: - HMSegmentedControl - OpenTelemetrySwiftApi - PLCrashReporter - - React-Codegen - SocketRocket EXTERNAL SOURCES: @@ -2281,9 +2049,9 @@ SPEC CHECKSUMS: DatadogInternal: 7837b2ce3d525d429682532eeda697b181299fdc DatadogLogs: 250894b5a99da5b924a019049c0d0326823cdbd6 DatadogRUM: 0d2a60e1abb8aacfb8827ef84f6d5deb4d5026c8 - DatadogSDKReactNative: e74b171da3b103bf9b2fd372f480fa71c230830d - DatadogSDKReactNativeSessionReplay: fcd758a85e16ef29e0a56c56bcee0d4895fc0c64 - DatadogSDKReactNativeWebView: b4aefc87771441e1598254a2f890b8877613656e + DatadogSDKReactNative: 4ba420fb772ed237ca2098f2a78ad6a459ce34eb + DatadogSDKReactNativeSessionReplay: 72bf7b80599e2752bff13b622b04fe6605aa1d5e + DatadogSDKReactNativeWebView: 5a7f23efb34f1fa9421dba531499193f8949495d DatadogSessionReplay: 6bc71888e2b41dd0de3325f06f0c0b3cee0e6df4 DatadogTrace: f59e933074cd285ad7e9f5af991f8fe04b095991 DatadogWebViewTracking: 9bc92b4147aeed47eb1911451f651094aa6dd6c1 @@ -2302,21 +2070,20 @@ SPEC CHECKSUMS: RCTTypeSafety: e7678bd60850ca5a41df9b8dc7154638cb66871f React: 4641770499c39f45d4e7cde1eba30e081f9d8a3d React-callinvoker: 4bef67b5c7f3f68db5929ab6a4d44b8a002998ea - React-Codegen: 4b8b4817cea7a54b83851d4c1f91f79aa73de30a React-Core: 0a06707a0b34982efc4a556aff5dae4b22863455 React-CoreModules: 907334e94314189c2e5eed4877f3efe7b26d85b0 React-cxxreact: 3a1d5e8f4faa5e09be26614e9c8bbcae8d11b73d React-debug: 817160c07dc8d24d020fbd1eac7b3558ffc08964 - React-defaultsnativemodule: 814830ccbc3fb08d67d0190e63b179ee4098c67b - React-domnativemodule: 270acf94bd0960b026bc3bfb327e703665d27fb4 + React-defaultsnativemodule: a965cb39fb0a79276ab611793d39f52e59a9a851 + React-domnativemodule: d647f94e503c62c44f54291334b1aa22a30fa08b React-Fabric: 64586dc191fc1c170372a638b8e722e4f1d0a09b React-FabricComponents: b0ebd032387468ea700574c581b139f57a7497fb React-FabricImage: 81f0e0794caf25ad1224fa406d288fbc1986607f React-featureflags: f2792b067a351d86fdc7bec23db3b9a2f2c8d26c - React-featureflagsnativemodule: 0d7091ae344d6160c0557048e127897654a5c00f + React-featureflagsnativemodule: 95a02d895475de8ace78fedd76143866838bb720 React-graphics: cbebe910e4a15b65b0bff94a4d3ed278894d6386 React-hermes: ec18c10f5a69d49fb9b5e17ae95494e9ea13d4d3 - React-idlecallbacksnativemodule: 6b84add48971da9c40403bd1860d4896462590f2 + React-idlecallbacksnativemodule: 0c1ae840cc5587197cd926a3cb76828ad059d116 React-ImageManager: f2a4c01c2ccb2193e60a20c135da74c7ca4d36f2 React-jserrorhandler: 61d205b5a7cbc57fed3371dd7eed48c97f49fc64 React-jsi: 95f7676103137861b79b0f319467627bcfa629ee @@ -2325,19 +2092,19 @@ SPEC CHECKSUMS: React-jsitracing: 3758cdb155ea7711f0e77952572ea62d90c69f0b React-logger: dbca7bdfd4aa5ef69431362bde6b36d49403cb20 React-Mapbuffer: 6efad4a606c1fae7e4a93385ee096681ef0300dc - React-microtasksnativemodule: a645237a841d733861c70b69908ab4a1707b52ad + React-microtasksnativemodule: 8732b71aa66045da4bb341ddee1bb539f71e5f38 react-native-crash-tester: 3ffaa64141427ca362079cb53559fe9a532487ae - react-native-safe-area-context: 511649fbcdb7b88d29660aa5c0936b3cd03c935d - react-native-webview: 7e0507e49529e26404dd5a37db31f2f8ad3d19b9 + react-native-safe-area-context: 04803a01f39f31cc6605a5531280b477b48f8a88 + react-native-webview: 1e12de2fad74c17b4f8b1b53ebd1e3baa0148d71 React-nativeconfig: 8efdb1ef1e9158c77098a93085438f7e7b463678 React-NativeModulesApple: 958d4f6c5c2ace4c0f427cf7ef82e28ae6538a22 React-perflogger: 9b4f13c0afe56bc7b4a0e93ec74b1150421ee22d React-performancetimeline: 359db1cb889aa0282fafc5838331b0987c4915a9 React-RCTActionSheet: aacf2375084dea6e7c221f4a727e579f732ff342 React-RCTAnimation: d8c82deebebe3aaf7a843affac1b57cb2dc073d4 - React-RCTAppDelegate: 1774aa421a29a41a704ecaf789811ef73c4634b6 + React-RCTAppDelegate: 6c0377d9c4058773ea7073bb34bb9ebd6ddf5a84 React-RCTBlob: 70a58c11a6a3500d1a12f2e51ca4f6c99babcff8 - React-RCTFabric: 731cda82aed592aacce2d32ead69d78cde5d9274 + React-RCTFabric: 7eb6dd2c8fda98cb860a572e3f4e4eb60d62c89e React-RCTImage: 5e9d655ba6a790c31e3176016f9b47fd0978fbf0 React-RCTLinking: 2a48338252805091f7521eaf92687206401bdf2a React-RCTNetwork: 0c1282b377257f6b1c81934f72d8a1d0c010e4c3 @@ -2356,10 +2123,10 @@ SPEC CHECKSUMS: React-utils: 54df9ada708578c8ad40d92895d6fed03e0e8a9e ReactCodegen: 21a52ccddc6479448fc91903a437dd23ddc7366c ReactCommon: bfd3600989d79bc3acbe7704161b171a1480b9fd - ReactNativeNavigation: 3ddd319cc013c5e2b3bbe90b1f7fadaa5e8aa66f - RNCAsyncStorage: 8724c3be379a3d5a02ba83e276d79c2899f8d53c - RNGestureHandler: db5f279b66899cb96c8b9773c0eaac2117fe0e8a - RNScreens: 638e0b7f980df30dbb57e18d42c3b07b9fb4b92e + ReactNativeNavigation: 50c1eef68b821e7265eff3a391d27ed18fdce459 + RNCAsyncStorage: 23e56519cc41d3bade3c8d4479f7760cb1c11996 + RNGestureHandler: 950dfa674dbf481460ca389c65b9036ac4ab8ada + RNScreens: 606ab1cf68162f7ba0d049a31f2a84089a6fffb4 SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 Yoga: feb4910aba9742cfedc059e2b2902e22ffe9954a diff --git a/example/src/WixApp.tsx b/example/src/WixApp.tsx index 55e93f6e4..dc6ed2dc5 100644 --- a/example/src/WixApp.tsx +++ b/example/src/WixApp.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { View, Text, Button } from 'react-native'; import MainScreen from './screens/MainScreen'; import ErrorScreen from './screens/ErrorScreen'; @@ -11,7 +11,7 @@ import { } from '@datadog/mobile-react-native-navigation'; import styles from './screens/styles'; -import { DdTrace } from '@datadog/mobile-react-native'; +import { DatadogFlags } from '@datadog/mobile-react-native'; import TraceScreen from './screens/TraceScreen'; const viewPredicate: ViewNamePredicate = ( @@ -44,6 +44,22 @@ function registerScreens() { } const HomeScreen = props => { + const [testFlagValue, setTestFlagValue] = useState(false); + useEffect(() => { + (async () => { + const flagsClient = DatadogFlags.getClient(); + await flagsClient.setEvaluationContext({ + targetingKey: 'test-user-1', + attributes: { + country: 'US', + }, + }); + const flag = await flagsClient.getBooleanDetails('rn-sdk-test-boolean-flag', false); // https://app.datadoghq.com/feature-flags/046d0e70-626d-41e1-8314-3f009fb79b7a?environmentId=d114cd9a-79ed-4c56-bcf3-bcac9293653b + console.log({flag}) + setTestFlagValue(flag.value); + })(); + }, []); + return ( @@ -84,6 +100,7 @@ const HomeScreen = props => { }); }} /> + rn-sdk-test-boolean-flag: {String(testFlagValue)} ); }; diff --git a/example/src/ddUtils.tsx b/example/src/ddUtils.tsx index db0f1b4e3..d10144127 100644 --- a/example/src/ddUtils.tsx +++ b/example/src/ddUtils.tsx @@ -4,7 +4,9 @@ import { DdSdkReactNative, DdSdkReactNativeConfiguration, SdkVerbosity, - TrackingConsent + TrackingConsent, + DatadogFlags, + type DatadogFlagsConfiguration, } from '@datadog/mobile-react-native'; import {APPLICATION_ID, CLIENT_TOKEN, ENVIRONMENT} from './ddCredentials'; @@ -25,6 +27,11 @@ export function getDatadogConfig(trackingConsent: TrackingConsent) { config.serviceName = "com.datadoghq.reactnative.sample" config.verbosity = SdkVerbosity.DEBUG; + const flagsConfiguration: DatadogFlagsConfiguration = { + enabled: true, + } + config.flagsConfiguration = flagsConfiguration + return config } @@ -51,9 +58,16 @@ export function initializeDatadog(trackingConsent: TrackingConsent) { config.serviceName = "com.datadoghq.reactnative.sample" config.verbosity = SdkVerbosity.DEBUG; + const flagsConfiguration: DatadogFlagsConfiguration = { + enabled: true, + } + config.flagsConfiguration = flagsConfiguration + DdSdkReactNative.initialize(config).then(() => { DdLogs.info('The RN Sdk was properly initialized') DdSdkReactNative.setUserInfo({id: "1337", name: "Xavier", email: "xg@example.com", extraInfo: { type: "premium" } }) DdSdkReactNative.addAttributes({campaign: "ad-network"}) }); + + DatadogFlags.enable(flagsConfiguration) } diff --git a/example/src/screens/MainScreen.tsx b/example/src/screens/MainScreen.tsx index 7f0fbdec2..5ac7cddb8 100644 --- a/example/src/screens/MainScreen.tsx +++ b/example/src/screens/MainScreen.tsx @@ -11,7 +11,7 @@ import { } from 'react-native'; import styles from './styles'; import { APPLICATION_KEY, API_KEY } from '../../src/ddCredentials'; -import { DdLogs, DdSdkReactNative, TrackingConsent } from '@datadog/mobile-react-native'; +import { DdLogs, DdSdkReactNative, TrackingConsent, DatadogFlags } from '@datadog/mobile-react-native'; import { getTrackingConsent, saveTrackingConsent } from '../utils'; import { ConsentModal } from '../components/consent'; import { DdRum } from '../../../packages/core/src/rum/DdRum'; @@ -27,6 +27,7 @@ interface MainScreenState { resultTouchableNativeFeedback: string, trackingConsent: TrackingConsent, trackingConsentModalVisible: boolean + testFlagValue: boolean } export default class MainScreen extends Component { @@ -40,7 +41,8 @@ export default class MainScreen extends Component { resultButtonAction: "", resultTouchableOpacityAction: "", trackingConsent: TrackingConsent.PENDING, - trackingConsentModalVisible: false + trackingConsentModalVisible: false, + testFlagValue: false } as MainScreenState; this.consentModal = React.createRef() } @@ -94,6 +96,7 @@ export default class MainScreen extends Component { componentDidMount() { this.updateTrackingConsent() + this.fetchBooleanFlag(); DdLogs.debug("[DATADOG SDK] Test React Native Debug Log"); } @@ -105,6 +108,21 @@ export default class MainScreen extends Component { }) } + fetchBooleanFlag() { + (async () => { + const flagsClient = DatadogFlags.getClient(); + await flagsClient.setEvaluationContext({ + targetingKey: 'test-user-1', + attributes: { + country: 'US', + }, + }); + const flag = await flagsClient.getBooleanDetails('rn-sdk-test-boolean-flag', false); // https://app.datadoghq.com/feature-flags/046d0e70-626d-41e1-8314-3f009fb79b7a?environmentId=d114cd9a-79ed-4c56-bcf3-bcac9293653b + console.log({flag}) + this.setState({ testFlagValue: flag.value }) + })(); + } + setTrackingConsentModalVisible(visible: boolean) { if (visible) { this.consentModal.current.setConsent(this.state.trackingConsent) @@ -205,6 +223,7 @@ export default class MainScreen extends Component { Click me (error) + rn-sdk-test-boolean-flag: {String(this.state.testFlagValue)} } diff --git a/packages/core/src/DdSdkReactNative.tsx b/packages/core/src/DdSdkReactNative.tsx index b7305d67a..5da26b249 100644 --- a/packages/core/src/DdSdkReactNative.tsx +++ b/packages/core/src/DdSdkReactNative.tsx @@ -24,6 +24,7 @@ import { import { InternalLog } from './InternalLog'; import { SdkVerbosity } from './SdkVerbosity'; import type { TrackingConsent } from './TrackingConsent'; +import { DatadogFlags } from './flags/DatadogFlags'; import { DdLogs } from './logs/DdLogs'; import { DdRum } from './rum/DdRum'; import { DdRumErrorTracking } from './rum/instrumentation/DdRumErrorTracking'; @@ -472,6 +473,10 @@ export class DdSdkReactNative { DdRum.registerActionEventMapper(configuration.actionEventMapper); } + if (configuration.flagsConfiguration) { + DatadogFlags.enable(configuration.flagsConfiguration); + } + DdSdkReactNative.wasAutoInstrumented = true; } } diff --git a/packages/core/src/DdSdkReactNativeConfiguration.tsx b/packages/core/src/DdSdkReactNativeConfiguration.tsx index 7a6f88665..5df2ff8d9 100644 --- a/packages/core/src/DdSdkReactNativeConfiguration.tsx +++ b/packages/core/src/DdSdkReactNativeConfiguration.tsx @@ -417,6 +417,7 @@ export type AutoInstrumentationParameters = { readonly actionEventMapper: ActionEventMapper | null; readonly useAccessibilityLabel: boolean; readonly actionNameAttribute?: string; + readonly flagsConfiguration?: DatadogFlagsConfiguration; }; /** diff --git a/packages/core/src/index.tsx b/packages/core/src/index.tsx index b95c2c10a..fcab971ce 100644 --- a/packages/core/src/index.tsx +++ b/packages/core/src/index.tsx @@ -22,6 +22,7 @@ import { ProxyConfiguration, ProxyType } from './ProxyConfiguration'; import { SdkVerbosity } from './SdkVerbosity'; import { TrackingConsent } from './TrackingConsent'; import { DatadogFlags } from './flags/DatadogFlags'; +import type { DatadogFlagsConfiguration } from './flags/types'; import { DdLogs } from './logs/DdLogs'; import { DdRum } from './rum/DdRum'; import { DdBabelInteractionTracking } from './rum/instrumentation/interactionTracking/DdBabelInteractionTracking'; @@ -88,5 +89,6 @@ export type { Timestamp, FirstPartyHost, AutoInstrumentationConfiguration, - PartialInitializationConfiguration + PartialInitializationConfiguration, + DatadogFlagsConfiguration }; From f2fd0d8a4735344bf32f8d7ea7f2126ee92e9858 Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Wed, 19 Nov 2025 13:11:17 +0200 Subject: [PATCH 19/64] Fix failing tests --- packages/core/ios/Tests/DdSdkTests.swift | 6 ++++-- .../sdk/DatadogProvider/__tests__/initialization.test.tsx | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/core/ios/Tests/DdSdkTests.swift b/packages/core/ios/Tests/DdSdkTests.swift index 4a5d13f2e..c64f1acf7 100644 --- a/packages/core/ios/Tests/DdSdkTests.swift +++ b/packages/core/ios/Tests/DdSdkTests.swift @@ -1634,7 +1634,8 @@ extension DdSdkConfiguration { appHangThreshold: Double? = nil, trackWatchdogTerminations: Bool = false, batchProcessingLevel: NSString? = "MEDIUM", - initialResourceThreshold: Double? = nil + initialResourceThreshold: Double? = nil, + configurationForFlags: NSDictionary? = nil ) -> DdSdkConfiguration { DdSdkConfiguration( clientToken: clientToken as String, @@ -1667,7 +1668,8 @@ extension DdSdkConfiguration { appHangThreshold: appHangThreshold, trackWatchdogTerminations: trackWatchdogTerminations, batchProcessingLevel: batchProcessingLevel.asBatchProcessingLevel(), - initialResourceThreshold: initialResourceThreshold + initialResourceThreshold: initialResourceThreshold, + configurationForFlags: configurationForFlags?.asConfigurationForFlags() ) } } diff --git a/packages/core/src/sdk/DatadogProvider/__tests__/initialization.test.tsx b/packages/core/src/sdk/DatadogProvider/__tests__/initialization.test.tsx index 8ace4b731..e896a9bc2 100644 --- a/packages/core/src/sdk/DatadogProvider/__tests__/initialization.test.tsx +++ b/packages/core/src/sdk/DatadogProvider/__tests__/initialization.test.tsx @@ -75,6 +75,7 @@ describe('DatadogProvider', () => { "bundleLogsWithRum": true, "bundleLogsWithTraces": true, "clientToken": "fakeToken", + "configurationForFlags": undefined, "configurationForTelemetry": { "initializationType": "SYNC", "reactNativeVersion": "0.76.9", From daff8ea2054101328e3afb0127ce560e12277449 Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Mon, 24 Nov 2025 22:07:44 +0200 Subject: [PATCH 20/64] Add tests for the flagging functionality --- packages/core/__mocks__/react-native.ts | 40 ++ .../ios/Sources/DdFlagsImplementation.swift | 17 +- .../ios/Sources/RNDdSdkConfiguration.swift | 2 +- packages/core/ios/Tests/DdFlagsTests.swift | 347 ++++++++++++++++++ packages/core/ios/Tests/DdSdkTests.swift | 30 ++ .../src/flags/__tests__/DatadogFlags.test.ts | 54 +++ .../src/flags/__tests__/FlagsClient.test.ts | 197 ++++++++++ 7 files changed, 684 insertions(+), 3 deletions(-) create mode 100644 packages/core/ios/Tests/DdFlagsTests.swift create mode 100644 packages/core/src/flags/__tests__/DatadogFlags.test.ts create mode 100644 packages/core/src/flags/__tests__/FlagsClient.test.ts diff --git a/packages/core/__mocks__/react-native.ts b/packages/core/__mocks__/react-native.ts index 73308f711..8e8b22d78 100644 --- a/packages/core/__mocks__/react-native.ts +++ b/packages/core/__mocks__/react-native.ts @@ -158,4 +158,44 @@ actualRN.NativeModules.DdRum = { ) as jest.MockedFunction }; +actualRN.NativeModules.DdFlags = { + setEvaluationContext: jest.fn().mockImplementation(() => Promise.resolve()), + getBooleanDetails: jest.fn().mockImplementation(() => + Promise.resolve({ + key: 'test-boolean-flag', + value: true, + variant: 'true', + reason: 'STATIC', + error: null + }) + ), + getStringDetails: jest.fn().mockImplementation(() => + Promise.resolve({ + key: 'test-string-flag', + value: 'hello world', + variant: 'hello world', + reason: 'STATIC', + error: null + }) + ), + getNumberDetails: jest.fn().mockImplementation(() => + Promise.resolve({ + key: 'test-number-flag', + value: 6, + variant: '6', + reason: 'STATIC', + error: null + }) + ), + getObjectDetails: jest.fn().mockImplementation(() => + Promise.resolve({ + key: 'test-object-flag', + value: { hello: 'world' }, + variant: 'hello world', + reason: 'STATIC', + error: null + }) + ) +}; + module.exports = actualRN; diff --git a/packages/core/ios/Sources/DdFlagsImplementation.swift b/packages/core/ios/Sources/DdFlagsImplementation.swift index f0eab15a3..7b128db17 100644 --- a/packages/core/ios/Sources/DdFlagsImplementation.swift +++ b/packages/core/ios/Sources/DdFlagsImplementation.swift @@ -10,8 +10,21 @@ import DatadogFlags @objc public class DdFlagsImplementation: NSObject { + private let core: DatadogCoreProtocol + private var clientProviders: [String: () -> FlagsClientProtocol] = [:] + internal init( + core: DatadogCoreProtocol + ) { + self.core = core + } + + @objc + public override convenience init() { + self.init(core: CoreRegistry.default) + } + /// Retrieve a `FlagsClient` instance in a non-interruptive way for usage in methods bridged to React Native. /// /// We create a simple registry of client providers by client name holding closures for retrieving a client since client references are kept internally in the flagging SDK. @@ -24,8 +37,8 @@ public class DdFlagsImplementation: NSObject { return provider() } - let client = FlagsClient.create(name: name) - clientProviders[name] = { FlagsClient.shared(named: name) } + let client = FlagsClient.create(name: name, in: self.core) + clientProviders[name] = { FlagsClient.shared(named: name, in: self.core) } return client } diff --git a/packages/core/ios/Sources/RNDdSdkConfiguration.swift b/packages/core/ios/Sources/RNDdSdkConfiguration.swift index ff0a46807..464161c3f 100644 --- a/packages/core/ios/Sources/RNDdSdkConfiguration.swift +++ b/packages/core/ios/Sources/RNDdSdkConfiguration.swift @@ -103,7 +103,7 @@ extension NSDictionary { } func asConfigurationForFlags() -> Flags.Configuration? { - let enabled = object(forKey: "enabled") as! Bool + let enabled = object(forKey: "enabled") as? Bool ?? false if !enabled { return nil diff --git a/packages/core/ios/Tests/DdFlagsTests.swift b/packages/core/ios/Tests/DdFlagsTests.swift new file mode 100644 index 000000000..7402d4d23 --- /dev/null +++ b/packages/core/ios/Tests/DdFlagsTests.swift @@ -0,0 +1,347 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +import XCTest +import DatadogCore +import DatadogFlags +import DatadogInternal +@testable import DatadogSDKReactNative + +class DdFlagsTests: XCTestCase { + + private var core: FlagsTestCore! + + override func setUp() { + super.setUp() + // MockDatadogCore doesn't work here because it returns `nil` in `feature` method. + core = FlagsTestCore() + CoreRegistry.register(default: core) + Flags.enable(in: core) + } + + override func tearDown() { + CoreRegistry.unregisterDefault() + super.tearDown() + } + + // MARK: - AnyValue Tests + + func testAnyValueWrapUnwrapNull() { + let original: Any = NSNull() + let wrapped = AnyValue.wrap(original) + + if case .null = wrapped { + XCTAssertTrue(true) + } else { + XCTFail("Expected .null, got \(wrapped)") + } + + let unwrapped = wrapped.unwrap() + XCTAssertTrue(unwrapped is NSNull) + } + + func testAnyValueWrapUnwrapString() { + let original = "test string" + let wrapped = AnyValue.wrap(original) + + if case .string(let value) = wrapped { + XCTAssertEqual(value, original) + } else { + XCTFail("Expected .string, got \(wrapped)") + } + + let unwrapped = wrapped.unwrap() as? String + XCTAssertEqual(unwrapped, original) + } + + func testAnyValueWrapUnwrapBool() { + let original = true + let wrapped = AnyValue.wrap(original) + + if case .bool(let value) = wrapped { + XCTAssertEqual(value, original) + } else { + XCTFail("Expected .bool, got \(wrapped)") + } + + let unwrapped = wrapped.unwrap() as? Bool + XCTAssertEqual(unwrapped, original) + } + + func testAnyValueWrapUnwrapInt() { + let original = 42 + let wrapped = AnyValue.wrap(original) + + if case .int(let value) = wrapped { + XCTAssertEqual(value, original) + } else { + XCTFail("Expected .int, got \(wrapped)") + } + + let unwrapped = wrapped.unwrap() as? Int + XCTAssertEqual(unwrapped, original) + } + + func testAnyValueWrapUnwrapDouble() { + let original = 3.14 + let wrapped = AnyValue.wrap(original) + + if case .double(let value) = wrapped { + XCTAssertEqual(value, original) + } else { + XCTFail("Expected .double, got \(wrapped)") + } + + let unwrapped = wrapped.unwrap() as? Double + XCTAssertEqual(unwrapped, original) + } + + func testAnyValueWrapUnwrapDictionary() { + let original: [String: Any] = ["key": "value", "number": 1] + let wrapped = AnyValue.wrap(original) + + if case .dictionary(let dict) = wrapped { + XCTAssertEqual(dict.count, 2) + if let val = dict["key"], case .string(let s) = val { + XCTAssertEqual(s, "value") + } else { + XCTFail("Expected string for key") + } + if let val = dict["number"], case .int(let i) = val { + XCTAssertEqual(i, 1) + } else { + XCTFail("Expected int for number") + } + } else { + XCTFail("Expected .dictionary, got \(wrapped)") + } + + let unwrapped = wrapped.unwrap() as? [String: Any] + XCTAssertEqual(unwrapped?["key"] as? String, "value") + XCTAssertEqual(unwrapped?["number"] as? Int, 1) + } + + func testAnyValueWrapUnwrapArray() { + let original: [Any] = ["value", 1] + let wrapped = AnyValue.wrap(original) + + if case .array(let array) = wrapped { + XCTAssertEqual(array.count, 2) + if case .string(let s) = array[0] { + XCTAssertEqual(s, "value") + } else { + XCTFail("Expected string at index 0") + } + if case .int(let i) = array[1] { + XCTAssertEqual(i, 1) + } else { + XCTFail("Expected int at index 1") + } + } else { + XCTFail("Expected .array, got \(wrapped)") + } + + let unwrapped = wrapped.unwrap() as? [Any] + XCTAssertEqual(unwrapped?[0] as? String, "value") + XCTAssertEqual(unwrapped?[1] as? Int, 1) + } + + func testAnyValueWrapUnknown() { + struct UnknownType {} + let original = UnknownType() + let wrapped = AnyValue.wrap(original) + + if case .null = wrapped { + XCTAssertTrue(true) + } else { + XCTFail("Expected .null for unknown type, got \(wrapped)") + } + } + + // MARK: - FlagDetails Tests + + func testFlagDetailsToSerializedDictionarySuccess() { + let details = FlagDetails( + key: "test_flag", + value: "test_value", + variant: "control", + reason: "targeting_match", + error: nil + ) + + let serialized = details.toSerializedDictionary() + + XCTAssertEqual(serialized["key"] as? String, "test_flag") + XCTAssertEqual(serialized["value"] as? String, "test_value") + XCTAssertEqual(serialized["variant"] as? String, "control") + XCTAssertEqual(serialized["reason"] as? String, "targeting_match") + XCTAssertNil(serialized["error"] as? String) + } + + func testFlagDetailsToSerializedDictionaryWithError() { + let details = FlagDetails( + key: "test_flag", + value: false, + variant: nil, + reason: nil, + error: .flagNotFound + ) + + let serialized = details.toSerializedDictionary() + + XCTAssertEqual(serialized["key"] as? String, "test_flag") + XCTAssertTrue(serialized["value"] as? Bool != nil) + XCTAssertNil(serialized["variant"] as? String) + XCTAssertNil(serialized["reason"] as? String) + XCTAssertEqual(serialized["error"] as? String, "FLAG_NOT_FOUND") + } + + func testFlagDetailsToSerializedDictionaryWithOtherErrors() { + let errorCases: [(FlagEvaluationError, String)] = [ + (.providerNotReady, "PROVIDER_NOT_READY"), + (.typeMismatch, "TYPE_MISMATCH"), + (.flagNotFound, "FLAG_NOT_FOUND") + ] + + for (error, expectedString) in errorCases { + let details = FlagDetails( + key: "key", + value: false, + variant: nil, + reason: nil, + error: error + ) + let serialized = details.toSerializedDictionary() + XCTAssertEqual(serialized["error"] as? String, expectedString) + } + } + + func testFlagDetailsToSerializedDictionaryWithDifferentValueTypes() { + let boolDetails = FlagDetails(key: "k", value: true, variant: nil, reason: nil, error: nil) + XCTAssertEqual(boolDetails.toSerializedDictionary()["value"] as? Bool, true) + + let intDetails = FlagDetails(key: "k", value: 123, variant: nil, reason: nil, error: nil) + XCTAssertEqual(intDetails.toSerializedDictionary()["value"] as? Int, 123) + + let doubleDetails = FlagDetails(key: "k", value: 12.34, variant: nil, reason: nil, error: nil) + XCTAssertEqual(doubleDetails.toSerializedDictionary()["value"] as? Double, 12.34) + + let anyValueDetails = FlagDetails(key: "k", value: AnyValue.string("s"), variant: nil, reason: nil, error: nil) + XCTAssertEqual(anyValueDetails.toSerializedDictionary()["value"] as? String, "s") + + struct Unknown: Equatable {} + let unknownDetails = FlagDetails(key: "k", value: Unknown(), variant: nil, reason: nil, error: nil) + XCTAssertTrue(unknownDetails.toSerializedDictionary()["value"] as? NSNull != nil) + } + + // MARK: - get*Details Tests + + func testGetBooleanDetails() { + let implementation = DdFlagsImplementation() + + let expectation = self.expectation(description: "Resolution called") + implementation.getBooleanDetails("default", key: "test_key", defaultValue: true, resolve: { result in + guard let dict = result as? [String: Any] else { + XCTFail("Expected dictionary result") + expectation.fulfill() + return + } + XCTAssertEqual(dict["value"] as? Bool, true) + expectation.fulfill() + }, reject: { _, _, _ in + XCTFail("Should not reject") + expectation.fulfill() + }) + + waitForExpectations(timeout: 1, handler: nil) + } + + func testGetStringDetails() { + let implementation = DdFlagsImplementation() + + let expectation = self.expectation(description: "Resolution called") + implementation.getStringDetails("default", key: "test_key", defaultValue: "default", resolve: { result in + guard let dict = result as? [String: Any] else { + XCTFail("Expected dictionary result") + expectation.fulfill() + return + } + XCTAssertEqual(dict["value"] as? String, "default") + expectation.fulfill() + }, reject: { _, _, _ in + XCTFail("Should not reject") + expectation.fulfill() + }) + + waitForExpectations(timeout: 1, handler: nil) + } + + func testGetNumberDetails() { + let implementation = DdFlagsImplementation() + + let expectation = self.expectation(description: "Resolution called") + implementation.getNumberDetails("default", key: "test_key", defaultValue: 123.45, resolve: { result in + guard let dict = result as? [String: Any] else { + XCTFail("Expected dictionary result") + expectation.fulfill() + return + } + XCTAssertEqual(dict["value"] as? Double, 123.45) + expectation.fulfill() + }, reject: { _, _, _ in + XCTFail("Should not reject") + expectation.fulfill() + }) + + waitForExpectations(timeout: 1, handler: nil) + } + + func testGetObjectDetails() { + let implementation = DdFlagsImplementation(core: core) + let defaultValue: [String: Any] = ["foo": "bar"] + + let expectation = self.expectation(description: "Resolution called") + implementation.getObjectDetails("default", key: "test_key", defaultValue: defaultValue, resolve: { result in + guard let dict = result as? [String: Any] else { + XCTFail("Expected dictionary result") + expectation.fulfill() + return + } + guard let value = dict["value"] as? [String: Any] else { + XCTFail("Expected dictionary value") + expectation.fulfill() + return + } + XCTAssertEqual(value["foo"] as? String, "bar") + expectation.fulfill() + }, reject: { _, _, _ in + XCTFail("Should not reject") + expectation.fulfill() + }) + + waitForExpectations(timeout: 1, handler: nil) + } +} + +private class FlagsTestCore: DatadogCoreProtocol { + private var features: [String: DatadogFeature] = [:] + + func register(feature: T) throws where T : DatadogFeature { + features[T.name] = feature + } + + func feature(named name: String, type: T.Type) -> T? { + return features[name] as? T + } + + func scope(for featureType: T.Type) -> any FeatureScope where T : DatadogFeature { + return NOPFeatureScope() + } + + func send(message: FeatureMessage, else fallback: @escaping () -> Void) {} + func set(context: @escaping () -> Context?) where Context: AdditionalContext {} + func mostRecentModifiedFileAt(before: Date) throws -> Date? { return nil } +} diff --git a/packages/core/ios/Tests/DdSdkTests.swift b/packages/core/ios/Tests/DdSdkTests.swift index c64f1acf7..e3c4334b3 100644 --- a/packages/core/ios/Tests/DdSdkTests.swift +++ b/packages/core/ios/Tests/DdSdkTests.swift @@ -8,6 +8,7 @@ import XCTest @testable import DatadogCore @testable import DatadogCrashReporting +@testable import DatadogFlags @testable import DatadogInternal @testable import DatadogLogs @testable import DatadogRUM @@ -309,6 +310,35 @@ class DdSdkTests: XCTestCase { XCTAssertNotNil(core.features[LogsFeature.name]) XCTAssertNotNil(core.features[TraceFeature.name]) } + + func testFlagsFeatureDisabledByDefault() { + let core = MockDatadogCore() + CoreRegistry.register(default: core) + defer { CoreRegistry.unregisterDefault() } + + let configuration: DdSdkConfiguration = .mockAny(configurationForFlags: nil) + + DdSdkNativeInitialization().enableFeatures( + sdkConfiguration: configuration + ) + + // Flagging SDK is disabled by default if no configuration is provided. + XCTAssertNil(core.features[FlagsFeature.name]) + } + + func testEnableFeatureFlags() { + let core = MockDatadogCore() + CoreRegistry.register(default: core) + defer { CoreRegistry.unregisterDefault() } + + let configuration: DdSdkConfiguration = .mockAny(configurationForFlags: ["enabled":true]) + + DdSdkNativeInitialization().enableFeatures( + sdkConfiguration: configuration + ) + + XCTAssertNotNil(core.features[FlagsFeature.name]) + } func testBuildConfigurationDefaultEndpoint() { let configuration: DdSdkConfiguration = .mockAny() diff --git a/packages/core/src/flags/__tests__/DatadogFlags.test.ts b/packages/core/src/flags/__tests__/DatadogFlags.test.ts new file mode 100644 index 000000000..84f9745a8 --- /dev/null +++ b/packages/core/src/flags/__tests__/DatadogFlags.test.ts @@ -0,0 +1,54 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +import { InternalLog } from '../../InternalLog'; +import { SdkVerbosity } from '../../SdkVerbosity'; +import { BufferSingleton } from '../../sdk/DatadogProvider/Buffer/BufferSingleton'; +import { DatadogFlags } from '../DatadogFlags'; + +jest.mock('../../InternalLog', () => { + return { + InternalLog: { + log: jest.fn() + }, + DATADOG_MESSAGE_PREFIX: 'DATADOG:' + }; +}); + +describe('DatadogFlags', () => { + beforeEach(() => { + jest.clearAllMocks(); + BufferSingleton.onInitialization(); + }); + + describe('Initialization', () => { + it('should print an error if retrieving the client before the feature is enabled', async () => { + DatadogFlags.getClient(); + + expect(InternalLog.log).toHaveBeenCalledWith( + 'DatadogFlags.getClient() called before DatadogFlags have been initialized. Flag evaluations will resolve to default values.', + SdkVerbosity.ERROR + ); + }); + + it('should print an error if retrieving the client if the feature was not enabled on purpose', async () => { + await DatadogFlags.enable({ enabled: false }); + DatadogFlags.getClient(); + + expect(InternalLog.log).toHaveBeenCalledWith( + 'DatadogFlags.getClient() called before DatadogFlags have been initialized. Flag evaluations will resolve to default values.', + SdkVerbosity.ERROR + ); + }); + + it('should not print an error if retrieving the client after the feature is enabled', async () => { + await DatadogFlags.enable({ enabled: true }); + DatadogFlags.getClient(); + + expect(InternalLog.log).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/packages/core/src/flags/__tests__/FlagsClient.test.ts b/packages/core/src/flags/__tests__/FlagsClient.test.ts new file mode 100644 index 000000000..8fd535d8d --- /dev/null +++ b/packages/core/src/flags/__tests__/FlagsClient.test.ts @@ -0,0 +1,197 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +import { NativeModules } from 'react-native'; + +import { InternalLog } from '../../InternalLog'; +import { SdkVerbosity } from '../../SdkVerbosity'; +import { BufferSingleton } from '../../sdk/DatadogProvider/Buffer/BufferSingleton'; +import { DatadogFlags } from '../DatadogFlags'; + +jest.mock('../../InternalLog', () => { + return { + InternalLog: { + log: jest.fn() + }, + DATADOG_MESSAGE_PREFIX: 'DATADOG:' + }; +}); + +describe('FlagsClient', () => { + beforeEach(async () => { + jest.clearAllMocks(); + BufferSingleton.onInitialization(); + + await DatadogFlags.enable({ enabled: true }); + }); + + describe('setEvaluationContext', () => { + it('should set the evaluation context', async () => { + const flagsClient = DatadogFlags.getClient(); + await flagsClient.setEvaluationContext({ + targetingKey: 'test-user-1', + attributes: { + country: 'US' + } + }); + + expect( + NativeModules.DdFlags.setEvaluationContext + ).toHaveBeenCalledWith('default', 'test-user-1', { country: 'US' }); + }); + + it('should print an error if there is an error', async () => { + NativeModules.DdFlags.setEvaluationContext.mockRejectedValue( + new Error('NETWORK_ERROR') + ); + + const flagsClient = DatadogFlags.getClient(); + await flagsClient.setEvaluationContext({ + targetingKey: 'test-user-1', + attributes: { + country: 'US' + } + }); + + expect(InternalLog.log).toHaveBeenCalledWith( + 'Error setting flag evaluation context: NETWORK_ERROR', + SdkVerbosity.ERROR + ); + }); + }); + + describe('getBooleanDetails', () => { + it('should fail the validation if the default value is not valid', async () => { + const flagsClient = DatadogFlags.getClient(); + const details = await flagsClient.getBooleanDetails( + 'test-boolean-flag', + // @ts-expect-error - we want to test the validation + 'true' + ); + + expect(details).toMatchObject({ + value: 'true', // The default value is passed through. + error: 'TYPE_MISMATCH', + reason: null, + variant: null + }); + }); + + it('should fetch the boolean details from native side', async () => { + const flagsClient = DatadogFlags.getClient(); + const details = await flagsClient.getBooleanDetails( + 'test-boolean-flag', + true + ); + + expect(details).toMatchObject({ + value: true, + variant: 'true', + reason: 'STATIC', + error: null + }); + }); + }); + + describe('getStringDetails', () => { + it('should fail the validation if the default value is not valid', async () => { + const flagsClient = DatadogFlags.getClient(); + const details = await flagsClient.getStringDetails( + 'test-string-flag', + // @ts-expect-error - we want to test the validation + true + ); + + expect(details).toMatchObject({ + value: true, // The default value is passed through. + error: 'TYPE_MISMATCH', + reason: null, + variant: null + }); + }); + + it('should fetch the string details from native side', async () => { + const flagsClient = DatadogFlags.getClient(); + const details = await flagsClient.getStringDetails( + 'test-string-flag', + 'hello world' + ); + + expect(details).toMatchObject({ + value: 'hello world', + variant: 'hello world', + reason: 'STATIC', + error: null + }); + }); + }); + + describe('getNumberDetails', () => { + it('should fail the validation if the default value is not valid', async () => { + const flagsClient = DatadogFlags.getClient(); + const details = await flagsClient.getNumberDetails( + 'test-number-flag', + // @ts-expect-error - we want to test the validation + 'hello world' + ); + + expect(details).toMatchObject({ + value: 'hello world', // The default value is passed through. + error: 'TYPE_MISMATCH', + reason: null, + variant: null + }); + }); + + it('should fetch the number details from native side', async () => { + const flagsClient = DatadogFlags.getClient(); + const details = await flagsClient.getNumberDetails( + 'test-number-flag', + 6 + ); + + expect(details).toMatchObject({ + value: 6, + variant: '6', + reason: 'STATIC', + error: null + }); + }); + }); + + describe('getObjectDetails', () => { + it('should fail the validation if the default value is not valid', async () => { + const flagsClient = DatadogFlags.getClient(); + const details = await flagsClient.getObjectDetails( + 'test-object-flag', + // @ts-expect-error - we want to test the validation + 'hello world' + ); + + expect(details).toMatchObject({ + value: 'hello world', // The default value is passed through. + error: 'TYPE_MISMATCH', + reason: null, + variant: null + }); + }); + + it('should fetch the object details from native side', async () => { + const flagsClient = DatadogFlags.getClient(); + const details = await flagsClient.getObjectDetails( + 'test-object-flag', + { hello: 'world' } + ); + + expect(details).toMatchObject({ + value: { hello: 'world' }, + variant: 'hello world', + reason: 'STATIC', + error: null + }); + }); + }); +}); From 8800f64c44a0a42ea18a90d85077225ec0b5fab6 Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Tue, 25 Nov 2025 17:37:07 +0200 Subject: [PATCH 21/64] Implement `DatadogFlags.enable()` method, add JSDoc comments to DatadogFlags --- example-new-architecture/App.tsx | 7 +- packages/core/ios/Sources/DdFlags.mm | 12 +++ .../ios/Sources/DdFlagsImplementation.swift | 11 +++ packages/core/src/flags/DatadogFlags.ts | 75 ++++++++++++++++--- packages/core/src/flags/types.ts | 49 ++++++++++-- packages/core/src/specs/NativeDdFlags.ts | 5 +- 6 files changed, 132 insertions(+), 27 deletions(-) diff --git a/example-new-architecture/App.tsx b/example-new-architecture/App.tsx index f6aecaac5..5e921c234 100644 --- a/example-new-architecture/App.tsx +++ b/example-new-architecture/App.tsx @@ -46,11 +46,7 @@ import {APPLICATION_ID, CLIENT_TOKEN, ENVIRONMENT} from './ddCredentials'; config.telemetrySampleRate = 100; config.uploadFrequency = UploadFrequency.FREQUENT; config.batchSize = BatchSize.SMALL; - config.flagsConfiguration = { - enabled: true, - }; await DdSdkReactNative.initialize(config); - await DatadogFlags.enable(config.flagsConfiguration); await DdRum.startView('main', 'Main'); setTimeout(async () => { await DdRum.addTiming('one_second'); @@ -95,6 +91,8 @@ function App(): React.JSX.Element { const [testFlagValue, setTestFlagValue] = React.useState(false); React.useEffect(() => { (async () => { + await DatadogFlags.enable(); + const flagsClient = DatadogFlags.getClient(); await flagsClient.setEvaluationContext({ targetingKey: 'test-user-1', @@ -103,7 +101,6 @@ function App(): React.JSX.Element { }, }); const flag = await flagsClient.getBooleanDetails('rn-sdk-test-boolean-flag', false); // https://app.datadoghq.com/feature-flags/046d0e70-626d-41e1-8314-3f009fb79b7a?environmentId=d114cd9a-79ed-4c56-bcf3-bcac9293653b - console.log({flag}) setTestFlagValue(flag.value); })(); }, []); diff --git a/packages/core/ios/Sources/DdFlags.mm b/packages/core/ios/Sources/DdFlags.mm index 59939d45b..cbf3abc7e 100644 --- a/packages/core/ios/Sources/DdFlags.mm +++ b/packages/core/ios/Sources/DdFlags.mm @@ -16,6 +16,14 @@ @implementation DdFlags RCT_EXPORT_MODULE() +RCT_REMAP_METHOD(enable, + withConfiguration:(NSDictionary *)configuration + withResolve:(RCTPromiseResolveBlock)resolve + withReject:(RCTPromiseRejectBlock)reject) +{ + [self enable:configuration resolve:resolve reject:reject]; +} + RCT_REMAP_METHOD(setEvaluationContext, withClientName:(NSString *)clientName withTargetingKey:(NSString *)targetingKey @@ -91,6 +99,10 @@ - (dispatch_queue_t)methodQueue { return [RNQueue getSharedQueue]; } +- (void)enable:(NSDictionary *)configuration resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + [self.ddFlagsImplementation enable:configuration resolve:resolve reject:reject]; +} + - (void)setEvaluationContext:(NSString *)clientName targetingKey:(NSString *)targetingKey attributes:(NSDictionary *)attributes resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { [self.ddFlagsImplementation setEvaluationContext:clientName targetingKey:targetingKey attributes:attributes resolve:resolve reject:reject]; } diff --git a/packages/core/ios/Sources/DdFlagsImplementation.swift b/packages/core/ios/Sources/DdFlagsImplementation.swift index 7b128db17..3b2ed4faf 100644 --- a/packages/core/ios/Sources/DdFlagsImplementation.swift +++ b/packages/core/ios/Sources/DdFlagsImplementation.swift @@ -25,6 +25,17 @@ public class DdFlagsImplementation: NSObject { self.init(core: CoreRegistry.default) } + @objc + public func enable(_ configuration: NSDictionary, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) { + if let config = configuration.asConfigurationForFlags() { + Flags.enable(with: config) + } else { + consolePrint("Invalid configuration provided for Flags. Feature initialization skipped.", .error) + } + + resolve(nil) + } + /// Retrieve a `FlagsClient` instance in a non-interruptive way for usage in methods bridged to React Native. /// /// We create a simple registry of client providers by client name holding closures for retrieving a client since client references are kept internally in the flagging SDK. diff --git a/packages/core/src/flags/DatadogFlags.ts b/packages/core/src/flags/DatadogFlags.ts index 195881112..206bbb276 100644 --- a/packages/core/src/flags/DatadogFlags.ts +++ b/packages/core/src/flags/DatadogFlags.ts @@ -6,6 +6,7 @@ import { InternalLog } from '../InternalLog'; import { SdkVerbosity } from '../SdkVerbosity'; +import type { DdNativeFlagsType } from '../nativeModulesTypes'; import { getGlobalInstance } from '../utils/singletonUtils'; import { FlagsClient } from './FlagsClient'; @@ -14,27 +15,81 @@ import type { DatadogFlagsType, DatadogFlagsConfiguration } from './types'; const FLAGS_MODULE = 'com.datadog.reactnative.flags'; class DatadogFlagsWrapper implements DatadogFlagsType { + // eslint-disable-next-line global-require, @typescript-eslint/no-var-requires + private nativeFlags: DdNativeFlagsType = require('../specs/NativeDdFlags') + .default; + private isFeatureEnabled = false; + /** + * Enables the Datadog Flags feature in your application. + * + * Call this method after initializing the Datadog SDK to enable feature flag evaluation. + * This method must be called before creating any `FlagsClient` instances via `DatadogFlags.getClient()`. + * + * @example + * ```ts + * import { DdSdkReactNativeConfiguration, DdSdkReactNative, DatadogFlags } from '@datadog/mobile-react-native'; + * + * // Initialize the Datadog SDK. + * await DdSdkReactNative.initialize(...); + * + * // Optinal flags configuration object. + * const flagsConfig = { + * customFlagsEndpoint: 'https://flags.example.com' + * }; + * + * // Enable the feature. + * await DatadogFlags.enable(flagsConfig); + * + * // Retrieve the client and access feature flags. + * const flagsClient = DatadogFlags.getClient(); + * const flagValue = await flagsClient.getBooleanValue('new-feature', false); + * ``` + * + * @param configuration Configuration options for the Datadog Flags feature. + */ + enable = async ( + configuration?: Omit + ): Promise => { + if (this.isFeatureEnabled) { + InternalLog.log( + 'Datadog Flags feature has already been enabled. Skipping this `DatadogFlags.enable()` call.', + SdkVerbosity.WARN + ); + return; + } + + await this.nativeFlags.enable({ ...configuration, enabled: true }); + + this.isFeatureEnabled = true; + }; + + /** + * Returns a `FlagsClient` instance for further feature flag evaluation. + * + * For most applications, you would need only one client. If you need multiple clients, + * you can retrieve a couple of clients with different names. + * + * @param clientName An optional name of the client to retrieve. Defaults to `'default'`. + * + * @example + * ```ts + * // Reminder: you need to initialize the SDK and enable the Flags feature before retrieving the client. + * const flagsClient = DatadogFlags.getClient(); + * const flagValue = await flagsClient.getBooleanValue('new-feature', false); + * ``` + */ getClient = (clientName: string = 'default'): FlagsClient => { if (!this.isFeatureEnabled) { InternalLog.log( - 'DatadogFlags.getClient() called before DatadogFlags have been initialized. Flag evaluations will resolve to default values.', + '`DatadogFlags.getClient()` called before Datadog Flags feature have been enabled. Client will fall back to serving default flag values.', SdkVerbosity.ERROR ); } return new FlagsClient(clientName); }; - - enable = async ( - _configuration: DatadogFlagsConfiguration - ): Promise => { - // Feature Flags are initialized globally by default for now. - this.isFeatureEnabled = _configuration.enabled; - - return Promise.resolve(); - }; } export const DatadogFlags: DatadogFlagsType = getGlobalInstance( diff --git a/packages/core/src/flags/types.ts b/packages/core/src/flags/types.ts index 6ecb0880f..43ff9aa7f 100644 --- a/packages/core/src/flags/types.ts +++ b/packages/core/src/flags/types.ts @@ -8,17 +8,50 @@ import type { FlagsClient } from './FlagsClient'; export type DatadogFlagsType = { /** - * Returns a `FlagsClient` instance for further feature flag evaluation. + * Enables the Datadog Flags feature in your application. + * + * Call this method after initializing the Datadog SDK to enable feature flag evaluation. + * This method must be called before creating any `FlagsClient` instances via `DatadogFlags.getClient()`. + * + * @example + * ```ts + * import { DdSdkReactNativeConfiguration, DdSdkReactNative, DatadogFlags } from '@datadog/mobile-react-native'; + * + * // Initialize the Datadog SDK. + * await DdSdkReactNative.initialize(...); + * + * // Optinal flags configuration object. + * const flagsConfig = { + * customFlagsEndpoint: 'https://flags.example.com' + * }; + * + * // Enable the feature. + * await DatadogFlags.enable(flagsConfig); + * + * // Retrieve the client and access feature flags. + * const flagsClient = DatadogFlags.getClient(); + * const flagValue = await flagsClient.getBooleanValue('new-feature', false); + * ``` * - * If client name is not provided, the `'default'` client is returned. + * @param configuration Configuration options for the Datadog Flags feature. */ - getClient: (clientName?: string) => FlagsClient; + enable: (configuration?: DatadogFlagsConfiguration) => Promise; /** - * Enables the Datadog Flags feature. + * Returns a `FlagsClient` instance for further feature flag evaluation. + * + * For most applications, you would need only one client. If you need multiple clients, + * you can retrieve a couple of clients with different names. * - * TODO: This method is no-op for now, as flags are initialized globally by default. + * @param clientName An optional name of the client to retrieve. Defaults to `'default'`. + * + * @example + * ```ts + * // Reminder: you need to initialize the SDK and enable the Flags feature before retrieving the client. + * const flagsClient = DatadogFlags.getClient(); + * const flagValue = await flagsClient.getBooleanValue('new-feature', false); + * ``` */ - enable: (configuration: DatadogFlagsConfiguration) => Promise; + getClient: (clientName?: string) => FlagsClient; }; /** @@ -27,7 +60,7 @@ export type DatadogFlagsType = { * Use this type to customize the behavior of feature flag evaluation, including custom endpoints, * exposure tracking, and error handling modes. */ -export interface DatadogFlagsConfiguration { +export type DatadogFlagsConfiguration = { /** * Controls whether the feature flag evaluation feature is enabled. */ @@ -72,7 +105,7 @@ export interface DatadogFlagsConfiguration { * @default true */ rumIntegrationEnabled?: boolean; -} +}; /** * Context information used for feature flag targeting and evaluation. diff --git a/packages/core/src/specs/NativeDdFlags.ts b/packages/core/src/specs/NativeDdFlags.ts index 52461c660..27d349319 100644 --- a/packages/core/src/specs/NativeDdFlags.ts +++ b/packages/core/src/specs/NativeDdFlags.ts @@ -14,10 +14,7 @@ import type { FlagDetails } from '../flags/types'; * Do not import this Spec directly, use DdNativeFlagsType instead. */ export interface Spec extends TurboModule { - // TODO: Flags and all other features are initialized globally for now. We want to change this in the future. - // readonly enable: ( - // configuration: DatadogFlagsConfiguration - // ) => Promise; + readonly enable: (configuration: Object) => Promise; readonly setEvaluationContext: ( clientName: string, From e266879557ac1f6c7c10b37885ab53ad9a58d56b Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Tue, 25 Nov 2025 17:38:27 +0200 Subject: [PATCH 22/64] Remove redundant `gracefulModeEnabled` setting --- packages/core/src/DdSdkReactNative.tsx | 8 -------- 1 file changed, 8 deletions(-) diff --git a/packages/core/src/DdSdkReactNative.tsx b/packages/core/src/DdSdkReactNative.tsx index 5da26b249..8954ea632 100644 --- a/packages/core/src/DdSdkReactNative.tsx +++ b/packages/core/src/DdSdkReactNative.tsx @@ -354,14 +354,6 @@ export class DdSdkReactNative { ] = `${reactNativeVersion}`; } - // Hard set `gracefulModeEnabled` to `true` because crashing an app on misconfiguration - // is not the usual workflow for React Native. - if (configuration.flagsConfiguration) { - Object.assign(configuration.flagsConfiguration, { - gracefulModeEnabled: true - }); - } - return new DdSdkConfiguration( configuration.clientToken, configuration.env, From 0e6a850ae01e809777aa409731d8b8fb0bce89a0 Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Tue, 25 Nov 2025 17:42:58 +0200 Subject: [PATCH 23/64] Move the `DatadogFlags.enable` call to `initializeNativeSDK` --- example-new-architecture/App.tsx | 5 +++-- packages/core/src/DdSdkReactNative.tsx | 8 ++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/example-new-architecture/App.tsx b/example-new-architecture/App.tsx index 5e921c234..a01451704 100644 --- a/example-new-architecture/App.tsx +++ b/example-new-architecture/App.tsx @@ -46,6 +46,9 @@ import {APPLICATION_ID, CLIENT_TOKEN, ENVIRONMENT} from './ddCredentials'; config.telemetrySampleRate = 100; config.uploadFrequency = UploadFrequency.FREQUENT; config.batchSize = BatchSize.SMALL; + config.flagsConfiguration = { + enabled: true, + }; await DdSdkReactNative.initialize(config); await DdRum.startView('main', 'Main'); setTimeout(async () => { @@ -91,8 +94,6 @@ function App(): React.JSX.Element { const [testFlagValue, setTestFlagValue] = React.useState(false); React.useEffect(() => { (async () => { - await DatadogFlags.enable(); - const flagsClient = DatadogFlags.getClient(); await flagsClient.setEvaluationContext({ targetingKey: 'test-user-1', diff --git a/packages/core/src/DdSdkReactNative.tsx b/packages/core/src/DdSdkReactNative.tsx index 8954ea632..c07f54a89 100644 --- a/packages/core/src/DdSdkReactNative.tsx +++ b/packages/core/src/DdSdkReactNative.tsx @@ -99,6 +99,10 @@ export class DdSdkReactNative { DdSdkReactNative.buildConfiguration(configuration, params) ); + if (configuration.flagsConfiguration) { + await DatadogFlags.enable(configuration.flagsConfiguration); + } + InternalLog.log('Datadog SDK was initialized', SdkVerbosity.INFO); GlobalState.instance.isInitialized = true; BufferSingleton.onInitialization(); @@ -465,10 +469,6 @@ export class DdSdkReactNative { DdRum.registerActionEventMapper(configuration.actionEventMapper); } - if (configuration.flagsConfiguration) { - DatadogFlags.enable(configuration.flagsConfiguration); - } - DdSdkReactNative.wasAutoInstrumented = true; } } From ab6fa5f7b42abd426e65c36b9613455392c8bfb6 Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Tue, 25 Nov 2025 22:44:00 +0200 Subject: [PATCH 24/64] Add more tests --- packages/core/__mocks__/react-native.ts | 1 + packages/core/src/DdSdkReactNative.tsx | 5 +- .../src/DdSdkReactNativeConfiguration.tsx | 1 + .../src/__tests__/DdSdkReactNative.test.tsx | 115 +++++++++ .../DdSdkReactNativeConfiguration.test.ts | 222 ++++++++++++++++++ .../src/flags/__tests__/DatadogFlags.test.ts | 25 +- 6 files changed, 356 insertions(+), 13 deletions(-) diff --git a/packages/core/__mocks__/react-native.ts b/packages/core/__mocks__/react-native.ts index 8e8b22d78..336e6be71 100644 --- a/packages/core/__mocks__/react-native.ts +++ b/packages/core/__mocks__/react-native.ts @@ -159,6 +159,7 @@ actualRN.NativeModules.DdRum = { }; actualRN.NativeModules.DdFlags = { + enable: jest.fn().mockImplementation(() => Promise.resolve()), setEvaluationContext: jest.fn().mockImplementation(() => Promise.resolve()), getBooleanDetails: jest.fn().mockImplementation(() => Promise.resolve({ diff --git a/packages/core/src/DdSdkReactNative.tsx b/packages/core/src/DdSdkReactNative.tsx index c07f54a89..9865a0f37 100644 --- a/packages/core/src/DdSdkReactNative.tsx +++ b/packages/core/src/DdSdkReactNative.tsx @@ -99,7 +99,10 @@ export class DdSdkReactNative { DdSdkReactNative.buildConfiguration(configuration, params) ); - if (configuration.flagsConfiguration) { + if ( + configuration.flagsConfiguration && + configuration.flagsConfiguration.enabled !== false + ) { await DatadogFlags.enable(configuration.flagsConfiguration); } diff --git a/packages/core/src/DdSdkReactNativeConfiguration.tsx b/packages/core/src/DdSdkReactNativeConfiguration.tsx index 5df2ff8d9..94db29814 100644 --- a/packages/core/src/DdSdkReactNativeConfiguration.tsx +++ b/packages/core/src/DdSdkReactNativeConfiguration.tsx @@ -485,6 +485,7 @@ export type PartialInitializationConfiguration = { readonly batchProcessingLevel?: BatchProcessingLevel; readonly initialResourceThreshold?: number; readonly trackMemoryWarnings?: boolean; + readonly flagsConfiguration?: DatadogFlagsConfiguration; }; const setConfigurationAttribute = < diff --git a/packages/core/src/__tests__/DdSdkReactNative.test.tsx b/packages/core/src/__tests__/DdSdkReactNative.test.tsx index 5e6f8c447..6ce8a05d6 100644 --- a/packages/core/src/__tests__/DdSdkReactNative.test.tsx +++ b/packages/core/src/__tests__/DdSdkReactNative.test.tsx @@ -12,6 +12,7 @@ import { DdSdkReactNative } from '../DdSdkReactNative'; import { ProxyConfiguration, ProxyType } from '../ProxyConfiguration'; import { SdkVerbosity } from '../SdkVerbosity'; import { TrackingConsent } from '../TrackingConsent'; +import { DatadogFlags } from '../flags/DatadogFlags'; import { DdLogs } from '../logs/DdLogs'; import { DdRum } from '../rum/DdRum'; import { DdRumErrorTracking } from '../rum/instrumentation/DdRumErrorTracking'; @@ -28,6 +29,14 @@ import { version as sdkVersion } from '../version'; jest.mock('../InternalLog'); +jest.mock('../flags/DatadogFlags', () => { + return { + DatadogFlags: { + enable: jest.fn().mockResolvedValue(undefined) + } + }; +}); + jest.mock( '../rum/instrumentation/interactionTracking/DdRumUserInteractionTracking', () => { @@ -75,6 +84,9 @@ beforeEach(async () => { (DdRumErrorTracking.startTracking as jest.MockedFunction< typeof DdRumErrorTracking.startTracking >).mockClear(); + (DatadogFlags.enable as jest.MockedFunction< + typeof DatadogFlags.enable + >).mockClear(); DdLogs.unregisterLogEventMapper(); UserInfoSingleton.reset(); @@ -507,6 +519,109 @@ describe('DdSdkReactNative', () => { }) ); }); + + it('does not enable DatadogFlags when flagsConfiguration is not provided', async () => { + // GIVEN + const fakeAppId = '1'; + const fakeClientToken = '2'; + const fakeEnvName = 'env'; + const configuration = new DdSdkReactNativeConfiguration( + fakeClientToken, + fakeEnvName, + fakeAppId + ); + + NativeModules.DdSdk.initialize.mockResolvedValue(null); + + // WHEN + await DdSdkReactNative.initialize(configuration); + + // THEN + expect(DatadogFlags.enable).not.toHaveBeenCalled(); + }); + + it('enables DatadogFlags when flagsConfiguration is provided', async () => { + // GIVEN + const fakeAppId = '1'; + const fakeClientToken = '2'; + const fakeEnvName = 'env'; + const configuration = new DdSdkReactNativeConfiguration( + fakeClientToken, + fakeEnvName, + fakeAppId + ); + configuration.flagsConfiguration = { + enabled: true + }; + + NativeModules.DdSdk.initialize.mockResolvedValue(null); + + // WHEN + await DdSdkReactNative.initialize(configuration); + + // THEN + expect(DatadogFlags.enable).toHaveBeenCalledTimes(1); + expect(DatadogFlags.enable).toHaveBeenCalledWith({ + enabled: true + }); + }); + + it('enables DatadogFlags with custom configuration when provided', async () => { + // GIVEN + const fakeAppId = '1'; + const fakeClientToken = '2'; + const fakeEnvName = 'env'; + const customFlagsEndpoint = 'https://flags.example.com'; + const customFlagsHeaders = { + Authorization: 'Bearer token123' + }; + const configuration = new DdSdkReactNativeConfiguration( + fakeClientToken, + fakeEnvName, + fakeAppId + ); + configuration.flagsConfiguration = { + enabled: true, + customFlagsEndpoint, + customFlagsHeaders + }; + + NativeModules.DdSdk.initialize.mockResolvedValue(null); + + // WHEN + await DdSdkReactNative.initialize(configuration); + + // THEN + expect(DatadogFlags.enable).toHaveBeenCalledTimes(1); + expect(DatadogFlags.enable).toHaveBeenCalledWith({ + enabled: true, + customFlagsEndpoint, + customFlagsHeaders + }); + }); + + it('does not call DatadogFlags.enable when flagsConfiguration.enabled is false', async () => { + // GIVEN + const fakeAppId = '1'; + const fakeClientToken = '2'; + const fakeEnvName = 'env'; + const configuration = new DdSdkReactNativeConfiguration( + fakeClientToken, + fakeEnvName, + fakeAppId + ); + configuration.flagsConfiguration = { + enabled: false + }; + + NativeModules.DdSdk.initialize.mockResolvedValue(null); + + // WHEN + await DdSdkReactNative.initialize(configuration); + + // THEN + expect(DatadogFlags.enable).not.toHaveBeenCalled(); + }); }); describe('feature enablement', () => { diff --git a/packages/core/src/__tests__/DdSdkReactNativeConfiguration.test.ts b/packages/core/src/__tests__/DdSdkReactNativeConfiguration.test.ts index 60a9d13ff..8193621c7 100644 --- a/packages/core/src/__tests__/DdSdkReactNativeConfiguration.test.ts +++ b/packages/core/src/__tests__/DdSdkReactNativeConfiguration.test.ts @@ -261,5 +261,227 @@ describe('DdSdkReactNativeConfiguration', () => { } `); }); + + it('builds the SDK configuration with flags configuration enabled', () => { + expect( + buildConfigurationFromPartialConfiguration( + { + trackErrors: false, + trackInteractions: false, + trackResources: false + }, + { + applicationId: 'fake-app-id', + clientToken: 'fake-client-token', + env: 'fake-env', + flagsConfiguration: { + enabled: true, + customFlagsEndpoint: 'https://flags.example.com', + customFlagsHeaders: { + Authorization: 'Bearer token123', + 'X-Custom-Header': 'custom-value' + } + } + } + ) + ).toMatchInlineSnapshot(` + DdSdkReactNativeConfiguration { + "actionEventMapper": null, + "additionalConfiguration": {}, + "applicationId": "fake-app-id", + "batchProcessingLevel": "MEDIUM", + "batchSize": "MEDIUM", + "bundleLogsWithRum": true, + "bundleLogsWithTraces": true, + "clientToken": "fake-client-token", + "customEndpoints": {}, + "env": "fake-env", + "errorEventMapper": null, + "firstPartyHosts": [], + "flagsConfiguration": { + "customFlagsEndpoint": "https://flags.example.com", + "customFlagsHeaders": { + "Authorization": "Bearer token123", + "X-Custom-Header": "custom-value", + }, + "enabled": true, + }, + "logEventMapper": null, + "longTaskThresholdMs": 0, + "nativeCrashReportEnabled": false, + "nativeInteractionTracking": false, + "nativeLongTaskThresholdMs": 200, + "nativeViewTracking": false, + "proxyConfig": undefined, + "resourceEventMapper": null, + "resourceTracingSamplingRate": 100, + "serviceName": undefined, + "sessionSamplingRate": 100, + "site": "US1", + "telemetrySampleRate": 20, + "trackBackgroundEvents": false, + "trackErrors": false, + "trackFrustrations": true, + "trackInteractions": false, + "trackMemoryWarnings": true, + "trackResources": false, + "trackWatchdogTerminations": false, + "trackingConsent": "granted", + "uploadFrequency": "AVERAGE", + "useAccessibilityLabel": true, + "verbosity": undefined, + "vitalsUpdateFrequency": "AVERAGE", + } + `); + }); + + it('builds the SDK configuration with flags configuration disabled', () => { + expect( + buildConfigurationFromPartialConfiguration( + { + trackErrors: false, + trackInteractions: false, + trackResources: false + }, + { + applicationId: 'fake-app-id', + clientToken: 'fake-client-token', + env: 'fake-env', + flagsConfiguration: { + enabled: false + } + } + ) + ).toMatchInlineSnapshot(` + DdSdkReactNativeConfiguration { + "actionEventMapper": null, + "additionalConfiguration": {}, + "applicationId": "fake-app-id", + "batchProcessingLevel": "MEDIUM", + "batchSize": "MEDIUM", + "bundleLogsWithRum": true, + "bundleLogsWithTraces": true, + "clientToken": "fake-client-token", + "customEndpoints": {}, + "env": "fake-env", + "errorEventMapper": null, + "firstPartyHosts": [], + "flagsConfiguration": { + "enabled": false, + }, + "logEventMapper": null, + "longTaskThresholdMs": 0, + "nativeCrashReportEnabled": false, + "nativeInteractionTracking": false, + "nativeLongTaskThresholdMs": 200, + "nativeViewTracking": false, + "proxyConfig": undefined, + "resourceEventMapper": null, + "resourceTracingSamplingRate": 100, + "serviceName": undefined, + "sessionSamplingRate": 100, + "site": "US1", + "telemetrySampleRate": 20, + "trackBackgroundEvents": false, + "trackErrors": false, + "trackFrustrations": true, + "trackInteractions": false, + "trackMemoryWarnings": true, + "trackResources": false, + "trackWatchdogTerminations": false, + "trackingConsent": "granted", + "uploadFrequency": "AVERAGE", + "useAccessibilityLabel": true, + "verbosity": undefined, + "vitalsUpdateFrequency": "AVERAGE", + } + `); + }); + + it('builds the SDK configuration with full flags configuration', () => { + expect( + buildConfigurationFromPartialConfiguration( + { + trackErrors: true, + trackInteractions: true, + trackResources: true, + firstPartyHosts: ['api.com'], + resourceTracingSamplingRate: 80 + }, + { + applicationId: 'fake-app-id', + clientToken: 'fake-client-token', + env: 'fake-env', + sessionSamplingRate: 80, + site: 'EU', + verbosity: SdkVerbosity.DEBUG, + serviceName: 'com.test.app', + version: '1.4.5', + flagsConfiguration: { + enabled: true, + customFlagsEndpoint: 'https://flags.example.com', + customFlagsHeaders: { + Authorization: 'Bearer token123' + }, + customExposureEndpoint: + 'https://exposure.example.com', + trackExposures: true + } + } + ) + ).toMatchInlineSnapshot(` + DdSdkReactNativeConfiguration { + "actionEventMapper": null, + "additionalConfiguration": {}, + "applicationId": "fake-app-id", + "batchProcessingLevel": "MEDIUM", + "batchSize": "MEDIUM", + "bundleLogsWithRum": true, + "bundleLogsWithTraces": true, + "clientToken": "fake-client-token", + "customEndpoints": {}, + "env": "fake-env", + "errorEventMapper": null, + "firstPartyHosts": [ + "api.com", + ], + "flagsConfiguration": { + "customExposureEndpoint": "https://exposure.example.com", + "customFlagsEndpoint": "https://flags.example.com", + "customFlagsHeaders": { + "Authorization": "Bearer token123", + }, + "enabled": true, + "trackExposures": true, + }, + "logEventMapper": null, + "longTaskThresholdMs": 0, + "nativeCrashReportEnabled": false, + "nativeInteractionTracking": false, + "nativeLongTaskThresholdMs": 200, + "nativeViewTracking": false, + "proxyConfig": undefined, + "resourceEventMapper": null, + "resourceTracingSamplingRate": 80, + "serviceName": "com.test.app", + "sessionSamplingRate": 80, + "site": "EU", + "telemetrySampleRate": 20, + "trackBackgroundEvents": false, + "trackErrors": true, + "trackFrustrations": true, + "trackInteractions": true, + "trackMemoryWarnings": true, + "trackResources": true, + "trackWatchdogTerminations": false, + "trackingConsent": "granted", + "uploadFrequency": "AVERAGE", + "useAccessibilityLabel": true, + "verbosity": "debug", + "version": "1.4.5", + "vitalsUpdateFrequency": "AVERAGE", + } + `); + }); }); }); diff --git a/packages/core/src/flags/__tests__/DatadogFlags.test.ts b/packages/core/src/flags/__tests__/DatadogFlags.test.ts index 84f9745a8..70a76102f 100644 --- a/packages/core/src/flags/__tests__/DatadogFlags.test.ts +++ b/packages/core/src/flags/__tests__/DatadogFlags.test.ts @@ -4,9 +4,10 @@ * Copyright 2016-Present Datadog, Inc. */ +import { NativeModules } from 'react-native'; + import { InternalLog } from '../../InternalLog'; import { SdkVerbosity } from '../../SdkVerbosity'; -import { BufferSingleton } from '../../sdk/DatadogProvider/Buffer/BufferSingleton'; import { DatadogFlags } from '../DatadogFlags'; jest.mock('../../InternalLog', () => { @@ -21,31 +22,31 @@ jest.mock('../../InternalLog', () => { describe('DatadogFlags', () => { beforeEach(() => { jest.clearAllMocks(); - BufferSingleton.onInitialization(); + // Reset state of DatadogFlags instance. + Object.assign(DatadogFlags, { isFeatureEnabled: false }); }); describe('Initialization', () => { - it('should print an error if retrieving the client before the feature is enabled', async () => { - DatadogFlags.getClient(); + it('should print an error if calling DatadogFlags.enable() for multiple times', async () => { + await DatadogFlags.enable(); + await DatadogFlags.enable(); + await DatadogFlags.enable(); - expect(InternalLog.log).toHaveBeenCalledWith( - 'DatadogFlags.getClient() called before DatadogFlags have been initialized. Flag evaluations will resolve to default values.', - SdkVerbosity.ERROR - ); + expect(InternalLog.log).toHaveBeenCalledTimes(2); + expect(NativeModules.DdFlags.enable).toHaveBeenCalledTimes(1); }); - it('should print an error if retrieving the client if the feature was not enabled on purpose', async () => { - await DatadogFlags.enable({ enabled: false }); + it('should print an error if retrieving the client before the feature is enabled', async () => { DatadogFlags.getClient(); expect(InternalLog.log).toHaveBeenCalledWith( - 'DatadogFlags.getClient() called before DatadogFlags have been initialized. Flag evaluations will resolve to default values.', + '`DatadogFlags.getClient()` called before Datadog Flags feature have been enabled. Client will fall back to serving default flag values.', SdkVerbosity.ERROR ); }); it('should not print an error if retrieving the client after the feature is enabled', async () => { - await DatadogFlags.enable({ enabled: true }); + await DatadogFlags.enable(); DatadogFlags.getClient(); expect(InternalLog.log).not.toHaveBeenCalled(); From 3ef21c52772a5e565e592405a0ba4bc814da63fb Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Tue, 25 Nov 2025 22:46:05 +0200 Subject: [PATCH 25/64] Update Podfile.lock --- example-new-architecture/ios/Podfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example-new-architecture/ios/Podfile.lock b/example-new-architecture/ios/Podfile.lock index 4a49e5325..0b8ceafb3 100644 --- a/example-new-architecture/ios/Podfile.lock +++ b/example-new-architecture/ios/Podfile.lock @@ -1981,6 +1981,6 @@ SPEC CHECKSUMS: SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 Yoga: feb4910aba9742cfedc059e2b2902e22ffe9954a -PODFILE CHECKSUM: c0a8ceabe25801227323f2a415c464bfca5e3f73 +PODFILE CHECKSUM: 470f1ade1ca669373855527342da02c29dfcdfdf COCOAPODS: 1.16.2 From ea1967de181de080b4ee745de5ef18328e196b3e Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Mon, 1 Dec 2025 14:17:29 +0200 Subject: [PATCH 26/64] Remove Flags initialization and configuration logic from the shared init step --- example-new-architecture/App.tsx | 2 ++ example/src/WixApp.tsx | 3 ++- example/src/ddUtils.tsx | 13 +------------ example/src/screens/MainScreen.tsx | 2 ++ .../ios/Sources/DdSdkNativeInitialization.swift | 5 ----- packages/core/src/DdSdkReactNative.tsx | 11 +---------- packages/core/src/DdSdkReactNativeConfiguration.tsx | 5 ----- packages/core/src/types.tsx | 4 +--- 8 files changed, 9 insertions(+), 36 deletions(-) diff --git a/example-new-architecture/App.tsx b/example-new-architecture/App.tsx index a01451704..f0f4434dd 100644 --- a/example-new-architecture/App.tsx +++ b/example-new-architecture/App.tsx @@ -94,6 +94,8 @@ function App(): React.JSX.Element { const [testFlagValue, setTestFlagValue] = React.useState(false); React.useEffect(() => { (async () => { + await DatadogFlags.enable(); + const flagsClient = DatadogFlags.getClient(); await flagsClient.setEvaluationContext({ targetingKey: 'test-user-1', diff --git a/example/src/WixApp.tsx b/example/src/WixApp.tsx index dc6ed2dc5..6f6482c10 100644 --- a/example/src/WixApp.tsx +++ b/example/src/WixApp.tsx @@ -47,6 +47,8 @@ const HomeScreen = props => { const [testFlagValue, setTestFlagValue] = useState(false); useEffect(() => { (async () => { + await DatadogFlags.enable(); + const flagsClient = DatadogFlags.getClient(); await flagsClient.setEvaluationContext({ targetingKey: 'test-user-1', @@ -55,7 +57,6 @@ const HomeScreen = props => { }, }); const flag = await flagsClient.getBooleanDetails('rn-sdk-test-boolean-flag', false); // https://app.datadoghq.com/feature-flags/046d0e70-626d-41e1-8314-3f009fb79b7a?environmentId=d114cd9a-79ed-4c56-bcf3-bcac9293653b - console.log({flag}) setTestFlagValue(flag.value); })(); }, []); diff --git a/example/src/ddUtils.tsx b/example/src/ddUtils.tsx index d10144127..5f3c43d3f 100644 --- a/example/src/ddUtils.tsx +++ b/example/src/ddUtils.tsx @@ -6,7 +6,6 @@ import { SdkVerbosity, TrackingConsent, DatadogFlags, - type DatadogFlagsConfiguration, } from '@datadog/mobile-react-native'; import {APPLICATION_ID, CLIENT_TOKEN, ENVIRONMENT} from './ddCredentials'; @@ -27,11 +26,6 @@ export function getDatadogConfig(trackingConsent: TrackingConsent) { config.serviceName = "com.datadoghq.reactnative.sample" config.verbosity = SdkVerbosity.DEBUG; - const flagsConfiguration: DatadogFlagsConfiguration = { - enabled: true, - } - config.flagsConfiguration = flagsConfiguration - return config } @@ -58,16 +52,11 @@ export function initializeDatadog(trackingConsent: TrackingConsent) { config.serviceName = "com.datadoghq.reactnative.sample" config.verbosity = SdkVerbosity.DEBUG; - const flagsConfiguration: DatadogFlagsConfiguration = { - enabled: true, - } - config.flagsConfiguration = flagsConfiguration - DdSdkReactNative.initialize(config).then(() => { DdLogs.info('The RN Sdk was properly initialized') DdSdkReactNative.setUserInfo({id: "1337", name: "Xavier", email: "xg@example.com", extraInfo: { type: "premium" } }) DdSdkReactNative.addAttributes({campaign: "ad-network"}) }); - DatadogFlags.enable(flagsConfiguration) + DatadogFlags.enable() } diff --git a/example/src/screens/MainScreen.tsx b/example/src/screens/MainScreen.tsx index 5ac7cddb8..b66caa2dc 100644 --- a/example/src/screens/MainScreen.tsx +++ b/example/src/screens/MainScreen.tsx @@ -110,6 +110,8 @@ export default class MainScreen extends Component { fetchBooleanFlag() { (async () => { + await DatadogFlags.enable(); + const flagsClient = DatadogFlags.getClient(); await flagsClient.setEvaluationContext({ targetingKey: 'test-user-1', diff --git a/packages/core/ios/Sources/DdSdkNativeInitialization.swift b/packages/core/ios/Sources/DdSdkNativeInitialization.swift index a05c7b0a0..bf39f5e97 100644 --- a/packages/core/ios/Sources/DdSdkNativeInitialization.swift +++ b/packages/core/ios/Sources/DdSdkNativeInitialization.swift @@ -6,7 +6,6 @@ import Foundation import DatadogCore -import DatadogFlags import DatadogRUM import DatadogLogs import DatadogTrace @@ -88,10 +87,6 @@ public class DdSdkNativeInitialization: NSObject { let traceConfig = buildTraceConfiguration(configuration: sdkConfiguration) Trace.enable(with: traceConfig) - if let configurationForFlags = sdkConfiguration.configurationForFlags { - Flags.enable(with: configurationForFlags) - } - if sdkConfiguration.nativeCrashReportEnabled ?? false { CrashReporting.enable() } diff --git a/packages/core/src/DdSdkReactNative.tsx b/packages/core/src/DdSdkReactNative.tsx index 9865a0f37..b1ea4f4c1 100644 --- a/packages/core/src/DdSdkReactNative.tsx +++ b/packages/core/src/DdSdkReactNative.tsx @@ -24,7 +24,6 @@ import { import { InternalLog } from './InternalLog'; import { SdkVerbosity } from './SdkVerbosity'; import type { TrackingConsent } from './TrackingConsent'; -import { DatadogFlags } from './flags/DatadogFlags'; import { DdLogs } from './logs/DdLogs'; import { DdRum } from './rum/DdRum'; import { DdRumErrorTracking } from './rum/instrumentation/DdRumErrorTracking'; @@ -99,13 +98,6 @@ export class DdSdkReactNative { DdSdkReactNative.buildConfiguration(configuration, params) ); - if ( - configuration.flagsConfiguration && - configuration.flagsConfiguration.enabled !== false - ) { - await DatadogFlags.enable(configuration.flagsConfiguration); - } - InternalLog.log('Datadog SDK was initialized', SdkVerbosity.INFO); GlobalState.instance.isInitialized = true; BufferSingleton.onInitialization(); @@ -406,8 +398,7 @@ export class DdSdkReactNative { configuration.trackWatchdogTerminations, configuration.batchProcessingLevel, configuration.initialResourceThreshold, - configuration.trackMemoryWarnings, - configuration.flagsConfiguration + configuration.trackMemoryWarnings ); }; diff --git a/packages/core/src/DdSdkReactNativeConfiguration.tsx b/packages/core/src/DdSdkReactNativeConfiguration.tsx index 94db29814..4ec1ef883 100644 --- a/packages/core/src/DdSdkReactNativeConfiguration.tsx +++ b/packages/core/src/DdSdkReactNativeConfiguration.tsx @@ -7,7 +7,6 @@ import type { ProxyConfiguration } from './ProxyConfiguration'; import type { SdkVerbosity } from './SdkVerbosity'; import { TrackingConsent } from './TrackingConsent'; -import type { DatadogFlagsConfiguration } from './flags/types'; import type { ActionEventMapper } from './rum/eventMappers/actionEventMapper'; import type { ErrorEventMapper } from './rum/eventMappers/errorEventMapper'; import type { ResourceEventMapper } from './rum/eventMappers/resourceEventMapper'; @@ -370,8 +369,6 @@ export class DdSdkReactNativeConfiguration { public customEndpoints: CustomEndpoints = DEFAULTS.getCustomEndpoints(); - public flagsConfiguration?: DatadogFlagsConfiguration; - constructor( readonly clientToken: string, readonly env: string, @@ -417,7 +414,6 @@ export type AutoInstrumentationParameters = { readonly actionEventMapper: ActionEventMapper | null; readonly useAccessibilityLabel: boolean; readonly actionNameAttribute?: string; - readonly flagsConfiguration?: DatadogFlagsConfiguration; }; /** @@ -485,7 +481,6 @@ export type PartialInitializationConfiguration = { readonly batchProcessingLevel?: BatchProcessingLevel; readonly initialResourceThreshold?: number; readonly trackMemoryWarnings?: boolean; - readonly flagsConfiguration?: DatadogFlagsConfiguration; }; const setConfigurationAttribute = < diff --git a/packages/core/src/types.tsx b/packages/core/src/types.tsx index 9e5c7ae3f..5c7d64cec 100644 --- a/packages/core/src/types.tsx +++ b/packages/core/src/types.tsx @@ -5,7 +5,6 @@ */ import type { BatchProcessingLevel } from './DdSdkReactNativeConfiguration'; -import type { DatadogFlagsConfiguration } from './flags/types'; declare global { // eslint-disable-next-line no-var, vars-on-top @@ -71,8 +70,7 @@ export class DdSdkConfiguration { readonly trackWatchdogTerminations: boolean | undefined, readonly batchProcessingLevel: BatchProcessingLevel, // eslint-disable-next-line no-empty-function readonly initialResourceThreshold: number | undefined, - readonly trackMemoryWarnings: boolean, - readonly configurationForFlags: DatadogFlagsConfiguration | undefined + readonly trackMemoryWarnings: boolean ) {} } From eb788adc49fa165641ba2297a7f99a96bf9fa950 Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Mon, 1 Dec 2025 14:23:12 +0200 Subject: [PATCH 27/64] Remove stale tests --- .../src/__tests__/DdSdkReactNative.test.tsx | 106 ------------------ 1 file changed, 106 deletions(-) diff --git a/packages/core/src/__tests__/DdSdkReactNative.test.tsx b/packages/core/src/__tests__/DdSdkReactNative.test.tsx index 6ce8a05d6..810a5bd9d 100644 --- a/packages/core/src/__tests__/DdSdkReactNative.test.tsx +++ b/packages/core/src/__tests__/DdSdkReactNative.test.tsx @@ -84,9 +84,6 @@ beforeEach(async () => { (DdRumErrorTracking.startTracking as jest.MockedFunction< typeof DdRumErrorTracking.startTracking >).mockClear(); - (DatadogFlags.enable as jest.MockedFunction< - typeof DatadogFlags.enable - >).mockClear(); DdLogs.unregisterLogEventMapper(); UserInfoSingleton.reset(); @@ -519,109 +516,6 @@ describe('DdSdkReactNative', () => { }) ); }); - - it('does not enable DatadogFlags when flagsConfiguration is not provided', async () => { - // GIVEN - const fakeAppId = '1'; - const fakeClientToken = '2'; - const fakeEnvName = 'env'; - const configuration = new DdSdkReactNativeConfiguration( - fakeClientToken, - fakeEnvName, - fakeAppId - ); - - NativeModules.DdSdk.initialize.mockResolvedValue(null); - - // WHEN - await DdSdkReactNative.initialize(configuration); - - // THEN - expect(DatadogFlags.enable).not.toHaveBeenCalled(); - }); - - it('enables DatadogFlags when flagsConfiguration is provided', async () => { - // GIVEN - const fakeAppId = '1'; - const fakeClientToken = '2'; - const fakeEnvName = 'env'; - const configuration = new DdSdkReactNativeConfiguration( - fakeClientToken, - fakeEnvName, - fakeAppId - ); - configuration.flagsConfiguration = { - enabled: true - }; - - NativeModules.DdSdk.initialize.mockResolvedValue(null); - - // WHEN - await DdSdkReactNative.initialize(configuration); - - // THEN - expect(DatadogFlags.enable).toHaveBeenCalledTimes(1); - expect(DatadogFlags.enable).toHaveBeenCalledWith({ - enabled: true - }); - }); - - it('enables DatadogFlags with custom configuration when provided', async () => { - // GIVEN - const fakeAppId = '1'; - const fakeClientToken = '2'; - const fakeEnvName = 'env'; - const customFlagsEndpoint = 'https://flags.example.com'; - const customFlagsHeaders = { - Authorization: 'Bearer token123' - }; - const configuration = new DdSdkReactNativeConfiguration( - fakeClientToken, - fakeEnvName, - fakeAppId - ); - configuration.flagsConfiguration = { - enabled: true, - customFlagsEndpoint, - customFlagsHeaders - }; - - NativeModules.DdSdk.initialize.mockResolvedValue(null); - - // WHEN - await DdSdkReactNative.initialize(configuration); - - // THEN - expect(DatadogFlags.enable).toHaveBeenCalledTimes(1); - expect(DatadogFlags.enable).toHaveBeenCalledWith({ - enabled: true, - customFlagsEndpoint, - customFlagsHeaders - }); - }); - - it('does not call DatadogFlags.enable when flagsConfiguration.enabled is false', async () => { - // GIVEN - const fakeAppId = '1'; - const fakeClientToken = '2'; - const fakeEnvName = 'env'; - const configuration = new DdSdkReactNativeConfiguration( - fakeClientToken, - fakeEnvName, - fakeAppId - ); - configuration.flagsConfiguration = { - enabled: false - }; - - NativeModules.DdSdk.initialize.mockResolvedValue(null); - - // WHEN - await DdSdkReactNative.initialize(configuration); - - // THEN - expect(DatadogFlags.enable).not.toHaveBeenCalled(); - }); }); describe('feature enablement', () => { From 7ec774ed49a7d059279940ee026e3548f96a49bc Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Mon, 1 Dec 2025 15:55:09 +0200 Subject: [PATCH 28/64] Finish removal of unnecessary Flags code in global SDK init code --- .../core/ios/Sources/DdSdkConfiguration.swift | 5 --- .../ios/Sources/RNDdSdkConfiguration.swift | 8 ++--- packages/core/ios/Tests/DdSdkTests.swift | 36 ++----------------- .../src/__tests__/DdSdkReactNative.test.tsx | 1 - .../__tests__/initialization.test.tsx | 1 - 5 files changed, 4 insertions(+), 47 deletions(-) diff --git a/packages/core/ios/Sources/DdSdkConfiguration.swift b/packages/core/ios/Sources/DdSdkConfiguration.swift index 8f852834e..ffbc2199d 100644 --- a/packages/core/ios/Sources/DdSdkConfiguration.swift +++ b/packages/core/ios/Sources/DdSdkConfiguration.swift @@ -6,7 +6,6 @@ import Foundation import DatadogCore -import DatadogFlags import DatadogInternal import DatadogRUM @@ -43,7 +42,6 @@ import DatadogRUM - trackWatchdogTerminations: Whether the SDK should track application termination by the watchdog - batchProcessingLevel: Maximum number of batches processed sequentially without a delay - initialResourceThreshold: The amount of time after a view starts where a Resource should be considered when calculating Time to Network-Settled (TNS) - - configurationForFlags: Configuration for the feature flags feature. */ @objc(DdSdkConfiguration) public class DdSdkConfiguration: NSObject { @@ -79,7 +77,6 @@ public class DdSdkConfiguration: NSObject { public var batchProcessingLevel: Datadog.Configuration.BatchProcessingLevel public var initialResourceThreshold: Double? = nil public var trackMemoryWarnings: Bool - public var configurationForFlags: Flags.Configuration? = nil public init( clientToken: String, @@ -114,7 +111,6 @@ public class DdSdkConfiguration: NSObject { batchProcessingLevel: Datadog.Configuration.BatchProcessingLevel, initialResourceThreshold: Double?, trackMemoryWarnings: Bool = true, - configurationForFlags: Flags.Configuration? ) { self.clientToken = clientToken self.env = env @@ -148,7 +144,6 @@ public class DdSdkConfiguration: NSObject { self.batchProcessingLevel = batchProcessingLevel self.initialResourceThreshold = initialResourceThreshold self.trackMemoryWarnings = trackMemoryWarnings - self.configurationForFlags = configurationForFlags } } diff --git a/packages/core/ios/Sources/RNDdSdkConfiguration.swift b/packages/core/ios/Sources/RNDdSdkConfiguration.swift index 464161c3f..f9b05aed0 100644 --- a/packages/core/ios/Sources/RNDdSdkConfiguration.swift +++ b/packages/core/ios/Sources/RNDdSdkConfiguration.swift @@ -45,7 +45,6 @@ extension NSDictionary { let batchProcessingLevel = object(forKey: "batchProcessingLevel") as? NSString let initialResourceThreshold = object(forKey: "initialResourceThreshold") as? Double let trackMemoryWarnings = object(forKey: "trackMemoryWarnings") as? Bool - let configurationForFlags = object(forKey: "configurationForFlags") as? NSDictionary return DdSdkConfiguration( clientToken: (clientToken != nil) ? clientToken! : String(), @@ -79,8 +78,7 @@ extension NSDictionary { trackWatchdogTerminations: trackWatchdogTerminations ?? DefaultConfiguration.trackWatchdogTerminations, batchProcessingLevel: batchProcessingLevel.asBatchProcessingLevel(), initialResourceThreshold: initialResourceThreshold, - trackMemoryWarnings: trackMemoryWarnings ?? DefaultConfiguration.trackMemoryWarnings, - configurationForFlags: configurationForFlags?.asConfigurationForFlags() + trackMemoryWarnings: trackMemoryWarnings ?? DefaultConfiguration.trackMemoryWarnings ) } @@ -284,7 +282,6 @@ extension Dictionary where Key == String, Value == AnyObject { let batchProcessingLevel = configuration["batchProcessingLevel"] as? NSString let initialResourceThreshold = configuration["initialResourceThreshold"] as? Double let trackMemoryWarnings = configuration["trackMemoryWarnings"] as? Bool - let configurationForFlags = configuration["configurationForFlags"] as? NSDictionary return DdSdkConfiguration( clientToken: clientToken ?? String(), @@ -321,8 +318,7 @@ extension Dictionary where Key == String, Value == AnyObject { trackWatchdogTerminations: trackWatchdogTerminations ?? DefaultConfiguration.trackWatchdogTerminations, batchProcessingLevel: batchProcessingLevel.asBatchProcessingLevel(), initialResourceThreshold: initialResourceThreshold, - trackMemoryWarnings: trackMemoryWarnings ?? DefaultConfiguration.trackMemoryWarnings, - configurationForFlags: configurationForFlags?.asConfigurationForFlags() + trackMemoryWarnings: trackMemoryWarnings ?? DefaultConfiguration.trackMemoryWarnings ) } } diff --git a/packages/core/ios/Tests/DdSdkTests.swift b/packages/core/ios/Tests/DdSdkTests.swift index e3c4334b3..4a5d13f2e 100644 --- a/packages/core/ios/Tests/DdSdkTests.swift +++ b/packages/core/ios/Tests/DdSdkTests.swift @@ -8,7 +8,6 @@ import XCTest @testable import DatadogCore @testable import DatadogCrashReporting -@testable import DatadogFlags @testable import DatadogInternal @testable import DatadogLogs @testable import DatadogRUM @@ -310,35 +309,6 @@ class DdSdkTests: XCTestCase { XCTAssertNotNil(core.features[LogsFeature.name]) XCTAssertNotNil(core.features[TraceFeature.name]) } - - func testFlagsFeatureDisabledByDefault() { - let core = MockDatadogCore() - CoreRegistry.register(default: core) - defer { CoreRegistry.unregisterDefault() } - - let configuration: DdSdkConfiguration = .mockAny(configurationForFlags: nil) - - DdSdkNativeInitialization().enableFeatures( - sdkConfiguration: configuration - ) - - // Flagging SDK is disabled by default if no configuration is provided. - XCTAssertNil(core.features[FlagsFeature.name]) - } - - func testEnableFeatureFlags() { - let core = MockDatadogCore() - CoreRegistry.register(default: core) - defer { CoreRegistry.unregisterDefault() } - - let configuration: DdSdkConfiguration = .mockAny(configurationForFlags: ["enabled":true]) - - DdSdkNativeInitialization().enableFeatures( - sdkConfiguration: configuration - ) - - XCTAssertNotNil(core.features[FlagsFeature.name]) - } func testBuildConfigurationDefaultEndpoint() { let configuration: DdSdkConfiguration = .mockAny() @@ -1664,8 +1634,7 @@ extension DdSdkConfiguration { appHangThreshold: Double? = nil, trackWatchdogTerminations: Bool = false, batchProcessingLevel: NSString? = "MEDIUM", - initialResourceThreshold: Double? = nil, - configurationForFlags: NSDictionary? = nil + initialResourceThreshold: Double? = nil ) -> DdSdkConfiguration { DdSdkConfiguration( clientToken: clientToken as String, @@ -1698,8 +1667,7 @@ extension DdSdkConfiguration { appHangThreshold: appHangThreshold, trackWatchdogTerminations: trackWatchdogTerminations, batchProcessingLevel: batchProcessingLevel.asBatchProcessingLevel(), - initialResourceThreshold: initialResourceThreshold, - configurationForFlags: configurationForFlags?.asConfigurationForFlags() + initialResourceThreshold: initialResourceThreshold ) } } diff --git a/packages/core/src/__tests__/DdSdkReactNative.test.tsx b/packages/core/src/__tests__/DdSdkReactNative.test.tsx index 810a5bd9d..74c2637b6 100644 --- a/packages/core/src/__tests__/DdSdkReactNative.test.tsx +++ b/packages/core/src/__tests__/DdSdkReactNative.test.tsx @@ -12,7 +12,6 @@ import { DdSdkReactNative } from '../DdSdkReactNative'; import { ProxyConfiguration, ProxyType } from '../ProxyConfiguration'; import { SdkVerbosity } from '../SdkVerbosity'; import { TrackingConsent } from '../TrackingConsent'; -import { DatadogFlags } from '../flags/DatadogFlags'; import { DdLogs } from '../logs/DdLogs'; import { DdRum } from '../rum/DdRum'; import { DdRumErrorTracking } from '../rum/instrumentation/DdRumErrorTracking'; diff --git a/packages/core/src/sdk/DatadogProvider/__tests__/initialization.test.tsx b/packages/core/src/sdk/DatadogProvider/__tests__/initialization.test.tsx index e896a9bc2..8ace4b731 100644 --- a/packages/core/src/sdk/DatadogProvider/__tests__/initialization.test.tsx +++ b/packages/core/src/sdk/DatadogProvider/__tests__/initialization.test.tsx @@ -75,7 +75,6 @@ describe('DatadogProvider', () => { "bundleLogsWithRum": true, "bundleLogsWithTraces": true, "clientToken": "fakeToken", - "configurationForFlags": undefined, "configurationForTelemetry": { "initializationType": "SYNC", "reactNativeVersion": "0.76.9", From 3d48d77a851f83a4bcd885fcf571f29e4135db18 Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Mon, 1 Dec 2025 16:39:38 +0200 Subject: [PATCH 29/64] Remove unnecessary comma --- packages/core/ios/Sources/DdSdkConfiguration.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/ios/Sources/DdSdkConfiguration.swift b/packages/core/ios/Sources/DdSdkConfiguration.swift index ffbc2199d..0b1e6222f 100644 --- a/packages/core/ios/Sources/DdSdkConfiguration.swift +++ b/packages/core/ios/Sources/DdSdkConfiguration.swift @@ -110,7 +110,7 @@ public class DdSdkConfiguration: NSObject { trackWatchdogTerminations: Bool, batchProcessingLevel: Datadog.Configuration.BatchProcessingLevel, initialResourceThreshold: Double?, - trackMemoryWarnings: Bool = true, + trackMemoryWarnings: Bool = true ) { self.clientToken = clientToken self.env = env From 14799f235c3b6ca11cc0d16999785df8f2a456b2 Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Mon, 1 Dec 2025 18:44:12 +0200 Subject: [PATCH 30/64] Remove leftovers for global init --- example-new-architecture/App.tsx | 3 - .../DdSdkReactNativeConfiguration.test.ts | 222 ------------------ 2 files changed, 225 deletions(-) diff --git a/example-new-architecture/App.tsx b/example-new-architecture/App.tsx index f0f4434dd..5e921c234 100644 --- a/example-new-architecture/App.tsx +++ b/example-new-architecture/App.tsx @@ -46,9 +46,6 @@ import {APPLICATION_ID, CLIENT_TOKEN, ENVIRONMENT} from './ddCredentials'; config.telemetrySampleRate = 100; config.uploadFrequency = UploadFrequency.FREQUENT; config.batchSize = BatchSize.SMALL; - config.flagsConfiguration = { - enabled: true, - }; await DdSdkReactNative.initialize(config); await DdRum.startView('main', 'Main'); setTimeout(async () => { diff --git a/packages/core/src/__tests__/DdSdkReactNativeConfiguration.test.ts b/packages/core/src/__tests__/DdSdkReactNativeConfiguration.test.ts index 8193621c7..60a9d13ff 100644 --- a/packages/core/src/__tests__/DdSdkReactNativeConfiguration.test.ts +++ b/packages/core/src/__tests__/DdSdkReactNativeConfiguration.test.ts @@ -261,227 +261,5 @@ describe('DdSdkReactNativeConfiguration', () => { } `); }); - - it('builds the SDK configuration with flags configuration enabled', () => { - expect( - buildConfigurationFromPartialConfiguration( - { - trackErrors: false, - trackInteractions: false, - trackResources: false - }, - { - applicationId: 'fake-app-id', - clientToken: 'fake-client-token', - env: 'fake-env', - flagsConfiguration: { - enabled: true, - customFlagsEndpoint: 'https://flags.example.com', - customFlagsHeaders: { - Authorization: 'Bearer token123', - 'X-Custom-Header': 'custom-value' - } - } - } - ) - ).toMatchInlineSnapshot(` - DdSdkReactNativeConfiguration { - "actionEventMapper": null, - "additionalConfiguration": {}, - "applicationId": "fake-app-id", - "batchProcessingLevel": "MEDIUM", - "batchSize": "MEDIUM", - "bundleLogsWithRum": true, - "bundleLogsWithTraces": true, - "clientToken": "fake-client-token", - "customEndpoints": {}, - "env": "fake-env", - "errorEventMapper": null, - "firstPartyHosts": [], - "flagsConfiguration": { - "customFlagsEndpoint": "https://flags.example.com", - "customFlagsHeaders": { - "Authorization": "Bearer token123", - "X-Custom-Header": "custom-value", - }, - "enabled": true, - }, - "logEventMapper": null, - "longTaskThresholdMs": 0, - "nativeCrashReportEnabled": false, - "nativeInteractionTracking": false, - "nativeLongTaskThresholdMs": 200, - "nativeViewTracking": false, - "proxyConfig": undefined, - "resourceEventMapper": null, - "resourceTracingSamplingRate": 100, - "serviceName": undefined, - "sessionSamplingRate": 100, - "site": "US1", - "telemetrySampleRate": 20, - "trackBackgroundEvents": false, - "trackErrors": false, - "trackFrustrations": true, - "trackInteractions": false, - "trackMemoryWarnings": true, - "trackResources": false, - "trackWatchdogTerminations": false, - "trackingConsent": "granted", - "uploadFrequency": "AVERAGE", - "useAccessibilityLabel": true, - "verbosity": undefined, - "vitalsUpdateFrequency": "AVERAGE", - } - `); - }); - - it('builds the SDK configuration with flags configuration disabled', () => { - expect( - buildConfigurationFromPartialConfiguration( - { - trackErrors: false, - trackInteractions: false, - trackResources: false - }, - { - applicationId: 'fake-app-id', - clientToken: 'fake-client-token', - env: 'fake-env', - flagsConfiguration: { - enabled: false - } - } - ) - ).toMatchInlineSnapshot(` - DdSdkReactNativeConfiguration { - "actionEventMapper": null, - "additionalConfiguration": {}, - "applicationId": "fake-app-id", - "batchProcessingLevel": "MEDIUM", - "batchSize": "MEDIUM", - "bundleLogsWithRum": true, - "bundleLogsWithTraces": true, - "clientToken": "fake-client-token", - "customEndpoints": {}, - "env": "fake-env", - "errorEventMapper": null, - "firstPartyHosts": [], - "flagsConfiguration": { - "enabled": false, - }, - "logEventMapper": null, - "longTaskThresholdMs": 0, - "nativeCrashReportEnabled": false, - "nativeInteractionTracking": false, - "nativeLongTaskThresholdMs": 200, - "nativeViewTracking": false, - "proxyConfig": undefined, - "resourceEventMapper": null, - "resourceTracingSamplingRate": 100, - "serviceName": undefined, - "sessionSamplingRate": 100, - "site": "US1", - "telemetrySampleRate": 20, - "trackBackgroundEvents": false, - "trackErrors": false, - "trackFrustrations": true, - "trackInteractions": false, - "trackMemoryWarnings": true, - "trackResources": false, - "trackWatchdogTerminations": false, - "trackingConsent": "granted", - "uploadFrequency": "AVERAGE", - "useAccessibilityLabel": true, - "verbosity": undefined, - "vitalsUpdateFrequency": "AVERAGE", - } - `); - }); - - it('builds the SDK configuration with full flags configuration', () => { - expect( - buildConfigurationFromPartialConfiguration( - { - trackErrors: true, - trackInteractions: true, - trackResources: true, - firstPartyHosts: ['api.com'], - resourceTracingSamplingRate: 80 - }, - { - applicationId: 'fake-app-id', - clientToken: 'fake-client-token', - env: 'fake-env', - sessionSamplingRate: 80, - site: 'EU', - verbosity: SdkVerbosity.DEBUG, - serviceName: 'com.test.app', - version: '1.4.5', - flagsConfiguration: { - enabled: true, - customFlagsEndpoint: 'https://flags.example.com', - customFlagsHeaders: { - Authorization: 'Bearer token123' - }, - customExposureEndpoint: - 'https://exposure.example.com', - trackExposures: true - } - } - ) - ).toMatchInlineSnapshot(` - DdSdkReactNativeConfiguration { - "actionEventMapper": null, - "additionalConfiguration": {}, - "applicationId": "fake-app-id", - "batchProcessingLevel": "MEDIUM", - "batchSize": "MEDIUM", - "bundleLogsWithRum": true, - "bundleLogsWithTraces": true, - "clientToken": "fake-client-token", - "customEndpoints": {}, - "env": "fake-env", - "errorEventMapper": null, - "firstPartyHosts": [ - "api.com", - ], - "flagsConfiguration": { - "customExposureEndpoint": "https://exposure.example.com", - "customFlagsEndpoint": "https://flags.example.com", - "customFlagsHeaders": { - "Authorization": "Bearer token123", - }, - "enabled": true, - "trackExposures": true, - }, - "logEventMapper": null, - "longTaskThresholdMs": 0, - "nativeCrashReportEnabled": false, - "nativeInteractionTracking": false, - "nativeLongTaskThresholdMs": 200, - "nativeViewTracking": false, - "proxyConfig": undefined, - "resourceEventMapper": null, - "resourceTracingSamplingRate": 80, - "serviceName": "com.test.app", - "sessionSamplingRate": 80, - "site": "EU", - "telemetrySampleRate": 20, - "trackBackgroundEvents": false, - "trackErrors": true, - "trackFrustrations": true, - "trackInteractions": true, - "trackMemoryWarnings": true, - "trackResources": true, - "trackWatchdogTerminations": false, - "trackingConsent": "granted", - "uploadFrequency": "AVERAGE", - "useAccessibilityLabel": true, - "verbosity": "debug", - "version": "1.4.5", - "vitalsUpdateFrequency": "AVERAGE", - } - `); - }); }); }); From 040c681ac1fde41fe17aec15ac63a6e7045ee60b Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Mon, 1 Dec 2025 19:43:48 +0200 Subject: [PATCH 31/64] Bump datadog SDK package versions, add flags SDK --- packages/core/android/build.gradle | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/core/android/build.gradle b/packages/core/android/build.gradle index 1344b2531..5da0e969e 100644 --- a/packages/core/android/build.gradle +++ b/packages/core/android/build.gradle @@ -195,10 +195,11 @@ dependencies { } implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" compileOnly "com.squareup.okhttp3:okhttp:3.12.13" - implementation "com.datadoghq:dd-sdk-android-rum:3.2.0" - implementation "com.datadoghq:dd-sdk-android-logs:3.2.0" - implementation "com.datadoghq:dd-sdk-android-trace:3.2.0" - implementation "com.datadoghq:dd-sdk-android-webview:3.2.0" + implementation "com.datadoghq:dd-sdk-android-rum:3.3.0" + implementation "com.datadoghq:dd-sdk-android-logs:3.3.0" + implementation "com.datadoghq:dd-sdk-android-trace:3.3.0" + implementation "com.datadoghq:dd-sdk-android-webview:3.3.0" + implementation "com.datadoghq:dd-sdk-android-flags:3.3.0" implementation "com.google.code.gson:gson:2.10.0" testImplementation "org.junit.platform:junit-platform-launcher:1.6.2" testImplementation "org.junit.jupiter:junit-jupiter-api:5.6.2" From c45b654a33a56509780ae644f25261f2360f44da Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Tue, 2 Dec 2025 16:29:22 +0200 Subject: [PATCH 32/64] Initial Android implementation --- .../reactnative/DdFlagsImplementation.kt | 251 ++++++++++++++++++ .../reactnative/DdSdkReactNativePackage.kt | 4 +- .../kotlin/com/datadog/reactnative/DdFlags.kt | 109 ++++++++ .../kotlin/com/datadog/reactnative/DdFlags.kt | 100 +++++++ packages/core/src/flags/types.ts | 7 + 5 files changed, 470 insertions(+), 1 deletion(-) create mode 100644 packages/core/android/src/main/kotlin/com/datadog/reactnative/DdFlagsImplementation.kt create mode 100644 packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdFlags.kt create mode 100644 packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdFlags.kt diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdFlagsImplementation.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdFlagsImplementation.kt new file mode 100644 index 000000000..ec301dfbd --- /dev/null +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdFlagsImplementation.kt @@ -0,0 +1,251 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.reactnative + +import com.datadog.android.Datadog +import com.datadog.android.api.InternalLogger +import com.datadog.android.api.SdkCore +import com.datadog.android.flags.Flags +import com.datadog.android.flags.FlagsClient +import com.datadog.android.flags.FlagsConfiguration +import com.datadog.android.flags.model.EvaluationContext +import com.datadog.android.flags.model.ErrorCode +import com.datadog.android.flags.model.ResolutionDetails +import com.facebook.react.bridge.Promise +import com.facebook.react.bridge.ReadableMap +import com.facebook.react.bridge.WritableMap +import com.facebook.react.bridge.WritableNativeMap +import org.json.JSONObject + +class DdFlagsImplementation(private val sdkCore: SdkCore = Datadog.getInstance()) { + + private val clients: MutableMap = mutableMapOf() + + /** + * Enable the Flags feature with the provided configuration. + * @param configuration The configuration for Flags. + */ + fun enable(configuration: ReadableMap, promise: Promise) { + val flagsConfig = configuration.asFlagsConfiguration() + if (flagsConfig != null) { + Flags.enable(flagsConfig, sdkCore) + } else { + InternalLogger.UNBOUND.log( + InternalLogger.Level.ERROR, + InternalLogger.Target.USER, + { "Invalid configuration provided for Flags. Feature initialization skipped." } + ) + } + promise.resolve(null) + } + + /** + * Retrieve or create a FlagsClient instance. + * + * Caches clients by name to avoid repeated Builder().build() calls. + * On hot reload, the cache is cleared and clients are recreated - this is safe + * because gracefulModeEnabled=true prevents crashes on duplicate creation. + */ + private fun getClient(name: String): FlagsClient { + return clients.getOrPut(name) { + FlagsClient.Builder(name, sdkCore).build() + } + } + + private fun parseAttributes(attributes: ReadableMap): Map { + val result = mutableMapOf() + val iterator = attributes.entryIterator + while (iterator.hasNext()) { + val entry = iterator.next() + // Convert all values to strings as required by Android SDK + result[entry.key] = entry.value?.toString() ?: "" + } + return result + } + + /** + * Set the evaluation context for a specific client. + * @param clientName The name of the client. + * @param targetingKey The targeting key. + * @param attributes The attributes for the evaluation context (will be converted to strings). + */ + fun setEvaluationContext( + clientName: String, + targetingKey: String, + attributes: ReadableMap, + promise: Promise + ) { + val client = getClient(clientName) + val parsedAttributes = parseAttributes(attributes) + val evaluationContext = EvaluationContext(targetingKey, parsedAttributes) + + client.setEvaluationContext(evaluationContext) + promise.resolve(null) + } + + /** + * Get details for a boolean flag. + * @param clientName The name of the client. + * @param key The flag key. + * @param defaultValue The default value. + */ + fun getBooleanDetails( + clientName: String, + key: String, + defaultValue: Boolean, + promise: Promise + ) { + val client = getClient(clientName) + val details = client.resolve(key, defaultValue) + promise.resolve(details.toReactNativeMap(key)) + } + + /** + * Get details for a string flag. + * @param clientName The name of the client. + * @param key The flag key. + * @param defaultValue The default value. + */ + fun getStringDetails(clientName: String, key: String, defaultValue: String, promise: Promise) { + val client = getClient(clientName) + val details = client.resolve(key, defaultValue) + promise.resolve(details.toReactNativeMap(key)) + } + + /** + * Get details for a number flag. Includes Number and Integer flags. + * @param clientName The name of the client. + * @param key The flag key. + * @param defaultValue The default value. + */ + fun getNumberDetails(clientName: String, key: String, defaultValue: Double, promise: Promise) { + val client = getClient(clientName) + + // Try as Double first. + val doubleDetails = client.resolve(key, defaultValue) + + // If type mismatch and value is an integer, try as Int. + if (doubleDetails.errorCode == ErrorCode.TYPE_MISMATCH) { + val safeInt = defaultValue.toInt() + val intDetails = client.resolve(key, safeInt) + + if (intDetails.errorCode == null) { + promise.resolve(intDetails.toReactNativeMap(key)) + return + } + } + + promise.resolve(doubleDetails.toReactNativeMap(key)) + } + + /** + * Get details for an object flag. + * @param clientName The name of the client. + * @param key The flag key. + * @param defaultValue The default value. + */ + fun getObjectDetails( + clientName: String, + key: String, + defaultValue: ReadableMap, + promise: Promise + ) { + val client = getClient(clientName) + val jsonDefaultValue = defaultValue.toJSONObject() + val details = client.resolve(key, jsonDefaultValue) + promise.resolve(details.toReactNativeMap(key)) + } + + internal companion object { + internal const val NAME = "DdFlags" + } +} + +/** Convert ResolutionDetails to React Native map format expected by the JS layer. */ +private fun ResolutionDetails.toReactNativeMap(key: String): WritableMap { + val map = WritableNativeMap() + map.putString("key", key) + + when (val v = value) { + is Boolean -> map.putBoolean("value", v) + is String -> map.putString("value", v) + is Int -> map.putDouble("value", v.toDouble()) // Convert to double for RN. + is Double -> map.putDouble("value", v) + is JSONObject -> map.putMap("value", v.toWritableMap()) + else -> map.putNull("value") + } + + variant?.let { map.putString("variant", it) } ?: map.putNull("variant") + reason?.let { map.putString("reason", it.name) } ?: map.putNull("reason") + errorCode?.let { map.putString("error", it.name) } ?: map.putNull("error") + + return map +} + +/** Convert ReadableMap to JSONObject for flag default values. */ +private fun ReadableMap.toJSONObject(): JSONObject { + val json = JSONObject() + val iterator = entryIterator + while (iterator.hasNext()) { + val entry = iterator.next() + json.put(entry.key, entry.value) + } + return json +} + +/** Convert JSONObject to WritableMap for React Native. */ +private fun JSONObject.toWritableMap(): WritableMap { + val map = WritableNativeMap() + val keys = keys() + while (keys.hasNext()) { + val key = keys.next() + when (val value = get(key)) { + is Boolean -> map.putBoolean(key, value) + is Int -> map.putInt(key, value) + is Double -> map.putDouble(key, value) + is String -> map.putString(key, value) + is JSONObject -> map.putMap(key, value.toWritableMap()) + JSONObject.NULL -> map.putNull(key) + else -> map.putNull(key) + } + } + return map +} + +/** Parse configuration from ReadableMap to FlagsConfiguration. */ +private fun ReadableMap.asFlagsConfiguration(): FlagsConfiguration? { + val enabled = if (hasKey("enabled")) getBoolean("enabled") else false + + if (!enabled) { + return null + } + + // Hard set `gracefulModeEnabled` to `true` because SDK misconfigurations are handled on JS side. + // This prevents crashes on hot reload when clients are recreated. + val gracefulModeEnabled = true + + val trackExposures = if (hasKey("trackExposures")) getBoolean("trackExposures") else true + val rumIntegrationEnabled = + if (hasKey("rumIntegrationEnabled")) getBoolean("rumIntegrationEnabled") else true + + return FlagsConfiguration.Builder() + .apply { + gracefulModeEnabled(gracefulModeEnabled) + trackExposures(trackExposures) + rumIntegrationEnabled(rumIntegrationEnabled) + + // The SDK automatically appends endpoint names to the custom endpoints. + // The input config expects a base URL rather than a full URL. + if (hasKey("customFlagsEndpoint")) { + getString("customFlagsEndpoint")?.let { useCustomFlagEndpoint("$it/precompute-assignments") } + } + if (hasKey("customExposureEndpoint")) { + getString("customExposureEndpoint")?.let { useCustomExposureEndpoint("$it/api/v2/exposures") } + } + } + .build() +} diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkReactNativePackage.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkReactNativePackage.kt index 3a5b022c1..98ffa83db 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkReactNativePackage.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkReactNativePackage.kt @@ -25,6 +25,7 @@ class DdSdkReactNativePackage : TurboReactPackage() { DdRumImplementation.NAME -> DdRum(reactContext, sdkWrapper) DdTraceImplementation.NAME -> DdTrace(reactContext) DdLogsImplementation.NAME -> DdLogs(reactContext, sdkWrapper) + DdFlagsImplementation.NAME -> DdFlags(reactContext) else -> null } } @@ -36,7 +37,8 @@ class DdSdkReactNativePackage : TurboReactPackage() { DdSdkImplementation.NAME, DdRumImplementation.NAME, DdTraceImplementation.NAME, - DdLogsImplementation.NAME + DdLogsImplementation.NAME, + DdFlagsImplementation.NAME ).associateWith { ReactModuleInfo( it, diff --git a/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdFlags.kt b/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdFlags.kt new file mode 100644 index 000000000..e5a3672c2 --- /dev/null +++ b/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdFlags.kt @@ -0,0 +1,109 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.reactnative + +import com.facebook.react.bridge.Promise +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.ReactMethod +import com.facebook.react.bridge.ReadableMap + +/** The entry point to use Datadog's Flags feature. */ +class DdFlags(reactContext: ReactApplicationContext) : NativeDdFlagsSpec(reactContext) { + + private val implementation = DdFlagsImplementation() + + override fun getName(): String = DdFlagsImplementation.NAME + + /** + * Enable the Flags feature with the provided configuration. + * @param configuration The configuration for Flags. + */ + @ReactMethod + override fun enable(configuration: ReadableMap, promise: Promise) { + implementation.enable(configuration, promise) + } + + /** + * Set the evaluation context for a specific client. + * @param clientName The name of the client. + * @param targetingKey The targeting key. + * @param attributes The attributes for the evaluation context. + */ + @ReactMethod + override fun setEvaluationContext( + clientName: String, + targetingKey: String, + attributes: ReadableMap, + promise: Promise + ) { + implementation.setEvaluationContext(clientName, targetingKey, attributes, promise) + } + + /** + * Get details for a boolean flag. + * @param clientName The name of the client. + * @param key The flag key. + * @param defaultValue The default value. + */ + @ReactMethod + override fun getBooleanDetails( + clientName: String, + key: String, + defaultValue: Boolean, + promise: Promise + ) { + implementation.getBooleanDetails(clientName, key, defaultValue, promise) + } + + /** + * Get details for a string flag. + * @param clientName The name of the client. + * @param key The flag key. + * @param defaultValue The default value. + */ + @ReactMethod + override fun getStringDetails( + clientName: String, + key: String, + defaultValue: String, + promise: Promise + ) { + implementation.getStringDetails(clientName, key, defaultValue, promise) + } + + /** + * Get details for a number flag. Includes Number and Integer flags. + * @param clientName The name of the client. + * @param key The flag key. + * @param defaultValue The default value. + */ + @ReactMethod + override fun getNumberDetails( + clientName: String, + key: String, + defaultValue: Double, + promise: Promise + ) { + implementation.getNumberDetails(clientName, key, defaultValue, promise) + } + + /** + * Get details for an object flag. + * @param clientName The name of the client. + * @param key The flag key. + * @param defaultValue The default value. + */ + @ReactMethod + override fun getObjectDetails( + clientName: String, + key: String, + defaultValue: ReadableMap, + promise: Promise + ) { + implementation.getObjectDetails(clientName, key, defaultValue, promise) + } +} diff --git a/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdFlags.kt b/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdFlags.kt new file mode 100644 index 000000000..8eaaae160 --- /dev/null +++ b/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdFlags.kt @@ -0,0 +1,100 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.reactnative + +import com.facebook.react.bridge.Promise +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.ReactContextBaseJavaModule +import com.facebook.react.bridge.ReactMethod +import com.facebook.react.bridge.ReadableMap + +/** The entry point to use Datadog's Flags feature. */ +class DdFlags(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) { + + private val implementation = DdFlagsImplementation() + + override fun getName(): String = DdFlagsImplementation.NAME + + /** + * Enable the Flags feature with the provided configuration. + * @param configuration The configuration for Flags. + */ + @ReactMethod + fun enable(configuration: ReadableMap, promise: Promise) { + implementation.enable(configuration, promise) + } + + /** + * Set the evaluation context for a specific client. + * @param clientName The name of the client. + * @param targetingKey The targeting key. + * @param attributes The attributes for the evaluation context. + */ + @ReactMethod + fun setEvaluationContext( + clientName: String, + targetingKey: String, + attributes: ReadableMap, + promise: Promise + ) { + implementation.setEvaluationContext(clientName, targetingKey, attributes, promise) + } + + /** + * Get details for a boolean flag. + * @param clientName The name of the client. + * @param key The flag key. + * @param defaultValue The default value. + */ + @ReactMethod + fun getBooleanDetails( + clientName: String, + key: String, + defaultValue: Boolean, + promise: Promise + ) { + implementation.getBooleanDetails(clientName, key, defaultValue, promise) + } + + /** + * Get details for a string flag. + * @param clientName The name of the client. + * @param key The flag key. + * @param defaultValue The default value. + */ + @ReactMethod + fun getStringDetails(clientName: String, key: String, defaultValue: String, promise: Promise) { + implementation.getStringDetails(clientName, key, defaultValue, promise) + } + + /** + * Get details for a number flag. Includes Number and Integer flags. + * @param clientName The name of the client. + * @param key The flag key. + * @param defaultValue The default value. + */ + @ReactMethod + fun getNumberDetails(clientName: String, key: String, defaultValue: Double, promise: Promise) { + implementation.getNumberDetails(clientName, key, defaultValue, promise) + } + + /** + * Get details for an object flag. + * @param clientName The name of the client. + * @param key The flag key. + * @param defaultValue The default value. + */ + @ReactMethod + fun getObjectDetails( + clientName: String, + key: String, + defaultValue: ReadableMap, + promise: Promise + ) { + implementation.getObjectDetails(clientName, key, defaultValue, promise) + } +} diff --git a/packages/core/src/flags/types.ts b/packages/core/src/flags/types.ts index 43ff9aa7f..1d80083f3 100644 --- a/packages/core/src/flags/types.ts +++ b/packages/core/src/flags/types.ts @@ -68,6 +68,9 @@ export type DatadogFlagsConfiguration = { /** * Custom server URL for retrieving flag assignments. * + * The provided value should only include the base URL, and the endpoint will be appended automatically. + * For example, if you provide 'https://flags.example.com', the SDK will use 'https://flags.example.com/precompute-assignments'. + * * If not set, the SDK uses the default Datadog Flags endpoint for the configured site. * * @default undefined @@ -84,6 +87,9 @@ export type DatadogFlagsConfiguration = { /** * Custom server URL for sending Flags exposure data. * + * The provided value should only include the base URL, and the endpoint will be appended automatically. + * For example, if you provide 'https://flags.example.com', the SDK will use 'https://flags.example.com/api/v2/exposures'. + * * If not set, the SDK uses the default Datadog Flags exposure endpoint. * * @default undefined @@ -156,6 +162,7 @@ export interface EvaluationContext { export type FlagEvaluationError = | 'PROVIDER_NOT_READY' | 'FLAG_NOT_FOUND' + | 'PARSE_ERROR' | 'TYPE_MISMATCH'; /** From 7094828e02c2295ad7659c2c58d4bf08c07aa379 Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Wed, 3 Dec 2025 13:32:04 +0200 Subject: [PATCH 33/64] Remove `customFlagsHeaders` from iOS wrapper configuration --- packages/core/ios/Sources/RNDdSdkConfiguration.swift | 2 -- packages/core/src/flags/types.ts | 10 ++-------- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/packages/core/ios/Sources/RNDdSdkConfiguration.swift b/packages/core/ios/Sources/RNDdSdkConfiguration.swift index f9b05aed0..6ef79d7d0 100644 --- a/packages/core/ios/Sources/RNDdSdkConfiguration.swift +++ b/packages/core/ios/Sources/RNDdSdkConfiguration.swift @@ -110,7 +110,6 @@ extension NSDictionary { // Hard set `gracefulModeEnabled` to `true` because this misconfiguration is handled on JS side. let gracefulModeEnabled = true - let customFlagsHeaders = object(forKey: "customFlagsHeaders") as? [String: String] let trackExposures = object(forKey: "trackExposures") as? Bool let rumIntegrationEnabled = object(forKey: "rumIntegrationEnabled") as? Bool @@ -126,7 +125,6 @@ extension NSDictionary { return Flags.Configuration( gracefulModeEnabled: gracefulModeEnabled, customFlagsEndpoint: customFlagsEndpointURL, - customFlagsHeaders: customFlagsHeaders, customExposureEndpoint: customExposureEndpointURL, trackExposures: trackExposures ?? true, rumIntegrationEnabled: rumIntegrationEnabled ?? true diff --git a/packages/core/src/flags/types.ts b/packages/core/src/flags/types.ts index 1d80083f3..076546ae7 100644 --- a/packages/core/src/flags/types.ts +++ b/packages/core/src/flags/types.ts @@ -76,14 +76,6 @@ export type DatadogFlagsConfiguration = { * @default undefined */ customFlagsEndpoint?: string; - /** - * Additional HTTP headers to attach to requests made to `customFlagsEndpoint`. - * - * Useful for authentication or routing when using your own Flags service. Ignored when using the default Datadog endpoint. - * - * @default undefined - */ - customFlagsHeaders?: Record; /** * Custom server URL for sending Flags exposure data. * @@ -151,6 +143,8 @@ export interface EvaluationContext { * Attributes can include user properties, session data, or any other contextual information * needed for flag evaluation rules. */ + + // TODO: This should be a map of string to string because Android doesn't support other types attributes: Record; } From 54d1324f451f10e073c28789389da8dcc023de99 Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Wed, 3 Dec 2025 14:34:48 +0200 Subject: [PATCH 34/64] Update Flags SDK to evaluate feature flags synchronously --- example-new-architecture/App.tsx | 11 +- .../ios/Sources/DdFlagsImplementation.swift | 12 +- packages/core/src/flags/DatadogFlags.ts | 17 +- packages/core/src/flags/FlagsClient.ts | 229 +++++++++++++----- packages/core/src/flags/types.ts | 2 + packages/core/src/specs/NativeDdFlags.ts | 8 +- 6 files changed, 201 insertions(+), 78 deletions(-) diff --git a/example-new-architecture/App.tsx b/example-new-architecture/App.tsx index 5e921c234..165cf2791 100644 --- a/example-new-architecture/App.tsx +++ b/example-new-architecture/App.tsx @@ -88,7 +88,8 @@ function Section({children, title}: SectionProps): React.JSX.Element { } function App(): React.JSX.Element { - const [testFlagValue, setTestFlagValue] = React.useState(false); + const testFlagKey = 'rn-sdk-test-json-flag'; + const [testFlagValue, setTestFlagValue] = React.useState<{[key: string]: unknown}>({default: false}); React.useEffect(() => { (async () => { await DatadogFlags.enable(); @@ -100,9 +101,9 @@ function App(): React.JSX.Element { country: 'US', }, }); - const flag = await flagsClient.getBooleanDetails('rn-sdk-test-boolean-flag', false); // https://app.datadoghq.com/feature-flags/046d0e70-626d-41e1-8314-3f009fb79b7a?environmentId=d114cd9a-79ed-4c56-bcf3-bcac9293653b + const flag = flagsClient.getObjectDetails(testFlagKey, {default: {hello: 'world'}}); // https://app.datadoghq.com/feature-flags/046d0e70-626d-41e1-8314-3f009fb79b7a?environmentId=d114cd9a-79ed-4c56-bcf3-bcac9293653b setTestFlagValue(flag.value); - })(); + })().catch(console.error); }, []); const isDarkMode = useColorScheme() === 'dark'; @@ -121,7 +122,9 @@ function App(): React.JSX.Element { contentInsetAdjustmentBehavior="automatic" style={backgroundStyle}>
- rn-sdk-test-boolean-flag: {String(testFlagValue)} + + {testFlagKey}: {JSON.stringify(testFlagValue)} + = {}; + /** * Enables the Datadog Flags feature in your application. * @@ -77,7 +79,16 @@ class DatadogFlagsWrapper implements DatadogFlagsType { * ```ts * // Reminder: you need to initialize the SDK and enable the Flags feature before retrieving the client. * const flagsClient = DatadogFlags.getClient(); - * const flagValue = await flagsClient.getBooleanValue('new-feature', false); + * + * // Set the evaluation context. + * await flagsClient.setEvaluationContext({ + * targetingKey: 'user-123', + * attributes: { + * favoriteFruit: 'apple' + * } + * }); + * + * const flagValue = flagsClient.getBooleanValue('new-feature', false); * ``` */ getClient = (clientName: string = 'default'): FlagsClient => { @@ -88,7 +99,9 @@ class DatadogFlagsWrapper implements DatadogFlagsType { ); } - return new FlagsClient(clientName); + this.clients[clientName] ??= new FlagsClient(clientName); + + return this.clients[clientName]; }; } diff --git a/packages/core/src/flags/FlagsClient.ts b/packages/core/src/flags/FlagsClient.ts index e9ccf00f4..76ddcb71d 100644 --- a/packages/core/src/flags/FlagsClient.ts +++ b/packages/core/src/flags/FlagsClient.ts @@ -8,7 +8,12 @@ import { InternalLog } from '../InternalLog'; import { SdkVerbosity } from '../SdkVerbosity'; import type { DdNativeFlagsType } from '../nativeModulesTypes'; -import type { EvaluationContext, FlagDetails } from './types'; +import type { + ObjectValue, + EvaluationContext, + FlagDetails, + FlagEvaluationError +} from './types'; export class FlagsClient { // eslint-disable-next-line global-require, @typescript-eslint/no-var-requires @@ -17,21 +22,48 @@ export class FlagsClient { private clientName: string; + private evaluationContext: EvaluationContext | undefined = undefined; + private flagsCache: Record> = {}; + constructor(clientName: string = 'default') { this.clientName = clientName; } + /** + * Sets the evaluation context for the client. + * + * Should be called before evaluating any flags. + * + * @param context The evaluation context to associate with the current session. + * + * @example + * ```ts + * const flagsClient = DatadogFlags.getClient(); + * + * await flagsClient.setEvaluationContext({ + * targetingKey: 'user-123', + * attributes: { + * favoriteFruit: 'apple' + * } + * }); + * + * const flagValue = flagsClient.getBooleanValue('new-feature', false); + * ``` + */ setEvaluationContext = async ( context: EvaluationContext ): Promise => { const { targetingKey, attributes } = context; try { - await this.nativeFlags.setEvaluationContext( + const result = await this.nativeFlags.setEvaluationContext( this.clientName, targetingKey, attributes ); + + this.evaluationContext = context; + this.flagsCache = result; } catch (error) { if (error instanceof Error) { InternalLog.log( @@ -42,10 +74,107 @@ export class FlagsClient { } }; - getBooleanDetails = async ( + /** + * Returns the value of a boolean feature flag. + * + * @param key The key of the flag to evaluate. + * @param defaultValue The value to return if the flag is not found or evaluation fails. + * + * @example + * ```ts + * const isNewFeatureEnabled = flagsClient.getBooleanValue('new-feature-enabled', false); + * ``` + */ + getBooleanValue = (key: string, defaultValue: boolean): boolean => { + return this.getBooleanDetails(key, defaultValue).value; + }; + + /** + * Returns the value of a string feature flag. + * + * @param key The key of the flag to evaluate. + * @param defaultValue The value to return if the flag is not found or evaluation fails. + * + * @example + * ```ts + * const appTheme = flagsClient.getStringValue('app-theme', 'light'); + * ``` + */ + getStringValue = (key: string, defaultValue: string): string => { + return this.getStringDetails(key, defaultValue).value; + }; + + /** + * Returns the value of a number feature flag. + * + * @param key The key of the flag to evaluate. + * @param defaultValue The value to return if the flag is not found or evaluation fails. + * + * @example + * ```ts + * const ctaButtonSize = flagsClient.getNumberValue('cta-button-size', 16); + * ``` + */ + getNumberValue = (key: string, defaultValue: number): number => { + return this.getNumberDetails(key, defaultValue).value; + }; + + /** + * Returns the value of an object feature flag. + * + * @param key The key of the flag to evaluate. + * @param defaultValue The value to return if the flag is not found or evaluation fails. + * + * @example + * ```ts + * const pageCalloutOptions = flagsClient.getObjectValue('page-callout', { color: 'purple', text: 'Woof!' }); + * ``` + */ + getObjectValue = (key: string, defaultValue: ObjectValue): ObjectValue => { + return this.getObjectDetails(key, defaultValue).value; + }; + + private getDetails = (key: string, defaultValue: T): FlagDetails => { + let error: FlagEvaluationError | null = null; + + if (!this.evaluationContext) { + InternalLog.log( + `The evaluation context is not set for the client ${this.clientName}. Please, call \`DatadogFlags.setEvaluationContext()\` before evaluating any flags.`, + SdkVerbosity.ERROR + ); + + error = 'PROVIDER_NOT_READY'; + } + + const details = this.flagsCache[key]; + + if (!details) { + error = 'FLAG_NOT_FOUND'; + } + + if (error) { + return { + key, + value: defaultValue, + variant: null, + reason: null, + error + }; + } + + return details as FlagDetails; + }; + + /** + * Evaluates a boolean feature flag with detailed evaluation information. + * + * @param key The key of the flag to evaluate. + * @param defaultValue The value to return if the flag is not found or evaluation fails. + */ + getBooleanDetails = ( key: string, defaultValue: boolean - ): Promise> => { + ): FlagDetails => { if (typeof defaultValue !== 'boolean') { return { key, @@ -56,18 +185,19 @@ export class FlagsClient { }; } - const details = await this.nativeFlags.getBooleanDetails( - this.clientName, - key, - defaultValue - ); - return details; + return this.getDetails(key, defaultValue); }; - getStringDetails = async ( + /** + * Evaluates a string feature flag with detailed evaluation information. + * + * @param key The key of the flag to evaluate. + * @param defaultValue The value to return if the flag is not found or evaluation fails. + */ + getStringDetails = ( key: string, defaultValue: string - ): Promise> => { + ): FlagDetails => { if (typeof defaultValue !== 'string') { return { key, @@ -78,18 +208,19 @@ export class FlagsClient { }; } - const details = await this.nativeFlags.getStringDetails( - this.clientName, - key, - defaultValue - ); - return details; + return this.getDetails(key, defaultValue); }; - getNumberDetails = async ( + /** + * Evaluates a number feature flag with detailed evaluation information. + * + * @param key The key of the flag to evaluate. + * @param defaultValue The value to return if the flag is not found or evaluation fails. + */ + getNumberDetails = ( key: string, defaultValue: number - ): Promise> => { + ): FlagDetails => { if (typeof defaultValue !== 'number') { return { key, @@ -100,18 +231,19 @@ export class FlagsClient { }; } - const details = await this.nativeFlags.getNumberDetails( - this.clientName, - key, - defaultValue - ); - return details; + return this.getDetails(key, defaultValue); }; - getObjectDetails = async ( + /** + * Evaluates an object feature flag with detailed evaluation information. + * + * @param key The key of the flag to evaluate. + * @param defaultValue The value to return if the flag is not found or evaluation fails. + */ + getObjectDetails = ( key: string, - defaultValue: { [key: string]: unknown } - ): Promise> => { + defaultValue: ObjectValue + ): FlagDetails => { if (typeof defaultValue !== 'object' || defaultValue === null) { return { key, @@ -122,43 +254,6 @@ export class FlagsClient { }; } - const details = await this.nativeFlags.getObjectDetails( - this.clientName, - key, - defaultValue - ); - return details; - }; - - getBooleanValue = async ( - key: string, - defaultValue: boolean - ): Promise => { - const details = await this.getBooleanDetails(key, defaultValue); - return details.value; - }; - - getStringValue = async ( - key: string, - defaultValue: string - ): Promise => { - const details = await this.getStringDetails(key, defaultValue); - return details.value; - }; - - getNumberValue = async ( - key: string, - defaultValue: number - ): Promise => { - const details = await this.getNumberDetails(key, defaultValue); - return details.value; - }; - - getObjectValue = async ( - key: string, - defaultValue: { [key: string]: unknown } - ): Promise<{ [key: string]: unknown }> => { - const details = await this.getObjectDetails(key, defaultValue); - return details.value; + return this.getDetails(key, defaultValue); }; } diff --git a/packages/core/src/flags/types.ts b/packages/core/src/flags/types.ts index 43ff9aa7f..9fa8c1118 100644 --- a/packages/core/src/flags/types.ts +++ b/packages/core/src/flags/types.ts @@ -148,6 +148,8 @@ export interface EvaluationContext { attributes: Record; } +export type ObjectValue = { [key: string]: unknown }; + /** * An error tha occurs during feature flag evaluation. * diff --git a/packages/core/src/specs/NativeDdFlags.ts b/packages/core/src/specs/NativeDdFlags.ts index 27d349319..9df521257 100644 --- a/packages/core/src/specs/NativeDdFlags.ts +++ b/packages/core/src/specs/NativeDdFlags.ts @@ -19,8 +19,8 @@ export interface Spec extends TurboModule { readonly setEvaluationContext: ( clientName: string, targetingKey: string, - attributes: { [key: string]: unknown } - ) => Promise; + attributes: Object + ) => Promise<{ [key: string]: FlagDetails }>; readonly getBooleanDetails: ( clientName: string, @@ -43,8 +43,8 @@ export interface Spec extends TurboModule { readonly getObjectDetails: ( clientName: string, key: string, - defaultValue: { [key: string]: unknown } - ) => Promise>; + defaultValue: Object + ) => Promise>; } // eslint-disable-next-line import/no-default-export From d5848d7deee09f4aacc96be8ed64382883cc4c72 Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Wed, 3 Dec 2025 15:40:34 +0200 Subject: [PATCH 35/64] FFL-1460 Track sync flag evaluations --- packages/core/ios/Sources/DdFlags.mm | 13 +++++++++++++ .../core/ios/Sources/DdFlagsImplementation.swift | 7 +++++++ packages/core/src/flags/FlagsClient.ts | 3 +++ packages/core/src/specs/NativeDdFlags.ts | 5 +++++ 4 files changed, 28 insertions(+) diff --git a/packages/core/ios/Sources/DdFlags.mm b/packages/core/ios/Sources/DdFlags.mm index cbf3abc7e..42498cc07 100644 --- a/packages/core/ios/Sources/DdFlags.mm +++ b/packages/core/ios/Sources/DdFlags.mm @@ -34,6 +34,15 @@ @implementation DdFlags [self setEvaluationContext:clientName targetingKey:targetingKey attributes:attributes resolve:resolve reject:reject]; } +RCT_REMAP_METHOD(trackEvaluation, + trackEvaluationWithClientName:(NSString *)clientName + withKey:(NSString *)key + withResolve:(RCTPromiseResolveBlock)resolve + withReject:(RCTPromiseRejectBlock)reject) +{ + [self trackEvaluation:clientName key:key resolve:resolve reject:reject]; +} + RCT_REMAP_METHOD(getBooleanDetails, getBooleanDetailsWithClientName:(NSString *)clientName withKey:(NSString *)key @@ -107,6 +116,10 @@ - (void)setEvaluationContext:(NSString *)clientName targetingKey:(NSString *)tar [self.ddFlagsImplementation setEvaluationContext:clientName targetingKey:targetingKey attributes:attributes resolve:resolve reject:reject]; } +- (void)trackEvaluation:(NSString *)clientName key:(NSString *)key resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + [self.ddFlagsImplementation trackEvaluation:clientName key:key resolve:resolve reject:reject]; +} + - (void)getBooleanDetails:(NSString *)clientName key:(NSString *)key defaultValue:(BOOL)defaultValue resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { [self.ddFlagsImplementation getBooleanDetails:clientName key:key defaultValue:defaultValue resolve:resolve reject:reject]; } diff --git a/packages/core/ios/Sources/DdFlagsImplementation.swift b/packages/core/ios/Sources/DdFlagsImplementation.swift index 6927676d7..b3df62963 100644 --- a/packages/core/ios/Sources/DdFlagsImplementation.swift +++ b/packages/core/ios/Sources/DdFlagsImplementation.swift @@ -102,6 +102,13 @@ public class DdFlagsImplementation: NSObject { } } + @objc + public func trackEvaluation(_ clientName: String, key: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) { + let client = getClient(name: clientName) + client.trackEvaluation(key: key) + resolve(nil) + } + @objc public func getBooleanDetails( _ clientName: String, diff --git a/packages/core/src/flags/FlagsClient.ts b/packages/core/src/flags/FlagsClient.ts index 76ddcb71d..7afb58af6 100644 --- a/packages/core/src/flags/FlagsClient.ts +++ b/packages/core/src/flags/FlagsClient.ts @@ -162,6 +162,9 @@ export class FlagsClient { }; } + // Don't await this; non-blocking. + this.nativeFlags.trackEvaluation(this.clientName, key); + return details as FlagDetails; }; diff --git a/packages/core/src/specs/NativeDdFlags.ts b/packages/core/src/specs/NativeDdFlags.ts index 9df521257..5397097c3 100644 --- a/packages/core/src/specs/NativeDdFlags.ts +++ b/packages/core/src/specs/NativeDdFlags.ts @@ -22,6 +22,11 @@ export interface Spec extends TurboModule { attributes: Object ) => Promise<{ [key: string]: FlagDetails }>; + readonly trackEvaluation: ( + clientName: string, + key: string + ) => Promise; + readonly getBooleanDetails: ( clientName: string, key: string, From 696b72c0c8e55e6ef1feaaac07efc043a9c8678e Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Wed, 3 Dec 2025 15:44:58 +0200 Subject: [PATCH 36/64] FFL-1460 Remove `get*Details` methods from DdFlagsImplementation --- packages/core/ios/Sources/DdFlags.mm | 56 --------------- .../ios/Sources/DdFlagsImplementation.swift | 70 ------------------- packages/core/src/specs/NativeDdFlags.ts | 24 ------- 3 files changed, 150 deletions(-) diff --git a/packages/core/ios/Sources/DdFlags.mm b/packages/core/ios/Sources/DdFlags.mm index 42498cc07..8cdaf8f43 100644 --- a/packages/core/ios/Sources/DdFlags.mm +++ b/packages/core/ios/Sources/DdFlags.mm @@ -43,46 +43,6 @@ @implementation DdFlags [self trackEvaluation:clientName key:key resolve:resolve reject:reject]; } -RCT_REMAP_METHOD(getBooleanDetails, - getBooleanDetailsWithClientName:(NSString *)clientName - withKey:(NSString *)key - withDefaultValue:(BOOL)defaultValue - withResolve:(RCTPromiseResolveBlock)resolve - withReject:(RCTPromiseRejectBlock)reject) -{ - [self getBooleanDetails:clientName key:key defaultValue:defaultValue resolve:resolve reject:reject]; -} - -RCT_REMAP_METHOD(getStringDetails, - getStringDetailsWithClientName:(NSString *)clientName - withKey:(NSString *)key - withDefaultValue:(NSString *)defaultValue - withResolve:(RCTPromiseResolveBlock)resolve - withReject:(RCTPromiseRejectBlock)reject) -{ - [self getStringDetails:clientName key:key defaultValue:defaultValue resolve:resolve reject:reject]; -} - -RCT_REMAP_METHOD(getNumberDetails, - getNumberDetailsWithClientName:(NSString *)clientName - withKey:(NSString *)key - withDefaultValue:(double)defaultValue - withResolve:(RCTPromiseResolveBlock)resolve - withReject:(RCTPromiseRejectBlock)reject) -{ - [self getNumberDetails:clientName key:key defaultValue:defaultValue resolve:resolve reject:reject]; -} - -RCT_REMAP_METHOD(getObjectDetails, - getObjectDetailsWithClientName:(NSString *)clientName - withKey:(NSString *)key - withDefaultValue:(NSDictionary *)defaultValue - withResolve:(RCTPromiseResolveBlock)resolve - withReject:(RCTPromiseRejectBlock)reject) -{ - [self getObjectDetails:clientName key:key defaultValue:defaultValue resolve:resolve reject:reject]; -} - // Thanks to this guard, we won't compile this code when we build for the new architecture. #ifdef RCT_NEW_ARCH_ENABLED - (std::shared_ptr)getTurboModule: @@ -119,20 +79,4 @@ - (void)setEvaluationContext:(NSString *)clientName targetingKey:(NSString *)tar - (void)trackEvaluation:(NSString *)clientName key:(NSString *)key resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { [self.ddFlagsImplementation trackEvaluation:clientName key:key resolve:resolve reject:reject]; } - -- (void)getBooleanDetails:(NSString *)clientName key:(NSString *)key defaultValue:(BOOL)defaultValue resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { - [self.ddFlagsImplementation getBooleanDetails:clientName key:key defaultValue:defaultValue resolve:resolve reject:reject]; -} - -- (void)getStringDetails:(NSString *)clientName key:(NSString *)key defaultValue:(NSString *)defaultValue resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { - [self.ddFlagsImplementation getStringDetails:clientName key:key defaultValue:defaultValue resolve:resolve reject:reject]; -} - -- (void)getNumberDetails:(NSString *)clientName key:(NSString *)key defaultValue:(double)defaultValue resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { - [self.ddFlagsImplementation getNumberDetails:clientName key:key defaultValue:defaultValue resolve:resolve reject:reject]; -} - -- (void)getObjectDetails:(NSString *)clientName key:(NSString *)key defaultValue:(NSDictionary *)defaultValue resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { - [self.ddFlagsImplementation getObjectDetails:clientName key:key defaultValue:defaultValue resolve:resolve reject:reject]; -} @end diff --git a/packages/core/ios/Sources/DdFlagsImplementation.swift b/packages/core/ios/Sources/DdFlagsImplementation.swift index b3df62963..7f0544be9 100644 --- a/packages/core/ios/Sources/DdFlagsImplementation.swift +++ b/packages/core/ios/Sources/DdFlagsImplementation.swift @@ -108,76 +108,6 @@ public class DdFlagsImplementation: NSObject { client.trackEvaluation(key: key) resolve(nil) } - - @objc - public func getBooleanDetails( - _ clientName: String, - key: String, - defaultValue: Bool, - resolve: RCTPromiseResolveBlock, - reject: RCTPromiseRejectBlock - ) { - let client = getClient(name: clientName) - let details = client.getBooleanDetails(key: key, defaultValue: defaultValue) - let serializedDetails = details.toSerializedDictionary() - resolve(serializedDetails) - } - - @objc - public func getStringDetails( - _ clientName: String, - key: String, - defaultValue: String, - resolve: RCTPromiseResolveBlock, - reject: RCTPromiseRejectBlock - ) { - let client = getClient(name: clientName) - let details = client.getStringDetails(key: key, defaultValue: defaultValue) - let serializedDetails = details.toSerializedDictionary() - resolve(serializedDetails) - } - - @objc - public func getNumberDetails( - _ clientName: String, - key: String, - defaultValue: Double, - resolve: RCTPromiseResolveBlock, - reject: RCTPromiseRejectBlock - ) { - let client = getClient(name: clientName) - - let doubleDetails = client.getDoubleDetails(key: key, defaultValue: defaultValue) - - // Try to retrieve this flag as Integer, not a Number flag type. - if doubleDetails.error == .typeMismatch { - if let safeInt = Int(exactly: defaultValue) { - let intDetails = client.getIntegerDetails(key: key, defaultValue: safeInt) - - // If resolved correctly, return Integer details. - if intDetails.error == nil { - resolve(intDetails.toSerializedDictionary()) - return - } - } - } - - resolve(doubleDetails.toSerializedDictionary()) - } - - @objc - public func getObjectDetails( - _ clientName: String, - key: String, - defaultValue: [String: Any], - resolve: RCTPromiseResolveBlock, - reject: RCTPromiseRejectBlock - ) { - let client = getClient(name: clientName) - let details = client.getObjectDetails(key: key, defaultValue: AnyValue.wrap(defaultValue)) - let serializedDetails = details.toSerializedDictionary() - resolve(serializedDetails) - } } extension AnyValue { diff --git a/packages/core/src/specs/NativeDdFlags.ts b/packages/core/src/specs/NativeDdFlags.ts index 5397097c3..f9f47a973 100644 --- a/packages/core/src/specs/NativeDdFlags.ts +++ b/packages/core/src/specs/NativeDdFlags.ts @@ -26,30 +26,6 @@ export interface Spec extends TurboModule { clientName: string, key: string ) => Promise; - - readonly getBooleanDetails: ( - clientName: string, - key: string, - defaultValue: boolean - ) => Promise>; - - readonly getStringDetails: ( - clientName: string, - key: string, - defaultValue: string - ) => Promise>; - - readonly getNumberDetails: ( - clientName: string, - key: string, - defaultValue: number - ) => Promise>; - - readonly getObjectDetails: ( - clientName: string, - key: string, - defaultValue: Object - ) => Promise>; } // eslint-disable-next-line import/no-default-export From 7183cc79b5bc55e276819ca23183286817206695 Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Wed, 3 Dec 2025 17:16:30 +0200 Subject: [PATCH 37/64] Address PR comments regarding DatadogFlags enable method --- packages/core/src/flags/DatadogFlags.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/core/src/flags/DatadogFlags.ts b/packages/core/src/flags/DatadogFlags.ts index 206bbb276..01d4cd858 100644 --- a/packages/core/src/flags/DatadogFlags.ts +++ b/packages/core/src/flags/DatadogFlags.ts @@ -50,14 +50,17 @@ class DatadogFlagsWrapper implements DatadogFlagsType { * @param configuration Configuration options for the Datadog Flags feature. */ enable = async ( - configuration?: Omit + configuration?: DatadogFlagsConfiguration ): Promise => { + if (configuration?.enabled === false) { + return; + } + if (this.isFeatureEnabled) { InternalLog.log( 'Datadog Flags feature has already been enabled. Skipping this `DatadogFlags.enable()` call.', SdkVerbosity.WARN ); - return; } await this.nativeFlags.enable({ ...configuration, enabled: true }); From 7ebf3a8babdc6411f045b9ec30a440ed67b673fb Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Wed, 3 Dec 2025 17:18:17 +0200 Subject: [PATCH 38/64] Fix Obj-C remap names --- packages/core/ios/Sources/DdFlags.mm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/ios/Sources/DdFlags.mm b/packages/core/ios/Sources/DdFlags.mm index cbf3abc7e..c7034a6c3 100644 --- a/packages/core/ios/Sources/DdFlags.mm +++ b/packages/core/ios/Sources/DdFlags.mm @@ -17,7 +17,7 @@ @implementation DdFlags RCT_EXPORT_MODULE() RCT_REMAP_METHOD(enable, - withConfiguration:(NSDictionary *)configuration + enableDdFlagsWithConfiguration:(NSDictionary *)configuration withResolve:(RCTPromiseResolveBlock)resolve withReject:(RCTPromiseRejectBlock)reject) { @@ -25,7 +25,7 @@ @implementation DdFlags } RCT_REMAP_METHOD(setEvaluationContext, - withClientName:(NSString *)clientName + setEvaluationContextWithClientName:(NSString *)clientName withTargetingKey:(NSString *)targetingKey withAttributes:(NSDictionary *)attributes withResolve:(RCTPromiseResolveBlock)resolve From c1f9446db12ee67745e0b19f9731a70c10768c1c Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Mon, 8 Dec 2025 15:01:25 +0200 Subject: [PATCH 39/64] Fix JS tests --- packages/core/src/flags/DatadogFlags.ts | 3 ++- packages/core/src/flags/__tests__/DatadogFlags.test.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/core/src/flags/DatadogFlags.ts b/packages/core/src/flags/DatadogFlags.ts index 01d4cd858..666226eba 100644 --- a/packages/core/src/flags/DatadogFlags.ts +++ b/packages/core/src/flags/DatadogFlags.ts @@ -63,7 +63,8 @@ class DatadogFlagsWrapper implements DatadogFlagsType { ); } - await this.nativeFlags.enable({ ...configuration, enabled: true }); + // Default `enabled` to `true`. + await this.nativeFlags.enable({ enabled: true, ...configuration }); this.isFeatureEnabled = true; }; diff --git a/packages/core/src/flags/__tests__/DatadogFlags.test.ts b/packages/core/src/flags/__tests__/DatadogFlags.test.ts index 70a76102f..7e579d533 100644 --- a/packages/core/src/flags/__tests__/DatadogFlags.test.ts +++ b/packages/core/src/flags/__tests__/DatadogFlags.test.ts @@ -33,7 +33,8 @@ describe('DatadogFlags', () => { await DatadogFlags.enable(); expect(InternalLog.log).toHaveBeenCalledTimes(2); - expect(NativeModules.DdFlags.enable).toHaveBeenCalledTimes(1); + // We let the native part of the SDK handle this gracefully. + expect(NativeModules.DdFlags.enable).toHaveBeenCalledTimes(3); }); it('should print an error if retrieving the client before the feature is enabled', async () => { From b7cebb47c7552e63ccf52b9c2826e4444750ffdf Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Mon, 8 Dec 2025 19:35:19 +0200 Subject: [PATCH 40/64] Support the `client.getFlagsDetails` -> `client.getAllFlagsDetails` rename --- .../core/ios/Sources/DdFlagsImplementation.swift | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/core/ios/Sources/DdFlagsImplementation.swift b/packages/core/ios/Sources/DdFlagsImplementation.swift index 7f0544be9..98e37ca1e 100644 --- a/packages/core/ios/Sources/DdFlagsImplementation.swift +++ b/packages/core/ios/Sources/DdFlagsImplementation.swift @@ -6,7 +6,7 @@ import Foundation import DatadogInternal -@_spi(Internal) +@_spi(Internal) import DatadogFlags @objc @@ -69,21 +69,21 @@ public class DdFlagsImplementation: NSObject { public func setEvaluationContext(_ clientName: String, targetingKey: String, attributes: NSDictionary, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { let client = getClient(name: clientName) - let parsedAttributes = parseAttributes(attributes: attributes) + let parsedAttributes = parseAttributes(attributes: attributes) let evaluationContext = FlagsEvaluationContext(targetingKey: targetingKey, attributes: parsedAttributes) client.setEvaluationContext(evaluationContext) { result in switch result { case .success: - guard let flagsDetails = client.getFlagsDetails() else { + guard let flagsDetails = client.getAllFlagsDetails() else { reject(nil, "CLIENT_NOT_INITIALIZED", nil) return } - + let result = flagsDetails.compactMapValues { details in details.toSerializedDictionary() } - + resolve(result) case .failure(let error): var errorCode: String @@ -165,7 +165,7 @@ extension FlagDetails { return dict } - + private func getSerializedValue() -> Any { if let boolValue = value as? Bool { return boolValue @@ -182,7 +182,7 @@ extension FlagDetails { // Fallback for unexpected types. return NSNull() } - + private func getSerializedError() -> String? { guard let error = error else { return nil From 2adedb02866735fe3aebac032179f872a80b4f23 Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Tue, 9 Dec 2025 19:15:01 +0200 Subject: [PATCH 41/64] Cast to FlagsClientInternal when using "internal" iOS APIs --- packages/core/ios/Sources/DdFlagsImplementation.swift | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/core/ios/Sources/DdFlagsImplementation.swift b/packages/core/ios/Sources/DdFlagsImplementation.swift index 98e37ca1e..2afe4cf97 100644 --- a/packages/core/ios/Sources/DdFlagsImplementation.swift +++ b/packages/core/ios/Sources/DdFlagsImplementation.swift @@ -68,6 +68,9 @@ public class DdFlagsImplementation: NSObject { @objc public func setEvaluationContext(_ clientName: String, targetingKey: String, attributes: NSDictionary, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { let client = getClient(name: clientName) + guard let clientInternal = getClient(name: clientName) as? FlagsClientInternal else { + return + } let parsedAttributes = parseAttributes(attributes: attributes) let evaluationContext = FlagsEvaluationContext(targetingKey: targetingKey, attributes: parsedAttributes) @@ -75,7 +78,7 @@ public class DdFlagsImplementation: NSObject { client.setEvaluationContext(evaluationContext) { result in switch result { case .success: - guard let flagsDetails = client.getAllFlagsDetails() else { + guard let flagsDetails = clientInternal.getAllFlagsDetails() else { reject(nil, "CLIENT_NOT_INITIALIZED", nil) return } @@ -104,7 +107,10 @@ public class DdFlagsImplementation: NSObject { @objc public func trackEvaluation(_ clientName: String, key: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) { - let client = getClient(name: clientName) + guard let client = getClient(name: clientName) as? FlagsClientInternal else { + return + } + client.trackEvaluation(key: key) resolve(nil) } From 8f78e646e25be4d1a2dcfb20a24ef9efcb1653ba Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Thu, 11 Dec 2025 14:20:09 +0200 Subject: [PATCH 42/64] FFL-1460 Implement a proper Android wrapper, update the flags caching functionality --- example-new-architecture/App.tsx | 9 +- .../reactnative/DdFlagsImplementation.kt | 288 ++++++++---------- .../com/datadog/reactnative/JSONBridgeExt.kt | 107 +++++++ .../kotlin/com/datadog/reactnative/DdFlags.kt | 85 ++---- .../kotlin/com/datadog/reactnative/DdFlags.kt | 52 +--- packages/core/src/flags/FlagsClient.ts | 65 ++-- packages/core/src/flags/internal.ts | 55 ++++ packages/core/src/flags/types.ts | 6 +- packages/core/src/index.tsx | 5 +- packages/core/src/specs/NativeDdFlags.ts | 9 +- 10 files changed, 375 insertions(+), 306 deletions(-) create mode 100644 packages/core/android/src/main/kotlin/com/datadog/reactnative/JSONBridgeExt.kt create mode 100644 packages/core/src/flags/internal.ts diff --git a/example-new-architecture/App.tsx b/example-new-architecture/App.tsx index 165cf2791..7b1bc25a0 100644 --- a/example-new-architecture/App.tsx +++ b/example-new-architecture/App.tsx @@ -10,6 +10,7 @@ import { DdTrace, DatadogFlags, } from '@datadog/mobile-react-native'; +import type { FlagDetails } from '@datadog/mobile-react-native'; import React from 'react'; import type {PropsWithChildren} from 'react'; import { @@ -89,7 +90,7 @@ function Section({children, title}: SectionProps): React.JSX.Element { function App(): React.JSX.Element { const testFlagKey = 'rn-sdk-test-json-flag'; - const [testFlagValue, setTestFlagValue] = React.useState<{[key: string]: unknown}>({default: false}); + const [testFlagValue, setTestFlagValue] = React.useState | null>(null); React.useEffect(() => { (async () => { await DatadogFlags.enable(); @@ -101,8 +102,8 @@ function App(): React.JSX.Element { country: 'US', }, }); - const flag = flagsClient.getObjectDetails(testFlagKey, {default: {hello: 'world'}}); // https://app.datadoghq.com/feature-flags/046d0e70-626d-41e1-8314-3f009fb79b7a?environmentId=d114cd9a-79ed-4c56-bcf3-bcac9293653b - setTestFlagValue(flag.value); + const flag = flagsClient.getObjectDetails(testFlagKey, {default: {hello: 'world'}}); // https://app.datadoghq.com/feature-flags/bcf75cd6-96d8-4182-8871-0b66ad76127a?environmentId=d114cd9a-79ed-4c56-bcf3-bcac9293653b + setTestFlagValue(flag); })().catch(console.error); }, []); @@ -123,7 +124,7 @@ function App(): React.JSX.Element { style={backgroundStyle}>
- {testFlagKey}: {JSON.stringify(testFlagValue)} + {testFlagKey}: {JSON.stringify(testFlagValue, null, 2)} = mutableMapOf() /** * Enable the Flags feature with the provided configuration. * @param configuration The configuration for Flags. */ - fun enable(configuration: ReadableMap, promise: Promise) { + fun enable( + configuration: ReadableMap, + promise: Promise, + ) { val flagsConfig = configuration.asFlagsConfiguration() if (flagsConfig != null) { Flags.enable(flagsConfig, sdkCore) } else { InternalLogger.UNBOUND.log( - InternalLogger.Level.ERROR, - InternalLogger.Target.USER, - { "Invalid configuration provided for Flags. Feature initialization skipped." } + InternalLogger.Level.ERROR, + InternalLogger.Target.USER, + { "Invalid configuration provided for Flags. Feature initialization skipped." }, ) } promise.resolve(null) @@ -50,22 +52,10 @@ class DdFlagsImplementation(private val sdkCore: SdkCore = Datadog.getInstance() * On hot reload, the cache is cleared and clients are recreated - this is safe * because gracefulModeEnabled=true prevents crashes on duplicate creation. */ - private fun getClient(name: String): FlagsClient { - return clients.getOrPut(name) { + private fun getClient(name: String): FlagsClient = + clients.getOrPut(name) { FlagsClient.Builder(name, sdkCore).build() } - } - - private fun parseAttributes(attributes: ReadableMap): Map { - val result = mutableMapOf() - val iterator = attributes.entryIterator - while (iterator.hasNext()) { - val entry = iterator.next() - // Convert all values to strings as required by Android SDK - result[entry.key] = entry.value?.toString() ?: "" - } - return result - } /** * Set the evaluation context for a specific client. @@ -74,90 +64,48 @@ class DdFlagsImplementation(private val sdkCore: SdkCore = Datadog.getInstance() * @param attributes The attributes for the evaluation context (will be converted to strings). */ fun setEvaluationContext( - clientName: String, - targetingKey: String, - attributes: ReadableMap, - promise: Promise + clientName: String, + targetingKey: String, + attributes: ReadableMap, + promise: Promise, ) { val client = getClient(clientName) - val parsedAttributes = parseAttributes(attributes) - val evaluationContext = EvaluationContext(targetingKey, parsedAttributes) + // Set the evaluation context. + val evaluationContext = buildEvaluationContext(targetingKey, attributes) client.setEvaluationContext(evaluationContext) - promise.resolve(null) - } - - /** - * Get details for a boolean flag. - * @param clientName The name of the client. - * @param key The flag key. - * @param defaultValue The default value. - */ - fun getBooleanDetails( - clientName: String, - key: String, - defaultValue: Boolean, - promise: Promise - ) { - val client = getClient(clientName) - val details = client.resolve(key, defaultValue) - promise.resolve(details.toReactNativeMap(key)) - } - /** - * Get details for a string flag. - * @param clientName The name of the client. - * @param key The flag key. - * @param defaultValue The default value. - */ - fun getStringDetails(clientName: String, key: String, defaultValue: String, promise: Promise) { - val client = getClient(clientName) - val details = client.resolve(key, defaultValue) - promise.resolve(details.toReactNativeMap(key)) - } + // Retrieve flags state snapshot. + val flagsSnapshot = client._getInternal()?.getFlagAssignmentsSnapshot() - /** - * Get details for a number flag. Includes Number and Integer flags. - * @param clientName The name of the client. - * @param key The flag key. - * @param defaultValue The default value. - */ - fun getNumberDetails(clientName: String, key: String, defaultValue: Double, promise: Promise) { - val client = getClient(clientName) - - // Try as Double first. - val doubleDetails = client.resolve(key, defaultValue) - - // If type mismatch and value is an integer, try as Int. - if (doubleDetails.errorCode == ErrorCode.TYPE_MISMATCH) { - val safeInt = defaultValue.toInt() - val intDetails = client.resolve(key, safeInt) + // Send the flags state snapshot to React Native. If `flagsSnapshot` is null, the FlagsClient client is not ready yet. + if (flagsSnapshot != null) { + val mapOfMaps = + flagsSnapshot.mapValues { (key, flag) -> + convertPrecomputedFlagToMap(key, flag) + } - if (intDetails.errorCode == null) { - promise.resolve(intDetails.toReactNativeMap(key)) - return - } + promise.resolve(mapOfMaps.toWritableMap()) + } else { + promise.reject("CLIENT_NOT_INITIALIZED", "CLIENT_NOT_INITIALIZED", null) } - - promise.resolve(doubleDetails.toReactNativeMap(key)) } - /** - * Get details for an object flag. - * @param clientName The name of the client. - * @param key The flag key. - * @param defaultValue The default value. - */ - fun getObjectDetails( - clientName: String, - key: String, - defaultValue: ReadableMap, - promise: Promise + fun trackEvaluation( + clientName: String, + key: String, + rawFlag: ReadableMap, + targetingKey: String, + attributes: ReadableMap, + promise: Promise, ) { val client = getClient(clientName) - val jsonDefaultValue = defaultValue.toJSONObject() - val details = client.resolve(key, jsonDefaultValue) - promise.resolve(details.toReactNativeMap(key)) + + val precomputedFlag = convertMapToPrecomputedFlag(rawFlag.toMap()) + val evaluationContext = buildEvaluationContext(targetingKey, attributes) + client._getInternal()?.trackFlagSnapshotEvaluation(key, precomputedFlag, evaluationContext) + + promise.resolve(null) } internal companion object { @@ -165,57 +113,87 @@ class DdFlagsImplementation(private val sdkCore: SdkCore = Datadog.getInstance() } } -/** Convert ResolutionDetails to React Native map format expected by the JS layer. */ -private fun ResolutionDetails.toReactNativeMap(key: String): WritableMap { - val map = WritableNativeMap() - map.putString("key", key) - - when (val v = value) { - is Boolean -> map.putBoolean("value", v) - is String -> map.putString("value", v) - is Int -> map.putDouble("value", v.toDouble()) // Convert to double for RN. - is Double -> map.putDouble("value", v) - is JSONObject -> map.putMap("value", v.toWritableMap()) - else -> map.putNull("value") - } +private fun buildEvaluationContext( + targetingKey: String, + attributes: ReadableMap, +): EvaluationContext { + val parsed = mutableMapOf() - variant?.let { map.putString("variant", it) } ?: map.putNull("variant") - reason?.let { map.putString("reason", it.name) } ?: map.putNull("reason") - errorCode?.let { map.putString("error", it.name) } ?: map.putNull("error") + for ((key, value) in attributes.entryIterator) { + parsed[key] = value.toString() + } - return map + return EvaluationContext(targetingKey, parsed) } -/** Convert ReadableMap to JSONObject for flag default values. */ -private fun ReadableMap.toJSONObject(): JSONObject { - val json = JSONObject() - val iterator = entryIterator - while (iterator.hasNext()) { - val entry = iterator.next() - json.put(entry.key, entry.value) - } - return json -} +/** + * Converts a [PrecomputedFlag] to a [Map] for further React Native bridge transfer. + * Includes the flag key and parses the value based on variationType. + */ +private fun convertPrecomputedFlagToMap( + flagKey: String, + flag: PrecomputedFlag, +): Map { + // Parse the value based on variationType + val parsedValue: Any = + when (flag.variationType) { + "boolean" -> { + flag.variationValue.lowercase().toBooleanStrictOrNull() ?: flag.variationValue + } -/** Convert JSONObject to WritableMap for React Native. */ -private fun JSONObject.toWritableMap(): WritableMap { - val map = WritableNativeMap() - val keys = keys() - while (keys.hasNext()) { - val key = keys.next() - when (val value = get(key)) { - is Boolean -> map.putBoolean(key, value) - is Int -> map.putInt(key, value) - is Double -> map.putDouble(key, value) - is String -> map.putString(key, value) - is JSONObject -> map.putMap(key, value.toWritableMap()) - JSONObject.NULL -> map.putNull(key) - else -> map.putNull(key) + "string" -> { + flag.variationValue + } + + "integer" -> { + flag.variationValue.toIntOrNull() ?: flag.variationValue + } + + "number", "float" -> { + flag.variationValue.toDoubleOrNull() ?: flag.variationValue + } + + "object" -> { + try { + JSONObject(flag.variationValue).toMap() + } catch (e: Exception) { + flag.variationValue + } + } + + else -> { + flag.variationValue + } } - } - return map + + return mapOf( + "key" to flagKey, + "value" to parsedValue, + "variationType" to flag.variationType, + "variationValue" to flag.variationValue, + "doLog" to flag.doLog, + "allocationKey" to flag.allocationKey, + "variationKey" to flag.variationKey, + "extraLogging" to flag.extraLogging.toMap(), + "reason" to flag.reason, + ) } +/** + * Converts a [Map] to a [PrecomputedFlag]. + */ +@Suppress("UNCHECKED_CAST") +private fun convertMapToPrecomputedFlag(map: Map): PrecomputedFlag = + PrecomputedFlag( + variationType = map["variationType"] as? String ?: "", + variationValue = map["variationValue"] as? String ?: "", + doLog = map["doLog"] as? Boolean ?: false, + allocationKey = map["allocationKey"] as? String ?: "", + variationKey = map["variationKey"] as? String ?: "", + extraLogging = (map["extraLogging"] as? Map)?.toJSONObject() ?: JSONObject(), + reason = map["reason"] as? String ?: "", + ) + /** Parse configuration from ReadableMap to FlagsConfiguration. */ private fun ReadableMap.asFlagsConfiguration(): FlagsConfiguration? { val enabled = if (hasKey("enabled")) getBoolean("enabled") else false @@ -230,22 +208,22 @@ private fun ReadableMap.asFlagsConfiguration(): FlagsConfiguration? { val trackExposures = if (hasKey("trackExposures")) getBoolean("trackExposures") else true val rumIntegrationEnabled = - if (hasKey("rumIntegrationEnabled")) getBoolean("rumIntegrationEnabled") else true - - return FlagsConfiguration.Builder() - .apply { - gracefulModeEnabled(gracefulModeEnabled) - trackExposures(trackExposures) - rumIntegrationEnabled(rumIntegrationEnabled) - - // The SDK automatically appends endpoint names to the custom endpoints. - // The input config expects a base URL rather than a full URL. - if (hasKey("customFlagsEndpoint")) { - getString("customFlagsEndpoint")?.let { useCustomFlagEndpoint("$it/precompute-assignments") } - } - if (hasKey("customExposureEndpoint")) { - getString("customExposureEndpoint")?.let { useCustomExposureEndpoint("$it/api/v2/exposures") } - } + if (hasKey("rumIntegrationEnabled")) getBoolean("rumIntegrationEnabled") else true + + return FlagsConfiguration + .Builder() + .apply { + gracefulModeEnabled(gracefulModeEnabled) + trackExposures(trackExposures) + rumIntegrationEnabled(rumIntegrationEnabled) + + // The SDK automatically appends endpoint names to the custom endpoints. + // The input config expects a base URL rather than a full URL. + if (hasKey("customFlagsEndpoint")) { + getString("customFlagsEndpoint")?.let { useCustomFlagEndpoint("$it/precompute-assignments") } + } + if (hasKey("customExposureEndpoint")) { + getString("customExposureEndpoint")?.let { useCustomExposureEndpoint("$it/api/v2/exposures") } } - .build() + }.build() } diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/JSONBridgeExt.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/JSONBridgeExt.kt new file mode 100644 index 000000000..219775bba --- /dev/null +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/JSONBridgeExt.kt @@ -0,0 +1,107 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.reactnative + +import com.facebook.react.bridge.ReadableMap +import com.facebook.react.bridge.WritableMap +import org.json.JSONObject + +/** + * Converts a [JSONObject] to a [WritableMap]. + */ +internal fun JSONObject.toWritableMap(): WritableMap = this.toMap().toWritableMap() + +/** + * Converts a [JSONObject] to a [Map]. + */ +internal fun JSONObject.toMap(): Map { + val map = mutableMapOf() + val keys = this.keys() + + while (keys.hasNext()) { + val key = keys.next() + val value = this.opt(key) + + map[key] = + when (value) { + null, JSONObject.NULL -> null + is JSONObject -> value.toMap() + is org.json.JSONArray -> value.toList() + else -> value + } + } + + return map +} + +/** + * Converts a [org.json.JSONArray] to a [List]. + */ +internal fun org.json.JSONArray.toList(): List { + val list = mutableListOf() + + for (i in 0 until this.length()) { + val value = this.opt(i) + + list.add( + when (value) { + null, JSONObject.NULL -> null + is JSONObject -> value.toMap() + is org.json.JSONArray -> value.toList() + else -> value + }, + ) + } + + return list +} + +/** + * Converts a [ReadableMap] to a [JSONObject]. + */ +internal fun ReadableMap.toJSONObject(): JSONObject = this.toMap().toJSONObject() + +/** + * Converts a [Map] to a [JSONObject]. + */ +@Suppress("UNCHECKED_CAST") +internal fun Map.toJSONObject(): JSONObject { + val jsonObject = JSONObject() + + for ((key, value) in this) { + jsonObject.put( + key, + when (value) { + is Map<*, *> -> (value as Map).toJSONObject() + is List<*> -> value.toJSONArray() + else -> value + }, + ) + } + + return jsonObject +} + +/** + * Converts a [List] to a [org.json.JSONArray]. + */ +@Suppress("UNCHECKED_CAST") +internal fun List<*>.toJSONArray(): org.json.JSONArray { + val jsonArray = org.json.JSONArray() + + for (value in this) { + jsonArray.put( + when (value) { + is Map<*, *> -> (value as Map).toJSONObject() + is List<*> -> value.toJSONArray() + else -> value + }, + ) + } + + return jsonArray +} diff --git a/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdFlags.kt b/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdFlags.kt index e5a3672c2..ffb67bf1d 100644 --- a/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdFlags.kt +++ b/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdFlags.kt @@ -12,8 +12,9 @@ import com.facebook.react.bridge.ReactMethod import com.facebook.react.bridge.ReadableMap /** The entry point to use Datadog's Flags feature. */ -class DdFlags(reactContext: ReactApplicationContext) : NativeDdFlagsSpec(reactContext) { - +class DdFlags( + reactContext: ReactApplicationContext, +) : NativeDdFlagsSpec(reactContext) { private val implementation = DdFlagsImplementation() override fun getName(): String = DdFlagsImplementation.NAME @@ -23,7 +24,10 @@ class DdFlags(reactContext: ReactApplicationContext) : NativeDdFlagsSpec(reactCo * @param configuration The configuration for Flags. */ @ReactMethod - override fun enable(configuration: ReadableMap, promise: Promise) { + override fun enable( + configuration: ReadableMap, + promise: Promise, + ) { implementation.enable(configuration, promise) } @@ -35,75 +39,28 @@ class DdFlags(reactContext: ReactApplicationContext) : NativeDdFlagsSpec(reactCo */ @ReactMethod override fun setEvaluationContext( - clientName: String, - targetingKey: String, - attributes: ReadableMap, - promise: Promise + clientName: String, + targetingKey: String, + attributes: ReadableMap, + promise: Promise, ) { implementation.setEvaluationContext(clientName, targetingKey, attributes, promise) } /** - * Get details for a boolean flag. - * @param clientName The name of the client. - * @param key The flag key. - * @param defaultValue The default value. - */ - @ReactMethod - override fun getBooleanDetails( - clientName: String, - key: String, - defaultValue: Boolean, - promise: Promise - ) { - implementation.getBooleanDetails(clientName, key, defaultValue, promise) - } - - /** - * Get details for a string flag. - * @param clientName The name of the client. - * @param key The flag key. - * @param defaultValue The default value. - */ - @ReactMethod - override fun getStringDetails( - clientName: String, - key: String, - defaultValue: String, - promise: Promise - ) { - implementation.getStringDetails(clientName, key, defaultValue, promise) - } - - /** - * Get details for a number flag. Includes Number and Integer flags. - * @param clientName The name of the client. - * @param key The flag key. - * @param defaultValue The default value. - */ - @ReactMethod - override fun getNumberDetails( - clientName: String, - key: String, - defaultValue: Double, - promise: Promise - ) { - implementation.getNumberDetails(clientName, key, defaultValue, promise) - } - - /** - * Get details for an object flag. + * Track the evaluation of a flag. * @param clientName The name of the client. - * @param key The flag key. - * @param defaultValue The default value. + * @param key The key of the flag. */ @ReactMethod - override fun getObjectDetails( - clientName: String, - key: String, - defaultValue: ReadableMap, - promise: Promise + override fun trackEvaluation( + clientName: String, + key: String, + rawFlag: ReadableMap, + targetingKey: String, + attributes: ReadableMap, + promise: Promise, ) { - implementation.getObjectDetails(clientName, key, defaultValue, promise) + implementation.trackEvaluation(clientName, key, rawFlag, targetingKey, attributes, promise) } } diff --git a/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdFlags.kt b/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdFlags.kt index 8eaaae160..f4131160b 100644 --- a/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdFlags.kt +++ b/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdFlags.kt @@ -45,56 +45,12 @@ class DdFlags(reactContext: ReactApplicationContext) : ReactContextBaseJavaModul } /** - * Get details for a boolean flag. + * Track the evaluation of a flag. * @param clientName The name of the client. - * @param key The flag key. - * @param defaultValue The default value. + * @param key The key of the flag. */ @ReactMethod - fun getBooleanDetails( - clientName: String, - key: String, - defaultValue: Boolean, - promise: Promise - ) { - implementation.getBooleanDetails(clientName, key, defaultValue, promise) - } - - /** - * Get details for a string flag. - * @param clientName The name of the client. - * @param key The flag key. - * @param defaultValue The default value. - */ - @ReactMethod - fun getStringDetails(clientName: String, key: String, defaultValue: String, promise: Promise) { - implementation.getStringDetails(clientName, key, defaultValue, promise) - } - - /** - * Get details for a number flag. Includes Number and Integer flags. - * @param clientName The name of the client. - * @param key The flag key. - * @param defaultValue The default value. - */ - @ReactMethod - fun getNumberDetails(clientName: String, key: String, defaultValue: Double, promise: Promise) { - implementation.getNumberDetails(clientName, key, defaultValue, promise) - } - - /** - * Get details for an object flag. - * @param clientName The name of the client. - * @param key The flag key. - * @param defaultValue The default value. - */ - @ReactMethod - fun getObjectDetails( - clientName: String, - key: String, - defaultValue: ReadableMap, - promise: Promise - ) { - implementation.getObjectDetails(clientName, key, defaultValue, promise) + fun trackEvaluation(clientName: String, key: String, rawFlag: ReadableMap, targetingKey: String, attributes: ReadableMap, promise: Promise) { + implementation.trackEvaluation(clientName, key, rawFlag, targetingKey, attributes, promise) } } diff --git a/packages/core/src/flags/FlagsClient.ts b/packages/core/src/flags/FlagsClient.ts index 7afb58af6..d631aa1bc 100644 --- a/packages/core/src/flags/FlagsClient.ts +++ b/packages/core/src/flags/FlagsClient.ts @@ -8,12 +8,12 @@ import { InternalLog } from '../InternalLog'; import { SdkVerbosity } from '../SdkVerbosity'; import type { DdNativeFlagsType } from '../nativeModulesTypes'; -import type { - ObjectValue, - EvaluationContext, - FlagDetails, - FlagEvaluationError -} from './types'; +import { + flagCacheEntryToFlagDetails, + processEvaluationContext +} from './internal'; +import type { FlagCacheEntry } from './internal'; +import type { ObjectValue, EvaluationContext, FlagDetails } from './types'; export class FlagsClient { // eslint-disable-next-line global-require, @typescript-eslint/no-var-requires @@ -22,8 +22,8 @@ export class FlagsClient { private clientName: string; - private evaluationContext: EvaluationContext | undefined = undefined; - private flagsCache: Record> = {}; + private _evaluationContext: EvaluationContext | undefined = undefined; + private _flagsCache: Record = {}; constructor(clientName: string = 'default') { this.clientName = clientName; @@ -53,17 +53,17 @@ export class FlagsClient { setEvaluationContext = async ( context: EvaluationContext ): Promise => { - const { targetingKey, attributes } = context; + const processedContext = processEvaluationContext(context); try { const result = await this.nativeFlags.setEvaluationContext( this.clientName, - targetingKey, - attributes + processedContext.targetingKey, + processedContext.attributes ); - this.evaluationContext = context; - this.flagsCache = result; + this._evaluationContext = processedContext; + this._flagsCache = result; } catch (error) { if (error instanceof Error) { InternalLog.log( @@ -135,37 +135,48 @@ export class FlagsClient { }; private getDetails = (key: string, defaultValue: T): FlagDetails => { - let error: FlagEvaluationError | null = null; - - if (!this.evaluationContext) { + // Check whether the evaluation context has already been set. + if (!this._evaluationContext) { InternalLog.log( `The evaluation context is not set for the client ${this.clientName}. Please, call \`DatadogFlags.setEvaluationContext()\` before evaluating any flags.`, SdkVerbosity.ERROR ); - error = 'PROVIDER_NOT_READY'; + return { + key, + value: defaultValue, + variant: null, + reason: null, + error: 'PROVIDER_NOT_READY' + }; } - const details = this.flagsCache[key]; + // Retrieve the flag from the cache. + const flagCacheEntry = this._flagsCache[key]; - if (!details) { - error = 'FLAG_NOT_FOUND'; - } - - if (error) { + if (!flagCacheEntry) { return { key, value: defaultValue, variant: null, reason: null, - error + error: 'FLAG_NOT_FOUND' }; } - // Don't await this; non-blocking. - this.nativeFlags.trackEvaluation(this.clientName, key); + // Convert to FlagDetails. + const details = flagCacheEntryToFlagDetails(flagCacheEntry); + + // Track the flag evaluation. Don't await this; non-blocking. + this.nativeFlags.trackEvaluation( + this.clientName, + key, + flagCacheEntry, + this._evaluationContext.targetingKey, + this._evaluationContext.attributes + ); - return details as FlagDetails; + return details; }; /** diff --git a/packages/core/src/flags/internal.ts b/packages/core/src/flags/internal.ts new file mode 100644 index 000000000..21a640dc4 --- /dev/null +++ b/packages/core/src/flags/internal.ts @@ -0,0 +1,55 @@ +import { InternalLog } from '../InternalLog'; +import { SdkVerbosity } from '../SdkVerbosity'; + +import type { EvaluationContext, FlagDetails } from './types'; + +export interface FlagCacheEntry { + key: string; + value: unknown; + variationType: string; + variationValue: string; + doLog: boolean; + allocationKey: string; + variationKey: string; + extraLogging: Record; + reason: string; +} + +export const flagCacheEntryToFlagDetails = ( + entry: FlagCacheEntry +): FlagDetails => { + return { + key: entry.key, + value: entry.value as T, + variant: entry.variationKey, + reason: entry.reason, + error: null + }; +}; + +export const processEvaluationContext = ( + context: EvaluationContext +): EvaluationContext => { + const { targetingKey } = context; + let { attributes } = context; + + // Filter out object values from attributes because Android doesn't support nested object values in the evaluation context. + attributes = Object.fromEntries( + Object.entries(attributes) + .filter(([key, value]) => { + if (typeof value === 'object' && value !== null) { + InternalLog.log( + `Nested object value under "${key}" is not supported in the evaluation context. Omitting this atribute from the evaluation context.`, + SdkVerbosity.WARN + ); + + return false; + } + + return true; + }) + .map(([key, value]) => [key, value?.toString() ?? '']) + ); + + return { targetingKey, attributes }; +}; diff --git a/packages/core/src/flags/types.ts b/packages/core/src/flags/types.ts index 7377008fb..382488127 100644 --- a/packages/core/src/flags/types.ts +++ b/packages/core/src/flags/types.ts @@ -142,10 +142,10 @@ export interface EvaluationContext { * * Attributes can include user properties, session data, or any other contextual information * needed for flag evaluation rules. + * + * NOTE: Nested object values are not supported and will be omitted from the evaluation context. */ - - // TODO: This should be a map of string to string because Android doesn't support other types - attributes: Record; + attributes: Record; } export type ObjectValue = { [key: string]: unknown }; diff --git a/packages/core/src/index.tsx b/packages/core/src/index.tsx index fcab971ce..a788a15ac 100644 --- a/packages/core/src/index.tsx +++ b/packages/core/src/index.tsx @@ -22,7 +22,7 @@ import { ProxyConfiguration, ProxyType } from './ProxyConfiguration'; import { SdkVerbosity } from './SdkVerbosity'; import { TrackingConsent } from './TrackingConsent'; import { DatadogFlags } from './flags/DatadogFlags'; -import type { DatadogFlagsConfiguration } from './flags/types'; +import type { DatadogFlagsConfiguration, FlagDetails } from './flags/types'; import { DdLogs } from './logs/DdLogs'; import { DdRum } from './rum/DdRum'; import { DdBabelInteractionTracking } from './rum/instrumentation/interactionTracking/DdBabelInteractionTracking'; @@ -90,5 +90,6 @@ export type { FirstPartyHost, AutoInstrumentationConfiguration, PartialInitializationConfiguration, - DatadogFlagsConfiguration + DatadogFlagsConfiguration, + FlagDetails }; diff --git a/packages/core/src/specs/NativeDdFlags.ts b/packages/core/src/specs/NativeDdFlags.ts index f9f47a973..5c8afd010 100644 --- a/packages/core/src/specs/NativeDdFlags.ts +++ b/packages/core/src/specs/NativeDdFlags.ts @@ -8,7 +8,7 @@ import type { TurboModule } from 'react-native'; import { TurboModuleRegistry } from 'react-native'; -import type { FlagDetails } from '../flags/types'; +import type { FlagCacheEntry } from '../flags/internal'; /** * Do not import this Spec directly, use DdNativeFlagsType instead. @@ -20,11 +20,14 @@ export interface Spec extends TurboModule { clientName: string, targetingKey: string, attributes: Object - ) => Promise<{ [key: string]: FlagDetails }>; + ) => Promise<{ [key: string]: FlagCacheEntry }>; readonly trackEvaluation: ( clientName: string, - key: string + key: string, + rawFlag: Object, + targetingKey: string, + attributes: Object ) => Promise; } From 846223664518a4304e9ca15fc0a0aa72d7c7fac4 Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Thu, 11 Dec 2025 14:30:47 +0200 Subject: [PATCH 43/64] FFL-1460 Small changes to building Flags configuration --- .../reactnative/DdFlagsImplementation.kt | 100 +++++++++--------- 1 file changed, 50 insertions(+), 50 deletions(-) diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdFlagsImplementation.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdFlagsImplementation.kt index 524915a43..57e838d5a 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdFlagsImplementation.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdFlagsImplementation.kt @@ -16,7 +16,6 @@ import com.datadog.android.flags.internal.model.PrecomputedFlag import com.datadog.android.flags.model.EvaluationContext import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReadableMap -import com.facebook.react.bridge.WritableMap import org.json.JSONObject class DdFlagsImplementation( @@ -32,7 +31,7 @@ class DdFlagsImplementation( configuration: ReadableMap, promise: Promise, ) { - val flagsConfig = configuration.asFlagsConfiguration() + val flagsConfig = buildFlagsConfiguration(configuration.toMap()) if (flagsConfig != null) { Flags.enable(flagsConfig, sdkCore) } else { @@ -48,14 +47,11 @@ class DdFlagsImplementation( /** * Retrieve or create a FlagsClient instance. * - * Caches clients by name to avoid repeated Builder().build() calls. - * On hot reload, the cache is cleared and clients are recreated - this is safe - * because gracefulModeEnabled=true prevents crashes on duplicate creation. + * Caches clients by name to avoid repeated Builder().build() calls. On hot reload, the cache is + * cleared and clients are recreated - this is safe because gracefulModeEnabled=true prevents + * crashes on duplicate creation. */ - private fun getClient(name: String): FlagsClient = - clients.getOrPut(name) { - FlagsClient.Builder(name, sdkCore).build() - } + private fun getClient(name: String): FlagsClient = clients.getOrPut(name) { FlagsClient.Builder(name, sdkCore).build() } /** * Set the evaluation context for a specific client. @@ -78,7 +74,8 @@ class DdFlagsImplementation( // Retrieve flags state snapshot. val flagsSnapshot = client._getInternal()?.getFlagAssignmentsSnapshot() - // Send the flags state snapshot to React Native. If `flagsSnapshot` is null, the FlagsClient client is not ready yet. + // Send the flags state snapshot to React Native. If `flagsSnapshot` is null, the + // FlagsClient client is not ready yet. if (flagsSnapshot != null) { val mapOfMaps = flagsSnapshot.mapValues { (key, flag) -> @@ -113,6 +110,40 @@ class DdFlagsImplementation( } } +@Suppress("UNCHECKED_CAST") +private fun buildFlagsConfiguration(configuration: Map): FlagsConfiguration? { + val enabled = configuration["enabled"] as? Boolean ?: false + + if (!enabled) { + return null + } + + // Hard set `gracefulModeEnabled` to `true` because SDK misconfigurations are handled on JS + // side. + // This prevents crashes on hot reload when clients are recreated. + val gracefulModeEnabled = true + + val trackExposures = configuration["trackExposures"] as? Boolean ?: true + val rumIntegrationEnabled = configuration["rumIntegrationEnabled"] as? Boolean ?: true + + return FlagsConfiguration + .Builder() + .apply { + gracefulModeEnabled(gracefulModeEnabled) + trackExposures(trackExposures) + rumIntegrationEnabled(rumIntegrationEnabled) + + // The SDK automatically appends endpoint names to the custom endpoints. + // The input config expects a base URL rather than a full URL. + (configuration["customFlagsEndpoint"] as? String)?.let { + useCustomFlagEndpoint("$it/precompute-assignments") + } + (configuration["customExposureEndpoint"] as? String)?.let { + useCustomExposureEndpoint("$it/api/v2/exposures") + } + }.build() +} + private fun buildEvaluationContext( targetingKey: String, attributes: ReadableMap, @@ -127,8 +158,11 @@ private fun buildEvaluationContext( } /** - * Converts a [PrecomputedFlag] to a [Map] for further React Native bridge transfer. - * Includes the flag key and parses the value based on variationType. + * Converts a [PrecomputedFlag] to a [Map] for further React Native bridge transfer. Includes the + * flag key and parses the value based on variationType. + * + * We are using Map instead of WritableMap as an intermediate because it is more handy, and we can + * convert to WritableMap right before sending to React Native. */ private fun convertPrecomputedFlagToMap( flagKey: String, @@ -179,9 +213,7 @@ private fun convertPrecomputedFlagToMap( ) } -/** - * Converts a [Map] to a [PrecomputedFlag]. - */ +/** Converts a [Map] to a [PrecomputedFlag]. */ @Suppress("UNCHECKED_CAST") private fun convertMapToPrecomputedFlag(map: Map): PrecomputedFlag = PrecomputedFlag( @@ -190,40 +222,8 @@ private fun convertMapToPrecomputedFlag(map: Map): PrecomputedFlag doLog = map["doLog"] as? Boolean ?: false, allocationKey = map["allocationKey"] as? String ?: "", variationKey = map["variationKey"] as? String ?: "", - extraLogging = (map["extraLogging"] as? Map)?.toJSONObject() ?: JSONObject(), + extraLogging = + (map["extraLogging"] as? Map)?.toJSONObject() + ?: JSONObject(), reason = map["reason"] as? String ?: "", ) - -/** Parse configuration from ReadableMap to FlagsConfiguration. */ -private fun ReadableMap.asFlagsConfiguration(): FlagsConfiguration? { - val enabled = if (hasKey("enabled")) getBoolean("enabled") else false - - if (!enabled) { - return null - } - - // Hard set `gracefulModeEnabled` to `true` because SDK misconfigurations are handled on JS side. - // This prevents crashes on hot reload when clients are recreated. - val gracefulModeEnabled = true - - val trackExposures = if (hasKey("trackExposures")) getBoolean("trackExposures") else true - val rumIntegrationEnabled = - if (hasKey("rumIntegrationEnabled")) getBoolean("rumIntegrationEnabled") else true - - return FlagsConfiguration - .Builder() - .apply { - gracefulModeEnabled(gracefulModeEnabled) - trackExposures(trackExposures) - rumIntegrationEnabled(rumIntegrationEnabled) - - // The SDK automatically appends endpoint names to the custom endpoints. - // The input config expects a base URL rather than a full URL. - if (hasKey("customFlagsEndpoint")) { - getString("customFlagsEndpoint")?.let { useCustomFlagEndpoint("$it/precompute-assignments") } - } - if (hasKey("customExposureEndpoint")) { - getString("customExposureEndpoint")?.let { useCustomExposureEndpoint("$it/api/v2/exposures") } - } - }.build() -} From 4ae663bf3197c8ffe96226b29f626f74cb090943 Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Thu, 11 Dec 2025 19:33:16 +0200 Subject: [PATCH 44/64] Update iOS wrapper code with the changes to the exposed API --- packages/core/ios/Sources/DdFlags.mm | 40 ++-- .../ios/Sources/DdFlagsImplementation.swift | 190 +++++++++++------- .../ios/Sources/RNDdSdkConfiguration.swift | 31 --- packages/core/src/flags/internal.ts | 6 +- 4 files changed, 143 insertions(+), 124 deletions(-) diff --git a/packages/core/ios/Sources/DdFlags.mm b/packages/core/ios/Sources/DdFlags.mm index a9e28d9bb..8c6a1d0e2 100644 --- a/packages/core/ios/Sources/DdFlags.mm +++ b/packages/core/ios/Sources/DdFlags.mm @@ -16,31 +16,31 @@ @implementation DdFlags RCT_EXPORT_MODULE() -RCT_REMAP_METHOD(enable, - enableDdFlagsWithConfiguration:(NSDictionary *)configuration - withResolve:(RCTPromiseResolveBlock)resolve - withReject:(RCTPromiseRejectBlock)reject) +RCT_EXPORT_METHOD(enable:(NSDictionary *)configuration + withResolver:(RCTPromiseResolveBlock)resolve + withRejecter:(RCTPromiseRejectBlock)reject) { [self enable:configuration resolve:resolve reject:reject]; } -RCT_REMAP_METHOD(setEvaluationContext, - setEvaluationContextWithClientName:(NSString *)clientName - withTargetingKey:(NSString *)targetingKey - withAttributes:(NSDictionary *)attributes - withResolve:(RCTPromiseResolveBlock)resolve - withReject:(RCTPromiseRejectBlock)reject) +RCT_EXPORT_METHOD(setEvaluationContext:(NSString *)clientName + targetingKey:(NSString *)targetingKey + attributes:(NSDictionary *)attributes + withResolver:(RCTPromiseResolveBlock)resolve + withRejecter:(RCTPromiseRejectBlock)reject) { [self setEvaluationContext:clientName targetingKey:targetingKey attributes:attributes resolve:resolve reject:reject]; } -RCT_REMAP_METHOD(trackEvaluation, - trackEvaluationWithClientName:(NSString *)clientName - withKey:(NSString *)key - withResolve:(RCTPromiseResolveBlock)resolve - withReject:(RCTPromiseRejectBlock)reject) +RCT_EXPORT_METHOD(trackEvaluation:(NSString *)clientName + withKey:(NSString *)key + withRawFlag:(NSDictionary *)rawFlag + targetingKey:(NSString *)targetingKey + attributes:(NSDictionary *)attributes + withResolver:(RCTPromiseResolveBlock)resolve + withRejecter:(RCTPromiseRejectBlock)reject) { - [self trackEvaluation:clientName key:key resolve:resolve reject:reject]; + [self trackEvaluation:clientName key:key rawFlag:rawFlag targetingKey:targetingKey attributes:attributes resolve:resolve reject:reject]; } // Thanks to this guard, we won't compile this code when we build for the new architecture. @@ -68,15 +68,15 @@ - (dispatch_queue_t)methodQueue { return [RNQueue getSharedQueue]; } -- (void)enable:(NSDictionary *)configuration resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { +- (void)enable:(NSDictionary *)configuration resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { [self.ddFlagsImplementation enable:configuration resolve:resolve reject:reject]; } -- (void)setEvaluationContext:(NSString *)clientName targetingKey:(NSString *)targetingKey attributes:(NSDictionary *)attributes resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { +- (void)setEvaluationContext:(NSString *)clientName targetingKey:(NSString *)targetingKey attributes:(NSDictionary *)attributes resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { [self.ddFlagsImplementation setEvaluationContext:clientName targetingKey:targetingKey attributes:attributes resolve:resolve reject:reject]; } -- (void)trackEvaluation:(NSString *)clientName key:(NSString *)key resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { - [self.ddFlagsImplementation trackEvaluation:clientName key:key resolve:resolve reject:reject]; +- (void)trackEvaluation:(NSString *)clientName key:(NSString *)key rawFlag:(NSDictionary *)rawFlag targetingKey:(NSString *)targetingKey attributes:(NSDictionary *)attributes resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + [self.ddFlagsImplementation trackEvaluation:clientName key:key rawFlag:rawFlag targetingKey:targetingKey attributes:attributes resolve:resolve reject:reject]; } @end diff --git a/packages/core/ios/Sources/DdFlagsImplementation.swift b/packages/core/ios/Sources/DdFlagsImplementation.swift index 2afe4cf97..d8d56e695 100644 --- a/packages/core/ios/Sources/DdFlagsImplementation.swift +++ b/packages/core/ios/Sources/DdFlagsImplementation.swift @@ -15,9 +15,8 @@ public class DdFlagsImplementation: NSObject { private var clientProviders: [String: () -> FlagsClientProtocol] = [:] - internal init( - core: DatadogCoreProtocol - ) { + /// Exposing this initializer for testing purposes. React Native will always use the default initializer. + internal init(core: DatadogCoreProtocol) { self.core = core } @@ -28,7 +27,7 @@ public class DdFlagsImplementation: NSObject { @objc public func enable(_ configuration: NSDictionary, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) { - if let config = configuration.asConfigurationForFlags() { + if let config = configuration.asFlagsConfiguration() { Flags.enable(with: config) } else { consolePrint("Invalid configuration provided for Flags. Feature initialization skipped.", .error) @@ -54,17 +53,6 @@ public class DdFlagsImplementation: NSObject { return client } - private func parseAttributes(attributes: NSDictionary) -> [String: AnyValue] { - var result: [String: AnyValue] = [:] - for (key, value) in attributes { - guard let stringKey = key as? String else { - continue - } - result[stringKey] = AnyValue.wrap(value) - } - return result - } - @objc public func setEvaluationContext(_ clientName: String, targetingKey: String, attributes: NSDictionary, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { let client = getClient(name: clientName) @@ -72,22 +60,23 @@ public class DdFlagsImplementation: NSObject { return } - let parsedAttributes = parseAttributes(attributes: attributes) - let evaluationContext = FlagsEvaluationContext(targetingKey: targetingKey, attributes: parsedAttributes) + let evaluationContext = buildEvaluationContext(targetingKey: targetingKey, attributes: attributes) client.setEvaluationContext(evaluationContext) { result in switch result { case .success: - guard let flagsDetails = clientInternal.getAllFlagsDetails() else { + guard let flagsSnapshot = clientInternal.getFlagAssignmentsSnapshot() else { reject(nil, "CLIENT_NOT_INITIALIZED", nil) return } - let result = flagsDetails.compactMapValues { details in - details.toSerializedDictionary() - } + let serializedFlagsSnapshot = Dictionary( + uniqueKeysWithValues: flagsSnapshot.map { key, flagAssignment in + (key, flagAssignment.asDictionary(flagKey: key)) + } + ) - resolve(result) + resolve(serializedFlagsSnapshot) case .failure(let error): var errorCode: String switch (error) { @@ -106,14 +95,121 @@ public class DdFlagsImplementation: NSObject { } @objc - public func trackEvaluation(_ clientName: String, key: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) { + public func trackEvaluation(_ clientName: String, key: String, rawFlag: NSDictionary, targetingKey: String, attributes: NSDictionary, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) { guard let client = getClient(name: clientName) as? FlagsClientInternal else { + reject(nil, "CLIENT_NOT_INITIALIZED", nil) + return + } + guard let flagAssignment = rawFlag.asFlagAssignment() else { + reject(nil, "INVALID_FLAG_ASSIGNMENT", nil) return } - client.trackEvaluation(key: key) + let evaluationContext = buildEvaluationContext(targetingKey: targetingKey, attributes: attributes) + + client.trackFlagSnapshotEvaluation(key: key, assignment: flagAssignment, context: evaluationContext) + resolve(nil) } + + /// Construct an `FlagsEvaluationContext` from a targeting key and a dictionary of attributes. + private func buildEvaluationContext(targetingKey: String, attributes: NSDictionary) -> FlagsEvaluationContext { + let dict = attributes as? [String: Any] ?? [:] + + let parsedAttributes = dict.compactMapValues { value in AnyValue.wrap(value) } + + return FlagsEvaluationContext(targetingKey: targetingKey, attributes: parsedAttributes) + } +} + +extension NSDictionary { + func asFlagsConfiguration() -> Flags.Configuration? { + let enabled = object(forKey: "enabled") as? Bool ?? false + + if !enabled { + return nil + } + + // Hard set `gracefulModeEnabled` to `true` because this misconfiguration is handled on JS side. + let gracefulModeEnabled = true + + let trackExposures = object(forKey: "trackExposures") as? Bool + let rumIntegrationEnabled = object(forKey: "rumIntegrationEnabled") as? Bool + + var customFlagsEndpointURL: URL? = nil + if let customFlagsEndpoint = object(forKey: "customFlagsEndpoint") as? String { + customFlagsEndpointURL = URL(string: "\(customFlagsEndpoint)/precompute-assignments" as String) + } + var customExposureEndpointURL: URL? = nil + if let customExposureEndpoint = object(forKey: "customExposureEndpoint") as? String { + customExposureEndpointURL = URL(string: "\(customExposureEndpoint)/api/v2/exposures" as String) + } + + return Flags.Configuration( + gracefulModeEnabled: gracefulModeEnabled, + customFlagsEndpoint: customFlagsEndpointURL, + customExposureEndpoint: customExposureEndpointURL, + trackExposures: trackExposures ?? true, + rumIntegrationEnabled: rumIntegrationEnabled ?? true + ) + } +} + +extension FlagAssignment { + func asDictionary(flagKey: String) -> [String: Any] { + let value = switch self.variation { + case .boolean(let v): v + case .string(let v): v + case .integer(let v): v + case .double(let v): v + case .object(let v): v.unwrap() + case .unknown: NSNull() + } + + return [ + "key": flagKey, + "value": value, + "allocationKey": allocationKey, + "variationKey": variationKey, + "reason": reason, + "doLog": doLog, + // Parity with Android. We don't use the following properties in iOS SDK. + "variationType": "", + "variationValue": "", + "extraLogging": [:], + ] + } +} + +extension NSDictionary { + func asFlagAssignment() -> FlagAssignment? { + guard + let allocationKey = object(forKey: "allocationKey") as? String, + let variationKey = object(forKey: "variationKey") as? String, + let reason = object(forKey: "reason") as? String, + let doLog = object(forKey: "doLog") as? Bool, + let value = object(forKey: "value") + else { + return nil + } + + let variation: FlagAssignment.Variation = switch value { + case let boolValue as Bool: .boolean(boolValue) + case let stringValue as String: .string(stringValue) + case let intValue as Int: .integer(intValue) + case let doubleValue as Double: .double(doubleValue) + case let dictValue as [String: Any]: .object(AnyValue.wrap(dictValue)) + default: .unknown(String(describing: value)) + } + + return FlagAssignment( + allocationKey: allocationKey, + variationKey: variationKey, + variation: variation, + reason: reason, + doLog: doLog + ) + } } extension AnyValue { @@ -158,49 +254,3 @@ extension AnyValue { } } } - -extension FlagDetails { - func toSerializedDictionary() -> [String: Any?] { - let dict: [String: Any?] = [ - "key": key, - "value": getSerializedValue(), - "variant": variant as Any?, - "reason": reason as Any?, - "error": getSerializedError() - ] - - return dict - } - - private func getSerializedValue() -> Any { - if let boolValue = value as? Bool { - return boolValue - } else if let stringValue = value as? String { - return stringValue - } else if let intValue = value as? Int { - return intValue - } else if let doubleValue = value as? Double { - return doubleValue - } else if let anyValue = value as? AnyValue { - return anyValue.unwrap() - } - - // Fallback for unexpected types. - return NSNull() - } - - private func getSerializedError() -> String? { - guard let error = error else { - return nil - } - - switch error { - case .providerNotReady: - return "PROVIDER_NOT_READY" - case .flagNotFound: - return "FLAG_NOT_FOUND" - case .typeMismatch: - return "TYPE_MISMATCH" - } - } -} diff --git a/packages/core/ios/Sources/RNDdSdkConfiguration.swift b/packages/core/ios/Sources/RNDdSdkConfiguration.swift index 6ef79d7d0..2fdd24eb5 100644 --- a/packages/core/ios/Sources/RNDdSdkConfiguration.swift +++ b/packages/core/ios/Sources/RNDdSdkConfiguration.swift @@ -100,37 +100,6 @@ extension NSDictionary { ) } - func asConfigurationForFlags() -> Flags.Configuration? { - let enabled = object(forKey: "enabled") as? Bool ?? false - - if !enabled { - return nil - } - - // Hard set `gracefulModeEnabled` to `true` because this misconfiguration is handled on JS side. - let gracefulModeEnabled = true - - let trackExposures = object(forKey: "trackExposures") as? Bool - let rumIntegrationEnabled = object(forKey: "rumIntegrationEnabled") as? Bool - - var customFlagsEndpointURL: URL? = nil - if let customFlagsEndpoint = object(forKey: "customFlagsEndpoint") as? String { - customFlagsEndpointURL = URL(string: "\(customFlagsEndpoint)/precompute-assignments" as String) - } - var customExposureEndpointURL: URL? = nil - if let customExposureEndpoint = object(forKey: "customExposureEndpoint") as? String { - customExposureEndpointURL = URL(string: "\(customExposureEndpoint)/api/v2/exposures" as String) - } - - return Flags.Configuration( - gracefulModeEnabled: gracefulModeEnabled, - customFlagsEndpoint: customFlagsEndpointURL, - customExposureEndpoint: customExposureEndpointURL, - trackExposures: trackExposures ?? true, - rumIntegrationEnabled: rumIntegrationEnabled ?? true - ) - } - func asCustomEndpoints() -> CustomEndpoints { let rum = object(forKey: "rum") as? NSString let logs = object(forKey: "logs") as? NSString diff --git a/packages/core/src/flags/internal.ts b/packages/core/src/flags/internal.ts index 21a640dc4..5a39dcfcc 100644 --- a/packages/core/src/flags/internal.ts +++ b/packages/core/src/flags/internal.ts @@ -6,13 +6,13 @@ import type { EvaluationContext, FlagDetails } from './types'; export interface FlagCacheEntry { key: string; value: unknown; + allocationKey: string; + variationKey: string; variationType: string; variationValue: string; + reason: string; doLog: boolean; - allocationKey: string; - variationKey: string; extraLogging: Record; - reason: string; } export const flagCacheEntryToFlagDetails = ( From 8b787317ff5ae921b0ed7544be730d82fbae06db Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Thu, 11 Dec 2025 20:08:57 +0200 Subject: [PATCH 45/64] Move extension methods to the general file, add tests for them --- .../com/datadog/reactnative/DdSdkBridgeExt.kt | 96 +++++++ .../com/datadog/reactnative/JSONBridgeExt.kt | 107 -------- .../datadog/reactnative/DdSdkBridgeExtTest.kt | 240 ++++++++++++++++++ 3 files changed, 336 insertions(+), 107 deletions(-) delete mode 100644 packages/core/android/src/main/kotlin/com/datadog/reactnative/JSONBridgeExt.kt diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkBridgeExt.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkBridgeExt.kt index 06841619b..a3023c1e6 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkBridgeExt.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkBridgeExt.kt @@ -242,3 +242,99 @@ internal fun ReadableMap.getDoubleOrNull(key: String): Double? { null } } + +/** + * Converts a [JSONObject] to a [WritableMap]. + */ +internal fun JSONObject.toWritableMap(): WritableMap = this.toMap().toWritableMap() + +/** + * Converts a [JSONObject] to a [Map]. + */ +internal fun JSONObject.toMap(): Map { + val map = mutableMapOf() + val keys = this.keys() + + while (keys.hasNext()) { + val key = keys.next() + val value = this.opt(key) + + map[key] = + when (value) { + null, JSONObject.NULL -> null + is JSONObject -> value.toMap() + is org.json.JSONArray -> value.toList() + else -> value + } + } + + return map +} + +/** + * Converts a [org.json.JSONArray] to a [List]. + */ +internal fun org.json.JSONArray.toList(): List { + val list = mutableListOf() + + for (i in 0 until this.length()) { + val value = this.opt(i) + + list.add( + when (value) { + null, JSONObject.NULL -> null + is JSONObject -> value.toMap() + is org.json.JSONArray -> value.toList() + else -> value + }, + ) + } + + return list +} + +/** + * Converts a [ReadableMap] to a [JSONObject]. + */ +internal fun ReadableMap.toJSONObject(): JSONObject = this.toMap().toJSONObject() + +/** + * Converts a [Map] to a [JSONObject]. + */ +@Suppress("UNCHECKED_CAST") +internal fun Map.toJSONObject(): JSONObject { + val jsonObject = JSONObject() + + for ((key, value) in this) { + jsonObject.put( + key, + when (value) { + is Map<*, *> -> (value as Map).toJSONObject() + is List<*> -> value.toJSONArray() + else -> value + }, + ) + } + + return jsonObject +} + +/** + * Converts a [List] to a [org.json.JSONArray]. + */ +@Suppress("UNCHECKED_CAST") +internal fun List<*>.toJSONArray(): org.json.JSONArray { + val jsonArray = org.json.JSONArray() + + for (value in this) { + jsonArray.put( + when (value) { + is Map<*, *> -> (value as Map).toJSONObject() + is List<*> -> value.toJSONArray() + else -> value + }, + ) + } + + return jsonArray +} diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/JSONBridgeExt.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/JSONBridgeExt.kt deleted file mode 100644 index 219775bba..000000000 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/JSONBridgeExt.kt +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. - * This product includes software developed at Datadog (https://www.datadoghq.com/). - * Copyright 2016-Present Datadog, Inc. - */ - -package com.datadog.reactnative - -import com.facebook.react.bridge.ReadableMap -import com.facebook.react.bridge.WritableMap -import org.json.JSONObject - -/** - * Converts a [JSONObject] to a [WritableMap]. - */ -internal fun JSONObject.toWritableMap(): WritableMap = this.toMap().toWritableMap() - -/** - * Converts a [JSONObject] to a [Map]. - */ -internal fun JSONObject.toMap(): Map { - val map = mutableMapOf() - val keys = this.keys() - - while (keys.hasNext()) { - val key = keys.next() - val value = this.opt(key) - - map[key] = - when (value) { - null, JSONObject.NULL -> null - is JSONObject -> value.toMap() - is org.json.JSONArray -> value.toList() - else -> value - } - } - - return map -} - -/** - * Converts a [org.json.JSONArray] to a [List]. - */ -internal fun org.json.JSONArray.toList(): List { - val list = mutableListOf() - - for (i in 0 until this.length()) { - val value = this.opt(i) - - list.add( - when (value) { - null, JSONObject.NULL -> null - is JSONObject -> value.toMap() - is org.json.JSONArray -> value.toList() - else -> value - }, - ) - } - - return list -} - -/** - * Converts a [ReadableMap] to a [JSONObject]. - */ -internal fun ReadableMap.toJSONObject(): JSONObject = this.toMap().toJSONObject() - -/** - * Converts a [Map] to a [JSONObject]. - */ -@Suppress("UNCHECKED_CAST") -internal fun Map.toJSONObject(): JSONObject { - val jsonObject = JSONObject() - - for ((key, value) in this) { - jsonObject.put( - key, - when (value) { - is Map<*, *> -> (value as Map).toJSONObject() - is List<*> -> value.toJSONArray() - else -> value - }, - ) - } - - return jsonObject -} - -/** - * Converts a [List] to a [org.json.JSONArray]. - */ -@Suppress("UNCHECKED_CAST") -internal fun List<*>.toJSONArray(): org.json.JSONArray { - val jsonArray = org.json.JSONArray() - - for (value in this) { - jsonArray.put( - when (value) { - is Map<*, *> -> (value as Map).toJSONObject() - is List<*> -> value.toJSONArray() - else -> value - }, - ) - } - - return jsonArray -} diff --git a/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkBridgeExtTest.kt b/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkBridgeExtTest.kt index 796936d04..b3b73e03e 100644 --- a/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkBridgeExtTest.kt +++ b/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkBridgeExtTest.kt @@ -14,6 +14,8 @@ import com.facebook.react.bridge.JavaOnlyMap import com.facebook.react.bridge.WritableArray import com.facebook.react.bridge.WritableMap import org.assertj.core.api.Assertions.assertThat +import org.json.JSONArray +import org.json.JSONObject import org.junit.jupiter.api.Test internal class DdSdkBridgeExtTest { @@ -276,6 +278,244 @@ internal class DdSdkBridgeExtTest { assertThat(value).isNull() } + @Test + fun `M do a proper conversion W JSONObject toMap { with raw types }`() { + // Given + val jsonObject = JSONObject().apply { + put("null", JSONObject.NULL) + put("int", 1) + put("long", 2L) + put("double", 3.0) + put("string", "test") + put("boolean", true) + } + + // When + val map = jsonObject.toMap() + + // Then + assertThat(map).hasSize(6) + assertThat(map["null"]).isNull() + assertThat(map["int"]).isEqualTo(1) + assertThat(map["long"]).isEqualTo(2L) + assertThat(map["double"]).isEqualTo(3.0) + assertThat(map["string"]).isEqualTo("test") + assertThat(map["boolean"]).isEqualTo(true) + } + + @Test + fun `M do a proper conversion W JSONObject toMap { with nested objects }`() { + // Given + val nestedObject = JSONObject().apply { + put("nestedKey", "nestedValue") + } + val nestedArray = JSONArray().apply { + put("item1") + put("item2") + } + val jsonObject = JSONObject().apply { + put("object", nestedObject) + put("array", nestedArray) + } + + // When + val map = jsonObject.toMap() + + // Then + assertThat(map).hasSize(2) + assertThat(map["object"]).isInstanceOf(Map::class.java) + assertThat((map["object"] as Map<*, *>)["nestedKey"]).isEqualTo("nestedValue") + assertThat(map["array"]).isInstanceOf(List::class.java) + assertThat((map["array"] as List<*>)).hasSize(2) + assertThat((map["array"] as List<*>)[0]).isEqualTo("item1") + assertThat((map["array"] as List<*>)[1]).isEqualTo("item2") + } + + @Test + fun `M do a proper conversion W JSONObject toWritableMap { with raw types }`() { + // Given + val jsonObject = JSONObject().apply { + put("int", 1) + put("double", 2.0) + put("string", "test") + put("boolean", true) + } + + // When + val writableMap = jsonObject.toWritableMap() + + // Then + assertThat(writableMap.getInt("int")).isEqualTo(1) + assertThat(writableMap.getDouble("double")).isEqualTo(2.0) + assertThat(writableMap.getString("string")).isEqualTo("test") + assertThat(writableMap.getBoolean("boolean")).isTrue() + } + + @Test + fun `M do a proper conversion W JSONArray toList { with raw types }`() { + // Given + val jsonArray = JSONArray().apply { + put(JSONObject.NULL) + put(1) + put(2.0) + put("test") + put(true) + } + + // When + val list = jsonArray.toList() + + // Then + assertThat(list).hasSize(5) + assertThat(list[0]).isNull() + assertThat(list[1]).isEqualTo(1) + assertThat(list[2]).isEqualTo(2.0) + assertThat(list[3]).isEqualTo("test") + assertThat(list[4]).isEqualTo(true) + } + + @Test + fun `M do a proper conversion W JSONArray toList { with nested objects }`() { + // Given + val nestedObject = JSONObject().apply { + put("key", "value") + } + val nestedArray = JSONArray().apply { + put("nested") + } + val jsonArray = JSONArray().apply { + put(nestedObject) + put(nestedArray) + } + + // When + val list = jsonArray.toList() + + // Then + assertThat(list).hasSize(2) + assertThat(list[0]).isInstanceOf(Map::class.java) + assertThat((list[0] as Map<*, *>)["key"]).isEqualTo("value") + assertThat(list[1]).isInstanceOf(List::class.java) + assertThat((list[1] as List<*>)[0]).isEqualTo("nested") + } + + @Test + fun `M do a proper conversion W ReadableMap toJSONObject { with raw types }`() { + // Given + val readableMap = mapOf( + "int" to 1, + "double" to 2.0, + "string" to "test", + "boolean" to true + ).toReadableMap() + + // When + val jsonObject = readableMap.toJSONObject() + + // Then + assertThat(jsonObject.length()).isEqualTo(4) + assertThat(jsonObject.getInt("int")).isEqualTo(1) + assertThat(jsonObject.getDouble("double")).isEqualTo(2.0) + assertThat(jsonObject.getString("string")).isEqualTo("test") + assertThat(jsonObject.getBoolean("boolean")).isTrue() + } + + @Test + fun `M do a proper conversion W ReadableMap toJSONObject { with nested objects }`() { + // Given + val readableMap = mapOf( + "map" to mapOf("nestedKey" to "nestedValue"), + "list" to listOf("item1", "item2") + ).toReadableMap() + + // When + val jsonObject = readableMap.toJSONObject() + + // Then + assertThat(jsonObject.length()).isEqualTo(2) + assertThat(jsonObject.getJSONObject("map").getString("nestedKey")).isEqualTo("nestedValue") + assertThat(jsonObject.getJSONArray("list").length()).isEqualTo(2) + assertThat(jsonObject.getJSONArray("list").getString(0)).isEqualTo("item1") + assertThat(jsonObject.getJSONArray("list").getString(1)).isEqualTo("item2") + } + + @Test + fun `M do a proper conversion W Map toJSONObject { with raw types }`() { + // Given + val map: Map = mapOf( + "int" to 1, + "double" to 2.0, + "string" to "test", + "boolean" to true + ) + + // When + val jsonObject = map.toJSONObject() + + // Then + assertThat(jsonObject.length()).isEqualTo(4) + assertThat(jsonObject.getInt("int")).isEqualTo(1) + assertThat(jsonObject.getDouble("double")).isEqualTo(2.0) + assertThat(jsonObject.getString("string")).isEqualTo("test") + assertThat(jsonObject.getBoolean("boolean")).isTrue() + } + + @Test + fun `M do a proper conversion W Map toJSONObject { with nested objects }`() { + // Given + val map: Map = mapOf( + "nestedMap" to mapOf("key" to "value"), + "nestedList" to listOf(1, 2, 3) + ) + + // When + val jsonObject = map.toJSONObject() + + // Then + assertThat(jsonObject.length()).isEqualTo(2) + assertThat(jsonObject.getJSONObject("nestedMap").getString("key")).isEqualTo("value") + assertThat(jsonObject.getJSONArray("nestedList").length()).isEqualTo(3) + assertThat(jsonObject.getJSONArray("nestedList").getInt(0)).isEqualTo(1) + assertThat(jsonObject.getJSONArray("nestedList").getInt(1)).isEqualTo(2) + assertThat(jsonObject.getJSONArray("nestedList").getInt(2)).isEqualTo(3) + } + + @Test + fun `M do a proper conversion W List toJSONArray { with raw types }`() { + // Given + val list = listOf(null, 1, 2.0, "test", true) + + // When + val jsonArray = list.toJSONArray() + + // Then + assertThat(jsonArray.length()).isEqualTo(5) + assertThat(jsonArray.isNull(0)).isTrue() + assertThat(jsonArray.getInt(1)).isEqualTo(1) + assertThat(jsonArray.getDouble(2)).isEqualTo(2.0) + assertThat(jsonArray.getString(3)).isEqualTo("test") + assertThat(jsonArray.getBoolean(4)).isTrue() + } + + @Test + fun `M do a proper conversion W List toJSONArray { with nested objects }`() { + // Given + val list = listOf( + mapOf("key" to "value"), + listOf("nested1", "nested2") + ) + + // When + val jsonArray = list.toJSONArray() + + // Then + assertThat(jsonArray.length()).isEqualTo(2) + assertThat(jsonArray.getJSONObject(0).getString("key")).isEqualTo("value") + assertThat(jsonArray.getJSONArray(1).length()).isEqualTo(2) + assertThat(jsonArray.getJSONArray(1).getString(0)).isEqualTo("nested1") + assertThat(jsonArray.getJSONArray(1).getString(1)).isEqualTo("nested2") + } + private fun getTestMap(): MutableMap = mutableMapOf( "null" to null, "int" to 1, From b695366f53e4df333b45b572790802df0a5c9dfb Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Thu, 11 Dec 2025 20:09:15 +0200 Subject: [PATCH 46/64] Move PrecomputedFlag from internal packages, minor changes --- .../com/datadog/reactnative/DdFlagsImplementation.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdFlagsImplementation.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdFlagsImplementation.kt index 57e838d5a..e2db4b22a 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdFlagsImplementation.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdFlagsImplementation.kt @@ -12,7 +12,7 @@ import com.datadog.android.api.SdkCore import com.datadog.android.flags.Flags import com.datadog.android.flags.FlagsClient import com.datadog.android.flags.FlagsConfiguration -import com.datadog.android.flags.internal.model.PrecomputedFlag +import com.datadog.android.flags.model.PrecomputedFlag import com.datadog.android.flags.model.EvaluationContext import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReadableMap @@ -203,13 +203,13 @@ private fun convertPrecomputedFlagToMap( return mapOf( "key" to flagKey, "value" to parsedValue, + "allocationKey" to flag.allocationKey, + "variationKey" to flag.variationKey, "variationType" to flag.variationType, "variationValue" to flag.variationValue, + "reason" to flag.reason, "doLog" to flag.doLog, - "allocationKey" to flag.allocationKey, - "variationKey" to flag.variationKey, "extraLogging" to flag.extraLogging.toMap(), - "reason" to flag.reason, ) } From 06ed0cb05f23201ee3734bd177d8aeb9d930c27b Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Thu, 11 Dec 2025 20:28:33 +0200 Subject: [PATCH 47/64] Fix build issues --- .../com/datadog/reactnative/DdSdkBridgeExt.kt | 256 ++++++++++++------ 1 file changed, 170 insertions(+), 86 deletions(-) diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkBridgeExt.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkBridgeExt.kt index a3023c1e6..3cb754a2c 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkBridgeExt.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkBridgeExt.kt @@ -15,16 +15,17 @@ import com.facebook.react.bridge.WritableArray import com.facebook.react.bridge.WritableMap import com.facebook.react.bridge.WritableNativeArray import com.facebook.react.bridge.WritableNativeMap +import org.json.JSONObject +import org.json.JSONArray /** * Converts the [List] to a [WritableNativeArray]. */ -internal fun List<*>.toWritableArray(): WritableArray { - return this.toWritableArray( +internal fun List<*>.toWritableArray(): WritableArray = + this.toWritableArray( createWritableMap = { WritableNativeMap() }, - createWritableArray = { WritableNativeArray() } + createWritableArray = { WritableNativeArray() }, ) -} /** * Converts the [List] to a [WritableArray]. @@ -33,35 +34,64 @@ internal fun List<*>.toWritableArray(): WritableArray { */ internal fun List<*>.toWritableArray( createWritableMap: () -> WritableMap, - createWritableArray: () -> WritableArray + createWritableArray: () -> WritableArray, ): WritableArray { val writableArray = createWritableArray() for (it in iterator()) { when (it) { - null -> writableArray.pushNull() - is Int -> writableArray.pushInt(it) - is Long -> writableArray.pushDouble(it.toDouble()) - is Float -> writableArray.pushDouble(it.toDouble()) - is Double -> writableArray.pushDouble(it) - is String -> writableArray.pushString(it) - is Boolean -> writableArray.pushBoolean(it) - is List<*> -> writableArray.pushArray( - it.toWritableArray( - createWritableMap, - createWritableArray + null -> { + writableArray.pushNull() + } + + is Int -> { + writableArray.pushInt(it) + } + + is Long -> { + writableArray.pushDouble(it.toDouble()) + } + + is Float -> { + writableArray.pushDouble(it.toDouble()) + } + + is Double -> { + writableArray.pushDouble(it) + } + + is String -> { + writableArray.pushString(it) + } + + is Boolean -> { + writableArray.pushBoolean(it) + } + + is List<*> -> { + writableArray.pushArray( + it.toWritableArray( + createWritableMap, + createWritableArray, + ), ) - ) - is Map<*, *> -> writableArray.pushMap( - it.toWritableMap( - createWritableMap, - createWritableArray + } + + is Map<*, *> -> { + writableArray.pushMap( + it.toWritableMap( + createWritableMap, + createWritableArray, + ), ) - ) - else -> Log.e( - javaClass.simpleName, - "toWritableArray(): Unhandled type ${it.javaClass.simpleName} has been ignored" - ) + } + + else -> { + Log.e( + javaClass.simpleName, + "toWritableArray(): Unhandled type ${it.javaClass.simpleName} has been ignored", + ) + } } } @@ -71,12 +101,11 @@ internal fun List<*>.toWritableArray( /** * Converts the [Map] to a [WritableNativeMap]. */ -internal fun Map<*, *>.toWritableMap(): WritableMap { - return this.toWritableMap( +internal fun Map<*, *>.toWritableMap(): WritableMap = + this.toWritableMap( createWritableMap = { WritableNativeMap() }, - createWritableArray = { WritableNativeArray() } + createWritableArray = { WritableNativeArray() }, ) -} /** * Converts the [Map] to a [WritableMap]. @@ -85,38 +114,67 @@ internal fun Map<*, *>.toWritableMap(): WritableMap { */ internal fun Map<*, *>.toWritableMap( createWritableMap: () -> WritableMap, - createWritableArray: () -> WritableArray + createWritableArray: () -> WritableArray, ): WritableMap { val map = createWritableMap() for ((k, v) in iterator()) { val key = (k as? String) ?: k.toString() when (v) { - null -> map.putNull(key) - is Int -> map.putInt(key, v) - is Long -> map.putDouble(key, v.toDouble()) - is Float -> map.putDouble(key, v.toDouble()) - is Double -> map.putDouble(key, v) - is String -> map.putString(key, v) - is Boolean -> map.putBoolean(key, v) - is List<*> -> map.putArray( - key, - v.toWritableArray( - createWritableMap, - createWritableArray + null -> { + map.putNull(key) + } + + is Int -> { + map.putInt(key, v) + } + + is Long -> { + map.putDouble(key, v.toDouble()) + } + + is Float -> { + map.putDouble(key, v.toDouble()) + } + + is Double -> { + map.putDouble(key, v) + } + + is String -> { + map.putString(key, v) + } + + is Boolean -> { + map.putBoolean(key, v) + } + + is List<*> -> { + map.putArray( + key, + v.toWritableArray( + createWritableMap, + createWritableArray, + ), ) - ) - is Map<*, *> -> map.putMap( - key, - v.toWritableMap( - createWritableMap, - createWritableArray + } + + is Map<*, *> -> { + map.putMap( + key, + v.toWritableMap( + createWritableMap, + createWritableArray, + ), ) - ) - else -> Log.e( - javaClass.simpleName, - "toWritableMap(): Unhandled type ${v.javaClass.simpleName} has been ignored" - ) + } + + else -> { + Log.e( + javaClass.simpleName, + "toWritableMap(): Unhandled type ${v.javaClass.simpleName} has been ignored", + ) + } } } @@ -128,20 +186,25 @@ internal fun Map<*, *>.toWritableMap( * such as [List], [Map] and the raw types. */ internal fun ReadableMap.toMap(): Map { - val map = this.toHashMap() - .filterValues { it != null } - .mapValues { it.value!! } - .toMap(HashMap()) + val map = + this + .toHashMap() + .filterValues { it != null } + .mapValues { it.value!! } + .toMap(HashMap()) val iterator = map.keys.iterator() - fun updateMap(key: String, value: Any?) { + fun updateMap( + key: String, + value: Any?, + ) { if (value != null) { map[key] = value } else { map.remove(key) Log.e( javaClass.simpleName, - "toMap(): Cannot convert nested object for key: $key" + "toMap(): Cannot convert nested object for key: $key", ) } } @@ -150,14 +213,21 @@ internal fun ReadableMap.toMap(): Map { val key = iterator.next() try { when (val type = getType(key)) { - ReadableType.Map -> updateMap(key, getMap(key)?.toMap()) - ReadableType.Array -> updateMap(key, getArray(key)?.toList()) + ReadableType.Map -> { + updateMap(key, getMap(key)?.toMap()) + } + + ReadableType.Array -> { + updateMap(key, getArray(key)?.toList()) + } + ReadableType.Null, ReadableType.Boolean, ReadableType.Number, ReadableType.String -> {} + else -> { map.remove(key) Log.e( javaClass.simpleName, - "toMap(): Skipping unhandled type [${type.name}] for key: $key" + "toMap(): Skipping unhandled type [${type.name}] for key: $key", ) } } @@ -166,7 +236,7 @@ internal fun ReadableMap.toMap(): Map { Log.e( javaClass.simpleName, "toMap(): Could not convert object for key: $key", - err + err, ) } } @@ -186,32 +256,48 @@ internal fun ReadableArray.toList(): List<*> { @Suppress("TooGenericExceptionCaught") try { when (val type = getType(i)) { - ReadableType.Null -> list.add(null) - ReadableType.Boolean -> list.add(getBoolean(i)) - ReadableType.Number -> list.add(getDouble(i)) - ReadableType.String -> list.add(getString(i)) + ReadableType.Null -> { + list.add(null) + } + + ReadableType.Boolean -> { + list.add(getBoolean(i)) + } + + ReadableType.Number -> { + list.add(getDouble(i)) + } + + ReadableType.String -> { + list.add(getString(i)) + } + ReadableType.Map -> { // getMap() return type is nullable in previous RN versions @Suppress("USELESS_ELVIS") val readableMap = getMap(i) ?: Arguments.createMap() list.add(readableMap.toMap()) } + ReadableType.Array -> { // getArray() return type is nullable in previous RN versions @Suppress("USELESS_ELVIS") val readableArray = getArray(i) ?: Arguments.createArray() list.add(readableArray.toList()) } - else -> Log.e( - javaClass.simpleName, - "toList(): Unhandled ReadableType: ${type.name}." - ) + + else -> { + Log.e( + javaClass.simpleName, + "toList(): Unhandled ReadableType: ${type.name}.", + ) + } } } catch (err: NullPointerException) { Log.e( javaClass.simpleName, "toList(): Could not convert object at index: $i.", - err + err, ) } } @@ -223,25 +309,23 @@ internal fun ReadableArray.toList(): List<*> { * Returns the boolean for the given key, or null if the entry is * not in the map. */ -internal fun ReadableMap.getBooleanOrNull(key: String): Boolean? { - return if (hasKey(key)) { +internal fun ReadableMap.getBooleanOrNull(key: String): Boolean? = + if (hasKey(key)) { getBoolean(key) } else { null } -} /** * Returns the double for the given key, or null if the entry is * not in the map. */ -internal fun ReadableMap.getDoubleOrNull(key: String): Double? { - return if (hasKey(key)) { +internal fun ReadableMap.getDoubleOrNull(key: String): Double? = + if (hasKey(key)) { getDouble(key) } else { null } -} /** * Converts a [JSONObject] to a [WritableMap]. @@ -263,7 +347,7 @@ internal fun JSONObject.toMap(): Map { when (value) { null, JSONObject.NULL -> null is JSONObject -> value.toMap() - is org.json.JSONArray -> value.toList() + is JSONArray -> value.toList() else -> value } } @@ -272,9 +356,9 @@ internal fun JSONObject.toMap(): Map { } /** - * Converts a [org.json.JSONArray] to a [List]. + * Converts a [JSONArray] to a [List]. */ -internal fun org.json.JSONArray.toList(): List { +internal fun JSONArray.toList(): List { val list = mutableListOf() for (i in 0 until this.length()) { @@ -284,7 +368,7 @@ internal fun org.json.JSONArray.toList(): List { when (value) { null, JSONObject.NULL -> null is JSONObject -> value.toMap() - is org.json.JSONArray -> value.toList() + is JSONArray -> value.toList() else -> value }, ) @@ -320,11 +404,11 @@ internal fun Map.toJSONObject(): JSONObject { } /** - * Converts a [List] to a [org.json.JSONArray]. + * Converts a [List] to a [JSONArray]. */ @Suppress("UNCHECKED_CAST") -internal fun List<*>.toJSONArray(): org.json.JSONArray { - val jsonArray = org.json.JSONArray() +internal fun List<*>.toJSONArray(): JSONArray { + val jsonArray = JSONArray() for (value in this) { jsonArray.put( From d9debbf7a3381efb85f720c074bcf8e9ba5b7b42 Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Mon, 15 Dec 2025 16:53:39 +0200 Subject: [PATCH 48/64] Rename of iOS methods --- packages/core/ios/Sources/DdFlagsImplementation.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/ios/Sources/DdFlagsImplementation.swift b/packages/core/ios/Sources/DdFlagsImplementation.swift index d8d56e695..f53aa1155 100644 --- a/packages/core/ios/Sources/DdFlagsImplementation.swift +++ b/packages/core/ios/Sources/DdFlagsImplementation.swift @@ -65,7 +65,7 @@ public class DdFlagsImplementation: NSObject { client.setEvaluationContext(evaluationContext) { result in switch result { case .success: - guard let flagsSnapshot = clientInternal.getFlagAssignmentsSnapshot() else { + guard let flagsSnapshot = clientInternal.getFlagAssignments() else { reject(nil, "CLIENT_NOT_INITIALIZED", nil) return } @@ -107,7 +107,7 @@ public class DdFlagsImplementation: NSObject { let evaluationContext = buildEvaluationContext(targetingKey: targetingKey, attributes: attributes) - client.trackFlagSnapshotEvaluation(key: key, assignment: flagAssignment, context: evaluationContext) + client.sendFlagEvaluation(key: key, assignment: flagAssignment, context: evaluationContext) resolve(nil) } From 62b258f335442d8810d2b96f624cd7eddd31580e Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Mon, 15 Dec 2025 19:37:18 +0200 Subject: [PATCH 49/64] Remove usage of `_getInternal()` in favor of using `_FlagsInternalProxy` class directly --- .../com/datadog/reactnative/DdFlagsImplementation.kt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdFlagsImplementation.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdFlagsImplementation.kt index e2db4b22a..e06766b38 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdFlagsImplementation.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdFlagsImplementation.kt @@ -9,6 +9,7 @@ package com.datadog.reactnative import com.datadog.android.Datadog import com.datadog.android.api.InternalLogger import com.datadog.android.api.SdkCore +import com.datadog.android.flags._FlagsInternalProxy import com.datadog.android.flags.Flags import com.datadog.android.flags.FlagsClient import com.datadog.android.flags.FlagsConfiguration @@ -66,13 +67,14 @@ class DdFlagsImplementation( promise: Promise, ) { val client = getClient(clientName) + val internalClient = _FlagsInternalProxy(client) // Set the evaluation context. val evaluationContext = buildEvaluationContext(targetingKey, attributes) client.setEvaluationContext(evaluationContext) // Retrieve flags state snapshot. - val flagsSnapshot = client._getInternal()?.getFlagAssignmentsSnapshot() + val flagsSnapshot = internalClient.getFlagAssignmentsSnapshot() // Send the flags state snapshot to React Native. If `flagsSnapshot` is null, the // FlagsClient client is not ready yet. @@ -97,10 +99,11 @@ class DdFlagsImplementation( promise: Promise, ) { val client = getClient(clientName) + val internalClient = _FlagsInternalProxy(client) val precomputedFlag = convertMapToPrecomputedFlag(rawFlag.toMap()) val evaluationContext = buildEvaluationContext(targetingKey, attributes) - client._getInternal()?.trackFlagSnapshotEvaluation(key, precomputedFlag, evaluationContext) + internalClient.trackFlagSnapshotEvaluation(key, precomputedFlag, evaluationContext) promise.resolve(null) } From 62a43dfe38ef90603124dd3d7502abb07c5f293f Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Tue, 16 Dec 2025 16:05:16 +0200 Subject: [PATCH 50/64] Update Android implementation to use UnparsedFlag --- .../reactnative/DdFlagsImplementation.kt | 84 +++++++++---------- 1 file changed, 39 insertions(+), 45 deletions(-) diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdFlagsImplementation.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdFlagsImplementation.kt index e06766b38..453f4a048 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdFlagsImplementation.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdFlagsImplementation.kt @@ -13,11 +13,12 @@ import com.datadog.android.flags._FlagsInternalProxy import com.datadog.android.flags.Flags import com.datadog.android.flags.FlagsClient import com.datadog.android.flags.FlagsConfiguration -import com.datadog.android.flags.model.PrecomputedFlag +import com.datadog.android.flags.model.UnparsedFlag import com.datadog.android.flags.model.EvaluationContext import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReadableMap import org.json.JSONObject +import java.util.Locale class DdFlagsImplementation( private val sdkCore: SdkCore = Datadog.getInstance(), @@ -81,7 +82,7 @@ class DdFlagsImplementation( if (flagsSnapshot != null) { val mapOfMaps = flagsSnapshot.mapValues { (key, flag) -> - convertPrecomputedFlagToMap(key, flag) + convertUnparsedFlagToMap(key, flag) } promise.resolve(mapOfMaps.toWritableMap()) @@ -101,9 +102,9 @@ class DdFlagsImplementation( val client = getClient(clientName) val internalClient = _FlagsInternalProxy(client) - val precomputedFlag = convertMapToPrecomputedFlag(rawFlag.toMap()) + val flag = convertMapToUnparsedFlag(rawFlag.toMap()) val evaluationContext = buildEvaluationContext(targetingKey, attributes) - internalClient.trackFlagSnapshotEvaluation(key, precomputedFlag, evaluationContext) + internalClient.trackFlagSnapshotEvaluation(key, flag, evaluationContext) promise.resolve(null) } @@ -161,51 +162,44 @@ private fun buildEvaluationContext( } /** - * Converts a [PrecomputedFlag] to a [Map] for further React Native bridge transfer. Includes the + * Converts [UnparsedFlag] to [Map] for further React Native bridge transfer. Includes the * flag key and parses the value based on variationType. * * We are using Map instead of WritableMap as an intermediate because it is more handy, and we can * convert to WritableMap right before sending to React Native. */ -private fun convertPrecomputedFlagToMap( +private fun convertUnparsedFlagToMap( flagKey: String, - flag: PrecomputedFlag, + flag: UnparsedFlag, ): Map { // Parse the value based on variationType - val parsedValue: Any = + val parsedValue: Any? = when (flag.variationType) { - "boolean" -> { - flag.variationValue.lowercase().toBooleanStrictOrNull() ?: flag.variationValue + "boolean" -> flag.variationValue.lowercase(Locale.US).toBooleanStrictOrNull() + "string" -> flag.variationValue + "integer" -> flag.variationValue.toIntOrNull() + "number", "float" -> flag.variationValue.toDoubleOrNull() + "object" -> try { + JSONObject(flag.variationValue).toMap() + } catch (e: Exception) { + null } - - "string" -> { - flag.variationValue - } - - "integer" -> { - flag.variationValue.toIntOrNull() ?: flag.variationValue - } - - "number", "float" -> { - flag.variationValue.toDoubleOrNull() ?: flag.variationValue - } - - "object" -> { - try { - JSONObject(flag.variationValue).toMap() - } catch (e: Exception) { - flag.variationValue - } - } - else -> { - flag.variationValue + null } } + if (parsedValue == null) { + InternalLogger.UNBOUND.log( + InternalLogger.Level.ERROR, + InternalLogger.Target.USER, + { "Flag '$flagKey': Failed to parse value '${flag.variationValue}' as '${flag.variationType}'" }, + ) + } + return mapOf( "key" to flagKey, - "value" to parsedValue, + "value" to parsedValue ?: flag.variationValue, "allocationKey" to flag.allocationKey, "variationKey" to flag.variationKey, "variationType" to flag.variationType, @@ -216,17 +210,17 @@ private fun convertPrecomputedFlagToMap( ) } -/** Converts a [Map] to a [PrecomputedFlag]. */ +/** Converts a [Map] to a [UnparsedFlag]. */ @Suppress("UNCHECKED_CAST") -private fun convertMapToPrecomputedFlag(map: Map): PrecomputedFlag = - PrecomputedFlag( - variationType = map["variationType"] as? String ?: "", - variationValue = map["variationValue"] as? String ?: "", - doLog = map["doLog"] as? Boolean ?: false, - allocationKey = map["allocationKey"] as? String ?: "", - variationKey = map["variationKey"] as? String ?: "", - extraLogging = +private fun convertMapToUnparsedFlag(map: Map): UnparsedFlag = + object : UnparsedFlag { + override val variationType: String = map["variationType"] as? String ?: "" + override val variationValue: String = map["variationValue"] as? String ?: "" + override val doLog: Boolean = map["doLog"] as? Boolean ?: false + override val allocationKey: String = map["allocationKey"] as? String ?: "" + override val variationKey: String = map["variationKey"] as? String ?: "" + override val extraLogging: JSONObject = (map["extraLogging"] as? Map)?.toJSONObject() - ?: JSONObject(), - reason = map["reason"] as? String ?: "", - ) + ?: JSONObject() + override val reason: String = map["reason"] as? String ?: "" + } From 00f6af2c7f67ffc2e972abdd7bf97676e899932e Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Mon, 5 Jan 2026 17:08:35 +0200 Subject: [PATCH 51/64] Update Android implementation to use the setEvaluationContext callback --- .../reactnative/DdFlagsImplementation.kt | 43 +++++++++++-------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdFlagsImplementation.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdFlagsImplementation.kt index 453f4a048..21acf16af 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdFlagsImplementation.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdFlagsImplementation.kt @@ -11,7 +11,9 @@ import com.datadog.android.api.InternalLogger import com.datadog.android.api.SdkCore import com.datadog.android.flags._FlagsInternalProxy import com.datadog.android.flags.Flags +import com.datadog.android.flags.EvaluationContextCallback import com.datadog.android.flags.FlagsClient +import com.datadog.android.flags.model.FlagsClientState import com.datadog.android.flags.FlagsConfiguration import com.datadog.android.flags.model.UnparsedFlag import com.datadog.android.flags.model.EvaluationContext @@ -72,23 +74,30 @@ class DdFlagsImplementation( // Set the evaluation context. val evaluationContext = buildEvaluationContext(targetingKey, attributes) - client.setEvaluationContext(evaluationContext) - - // Retrieve flags state snapshot. - val flagsSnapshot = internalClient.getFlagAssignmentsSnapshot() + client.setEvaluationContext(evaluationContext, object : EvaluationContextCallback { + override fun onSuccess() { + val flagsSnapshot = internalClient.getFlagAssignmentsSnapshot() + val serializedFlagsSnapshot = + flagsSnapshot.mapValues { (key, flag) -> + convertUnparsedFlagToMap(key, flag) + }.toWritableMap() + promise.resolve(serializedFlagsSnapshot) + } - // Send the flags state snapshot to React Native. If `flagsSnapshot` is null, the - // FlagsClient client is not ready yet. - if (flagsSnapshot != null) { - val mapOfMaps = - flagsSnapshot.mapValues { (key, flag) -> - convertUnparsedFlagToMap(key, flag) + override fun onFailure(error: Throwable) { + // If network request fails and there are cached flags, return them. + if (client.state.getCurrentState() == FlagsClientState.Stale) { + val flagsSnapshot = internalClient.getFlagAssignmentsSnapshot() + val serializedFlagsSnapshot = + flagsSnapshot.mapValues { (key, flag) -> + convertUnparsedFlagToMap(key, flag) + }.toWritableMap() + promise.resolve(serializedFlagsSnapshot) + } else { + promise.reject("CLIENT_NOT_INITIALIZED", error.message, error) } - - promise.resolve(mapOfMaps.toWritableMap()) - } else { - promise.reject("CLIENT_NOT_INITIALIZED", "CLIENT_NOT_INITIALIZED", null) - } + } + }) } fun trackEvaluation( @@ -181,7 +190,7 @@ private fun convertUnparsedFlagToMap( "number", "float" -> flag.variationValue.toDoubleOrNull() "object" -> try { JSONObject(flag.variationValue).toMap() - } catch (e: Exception) { + } catch (_: Exception) { null } else -> { @@ -199,7 +208,7 @@ private fun convertUnparsedFlagToMap( return mapOf( "key" to flagKey, - "value" to parsedValue ?: flag.variationValue, + "value" to (parsedValue ?: flag.variationValue), "allocationKey" to flag.allocationKey, "variationKey" to flag.variationKey, "variationType" to flag.variationType, From 3f5b5c9f47b04df0bd61bb77ae5df2cf429d382b Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Wed, 7 Jan 2026 16:36:04 +0200 Subject: [PATCH 52/64] FFL-1460 Update tests to accomodate new FlagsClient changes --- packages/core/__mocks__/react-native.ts | 45 +- .../ios/Sources/DdFlagsImplementation.swift | 4 +- packages/core/ios/Tests/DdFlagsTests.swift | 458 +++++++++++------- .../src/__tests__/DdSdkReactNative.test.tsx | 8 - packages/core/src/flags/FlagsClient.ts | 126 ++--- .../src/flags/__tests__/DatadogFlags.test.ts | 5 +- .../src/flags/__tests__/FlagsClient.test.ts | 314 ++++++++---- packages/core/src/flags/internal.ts | 2 +- packages/core/src/flags/types.ts | 2 +- 9 files changed, 577 insertions(+), 387 deletions(-) diff --git a/packages/core/__mocks__/react-native.ts b/packages/core/__mocks__/react-native.ts index 336e6be71..485863427 100644 --- a/packages/core/__mocks__/react-native.ts +++ b/packages/core/__mocks__/react-native.ts @@ -5,6 +5,7 @@ */ import type { + DdNativeFlagsType, DdNativeSdkType, DdNativeLogsType } from '../src/nativeModulesTypes'; @@ -158,45 +159,11 @@ actualRN.NativeModules.DdRum = { ) as jest.MockedFunction }; -actualRN.NativeModules.DdFlags = { - enable: jest.fn().mockImplementation(() => Promise.resolve()), - setEvaluationContext: jest.fn().mockImplementation(() => Promise.resolve()), - getBooleanDetails: jest.fn().mockImplementation(() => - Promise.resolve({ - key: 'test-boolean-flag', - value: true, - variant: 'true', - reason: 'STATIC', - error: null - }) - ), - getStringDetails: jest.fn().mockImplementation(() => - Promise.resolve({ - key: 'test-string-flag', - value: 'hello world', - variant: 'hello world', - reason: 'STATIC', - error: null - }) - ), - getNumberDetails: jest.fn().mockImplementation(() => - Promise.resolve({ - key: 'test-number-flag', - value: 6, - variant: '6', - reason: 'STATIC', - error: null - }) - ), - getObjectDetails: jest.fn().mockImplementation(() => - Promise.resolve({ - key: 'test-object-flag', - value: { hello: 'world' }, - variant: 'hello world', - reason: 'STATIC', - error: null - }) - ) +const DdFlags: DdNativeFlagsType = { + enable: jest.fn(() => Promise.resolve()), + setEvaluationContext: jest.fn(() => Promise.resolve({})), + trackEvaluation: jest.fn(() => Promise.resolve()) }; +actualRN.NativeModules.DdFlags = DdFlags; module.exports = actualRN; diff --git a/packages/core/ios/Sources/DdFlagsImplementation.swift b/packages/core/ios/Sources/DdFlagsImplementation.swift index f53aa1155..d2b8e1793 100644 --- a/packages/core/ios/Sources/DdFlagsImplementation.swift +++ b/packages/core/ios/Sources/DdFlagsImplementation.swift @@ -13,7 +13,7 @@ import DatadogFlags public class DdFlagsImplementation: NSObject { private let core: DatadogCoreProtocol - private var clientProviders: [String: () -> FlagsClientProtocol] = [:] + internal var clientProviders: [String: () -> FlagsClientProtocol] = [:] /// Exposing this initializer for testing purposes. React Native will always use the default initializer. internal init(core: DatadogCoreProtocol) { @@ -156,7 +156,7 @@ extension NSDictionary { } extension FlagAssignment { - func asDictionary(flagKey: String) -> [String: Any] { + public func asDictionary(flagKey: String) -> [String: Any] { let value = switch self.variation { case .boolean(let v): v case .string(let v): v diff --git a/packages/core/ios/Tests/DdFlagsTests.swift b/packages/core/ios/Tests/DdFlagsTests.swift index 7402d4d23..45849c8d8 100644 --- a/packages/core/ios/Tests/DdFlagsTests.swift +++ b/packages/core/ios/Tests/DdFlagsTests.swift @@ -6,13 +6,17 @@ import XCTest import DatadogCore +@_spi(Internal) import DatadogFlags import DatadogInternal -@testable import DatadogSDKReactNative +@_spi(Internal) +@testable +import DatadogSDKReactNative class DdFlagsTests: XCTestCase { private var core: FlagsTestCore! + private var implementation: DdFlagsImplementation! override func setUp() { super.setUp() @@ -20,89 +24,229 @@ class DdFlagsTests: XCTestCase { core = FlagsTestCore() CoreRegistry.register(default: core) Flags.enable(in: core) + implementation = DdFlagsImplementation(core: core) } override func tearDown() { CoreRegistry.unregisterDefault() super.tearDown() } - + + // MARK: - Bridge Tests + + func testEnable() { + let expectation = self.expectation(description: "Enable resolves") + implementation.enable(["enabled": true], resolve: { _ in + expectation.fulfill() + }, reject: { _, _, _ in + XCTFail("Should not reject") + }) + waitForExpectations(timeout: 1, handler: nil) + } + + func testSetEvaluationContextSuccess() { + let expectation = self.expectation(description: "SetEvaluationContext resolves with flags") + + let mockClient = MockFlagsClient() + mockClient.assignments = [ + "flag1": FlagAssignment( + allocationKey: "a", + variationKey: "v", + variation: .boolean(true), + reason: "r", + doLog: true + ) + ] + + implementation.clientProviders["test_client"] = { mockClient } + + implementation.setEvaluationContext("test_client", targetingKey: "user_1", attributes: ["tier": "pro"], resolve: { result in + guard let flags = result as? [String: Any] else { + XCTFail("Expected dictionary result") + expectation.fulfill() + return + } + + XCTAssertNotNil(flags["flag1"]) + if let flag = flags["flag1"] as? [String: Any] { + XCTAssertEqual(flag["value"] as? Bool, true) + } else { + XCTFail("Expected flag1 dictionary") + } + + expectation.fulfill() + }, reject: { code, message, error in + XCTFail("Should not reject: \(String(describing: code))") + expectation.fulfill() + }) + + waitForExpectations(timeout: 1.0, handler: nil) + + XCTAssertEqual(mockClient.lastEvaluationContext?.targetingKey, "user_1") + XCTAssertEqual(mockClient.lastEvaluationContext?.attributes["tier"], .string("pro")) + } + + func testSetEvaluationContextClientNotInitialized() { + let expectation = self.expectation(description: "SetEvaluationContext rejects when client returns nil assignments") + + let mockClient = MockFlagsClient() + mockClient.assignments = nil // Simulates uninitialized state + + implementation.clientProviders["test_client"] = { mockClient } + + implementation.setEvaluationContext("test_client", targetingKey: "user_1", attributes: [:], resolve: { result in + XCTFail("Should not resolve") + expectation.fulfill() + }, reject: { code, message, error in + XCTAssertEqual(message, "CLIENT_NOT_INITIALIZED") + expectation.fulfill() + }) + + waitForExpectations(timeout: 1.0, handler: nil) + } + + func testSetEvaluationContextFailure() { + let expectation = self.expectation(description: "SetEvaluationContext rejects with error") + + let mockClient = MockFlagsClient() + mockClient.errorToReturn = .networkError(NSError(domain: "test_domain", code: 400, userInfo: nil)) + + implementation.clientProviders["test_client"] = { mockClient } + + implementation.setEvaluationContext("test_client", targetingKey: "user_1", attributes: [:], resolve: { result in + XCTFail("Should not resolve") + expectation.fulfill() + }, reject: { code, message, error in + XCTAssertEqual(message, "NETWORK_ERROR") + expectation.fulfill() + }) + + waitForExpectations(timeout: 1.0, handler: nil) + } + + func testTrackEvaluation() { + let expectation = self.expectation(description: "TrackEvaluation resolves") + + let mockClient = MockFlagsClient() + implementation.clientProviders["test_client"] = { mockClient } + + let rawFlag: NSDictionary = [ + "allocationKey": "alloc", + "variationKey": "var", + "reason": "reason", + "doLog": true, + "value": true + ] + + implementation.trackEvaluation("test_client", key: "feature_flag", rawFlag: rawFlag, targetingKey: "user_1", attributes: [:], resolve: { result in + XCTAssertNil(result) + expectation.fulfill() + }, reject: { code, message, error in + XCTFail("Should not reject: \(String(describing: code))") + expectation.fulfill() + }) + + waitForExpectations(timeout: 1.0, handler: nil) + + XCTAssertEqual(mockClient.trackedEvaluation?.key, "feature_flag") + XCTAssertEqual(mockClient.trackedEvaluation?.assignment.variationKey, "var") + } + + func testTrackEvaluationWithInvalidFlag() { + let expectation = self.expectation(description: "TrackEvaluation rejects invalid flag") + + let invalidFlag: NSDictionary = [ + "allocationKey": "alloc" + // Missing required fields + ] + + implementation.trackEvaluation("test_client", key: "feature_flag", rawFlag: invalidFlag, targetingKey: "user_1", attributes: [:], resolve: { result in + XCTFail("Should not resolve") + expectation.fulfill() + }, reject: { code, message, error in + XCTAssertEqual(message, "INVALID_FLAG_ASSIGNMENT") + expectation.fulfill() + }) + + waitForExpectations(timeout: 1.0, handler: nil) + } + // MARK: - AnyValue Tests - + func testAnyValueWrapUnwrapNull() { let original: Any = NSNull() let wrapped = AnyValue.wrap(original) - + if case .null = wrapped { XCTAssertTrue(true) } else { XCTFail("Expected .null, got \(wrapped)") } - + let unwrapped = wrapped.unwrap() XCTAssertTrue(unwrapped is NSNull) } - + func testAnyValueWrapUnwrapString() { let original = "test string" let wrapped = AnyValue.wrap(original) - + if case .string(let value) = wrapped { XCTAssertEqual(value, original) } else { XCTFail("Expected .string, got \(wrapped)") } - + let unwrapped = wrapped.unwrap() as? String XCTAssertEqual(unwrapped, original) } - + func testAnyValueWrapUnwrapBool() { let original = true let wrapped = AnyValue.wrap(original) - + if case .bool(let value) = wrapped { XCTAssertEqual(value, original) } else { XCTFail("Expected .bool, got \(wrapped)") } - + let unwrapped = wrapped.unwrap() as? Bool XCTAssertEqual(unwrapped, original) } - + func testAnyValueWrapUnwrapInt() { let original = 42 let wrapped = AnyValue.wrap(original) - + if case .int(let value) = wrapped { XCTAssertEqual(value, original) } else { XCTFail("Expected .int, got \(wrapped)") } - + let unwrapped = wrapped.unwrap() as? Int XCTAssertEqual(unwrapped, original) } - + func testAnyValueWrapUnwrapDouble() { let original = 3.14 let wrapped = AnyValue.wrap(original) - + if case .double(let value) = wrapped { XCTAssertEqual(value, original) } else { XCTFail("Expected .double, got \(wrapped)") } - + let unwrapped = wrapped.unwrap() as? Double XCTAssertEqual(unwrapped, original) } - + func testAnyValueWrapUnwrapDictionary() { let original: [String: Any] = ["key": "value", "number": 1] let wrapped = AnyValue.wrap(original) - + if case .dictionary(let dict) = wrapped { XCTAssertEqual(dict.count, 2) if let val = dict["key"], case .string(let s) = val { @@ -118,16 +262,16 @@ class DdFlagsTests: XCTestCase { } else { XCTFail("Expected .dictionary, got \(wrapped)") } - + let unwrapped = wrapped.unwrap() as? [String: Any] XCTAssertEqual(unwrapped?["key"] as? String, "value") XCTAssertEqual(unwrapped?["number"] as? Int, 1) } - + func testAnyValueWrapUnwrapArray() { let original: [Any] = ["value", 1] let wrapped = AnyValue.wrap(original) - + if case .array(let array) = wrapped { XCTAssertEqual(array.count, 2) if case .string(let s) = array[0] { @@ -143,7 +287,7 @@ class DdFlagsTests: XCTestCase { } else { XCTFail("Expected .array, got \(wrapped)") } - + let unwrapped = wrapped.unwrap() as? [Any] XCTAssertEqual(unwrapped?[0] as? String, "value") XCTAssertEqual(unwrapped?[1] as? Int, 1) @@ -153,195 +297,143 @@ class DdFlagsTests: XCTestCase { struct UnknownType {} let original = UnknownType() let wrapped = AnyValue.wrap(original) - + if case .null = wrapped { XCTAssertTrue(true) } else { XCTFail("Expected .null for unknown type, got \(wrapped)") } } - - // MARK: - FlagDetails Tests - - func testFlagDetailsToSerializedDictionarySuccess() { - let details = FlagDetails( - key: "test_flag", - value: "test_value", - variant: "control", - reason: "targeting_match", - error: nil - ) - - let serialized = details.toSerializedDictionary() - - XCTAssertEqual(serialized["key"] as? String, "test_flag") - XCTAssertEqual(serialized["value"] as? String, "test_value") - XCTAssertEqual(serialized["variant"] as? String, "control") - XCTAssertEqual(serialized["reason"] as? String, "targeting_match") - XCTAssertNil(serialized["error"] as? String) - } - - func testFlagDetailsToSerializedDictionaryWithError() { - let details = FlagDetails( - key: "test_flag", - value: false, - variant: nil, - reason: nil, - error: .flagNotFound - ) - - let serialized = details.toSerializedDictionary() - - XCTAssertEqual(serialized["key"] as? String, "test_flag") - XCTAssertTrue(serialized["value"] as? Bool != nil) - XCTAssertNil(serialized["variant"] as? String) - XCTAssertNil(serialized["reason"] as? String) - XCTAssertEqual(serialized["error"] as? String, "FLAG_NOT_FOUND") - } - - func testFlagDetailsToSerializedDictionaryWithOtherErrors() { - let errorCases: [(FlagEvaluationError, String)] = [ - (.providerNotReady, "PROVIDER_NOT_READY"), - (.typeMismatch, "TYPE_MISMATCH"), - (.flagNotFound, "FLAG_NOT_FOUND") + + // MARK: - Configuration Tests + + func testConfigurationParsing() { + let configDict: NSDictionary = [ + "enabled": true, + "trackExposures": false, + "rumIntegrationEnabled": false, + "customFlagsEndpoint": "https://flags.example.com", + "customExposureEndpoint": "https://exposure.example.com" ] - - for (error, expectedString) in errorCases { - let details = FlagDetails( - key: "key", - value: false, - variant: nil, - reason: nil, - error: error - ) - let serialized = details.toSerializedDictionary() - XCTAssertEqual(serialized["error"] as? String, expectedString) - } - } - - func testFlagDetailsToSerializedDictionaryWithDifferentValueTypes() { - let boolDetails = FlagDetails(key: "k", value: true, variant: nil, reason: nil, error: nil) - XCTAssertEqual(boolDetails.toSerializedDictionary()["value"] as? Bool, true) - - let intDetails = FlagDetails(key: "k", value: 123, variant: nil, reason: nil, error: nil) - XCTAssertEqual(intDetails.toSerializedDictionary()["value"] as? Int, 123) - - let doubleDetails = FlagDetails(key: "k", value: 12.34, variant: nil, reason: nil, error: nil) - XCTAssertEqual(doubleDetails.toSerializedDictionary()["value"] as? Double, 12.34) - - let anyValueDetails = FlagDetails(key: "k", value: AnyValue.string("s"), variant: nil, reason: nil, error: nil) - XCTAssertEqual(anyValueDetails.toSerializedDictionary()["value"] as? String, "s") - - struct Unknown: Equatable {} - let unknownDetails = FlagDetails(key: "k", value: Unknown(), variant: nil, reason: nil, error: nil) - XCTAssertTrue(unknownDetails.toSerializedDictionary()["value"] as? NSNull != nil) + + let config = configDict.asFlagsConfiguration() + + XCTAssertNotNil(config) + XCTAssertEqual(config?.trackExposures, false) + XCTAssertEqual(config?.rumIntegrationEnabled, false) + XCTAssertEqual(config?.customFlagsEndpoint?.absoluteString, "https://flags.example.com/precompute-assignments") + XCTAssertEqual(config?.customExposureEndpoint?.absoluteString, "https://exposure.example.com/api/v2/exposures") } - - // MARK: - get*Details Tests - - func testGetBooleanDetails() { - let implementation = DdFlagsImplementation() - - let expectation = self.expectation(description: "Resolution called") - implementation.getBooleanDetails("default", key: "test_key", defaultValue: true, resolve: { result in - guard let dict = result as? [String: Any] else { - XCTFail("Expected dictionary result") - expectation.fulfill() - return - } - XCTAssertEqual(dict["value"] as? Bool, true) - expectation.fulfill() - }, reject: { _, _, _ in - XCTFail("Should not reject") - expectation.fulfill() - }) - - waitForExpectations(timeout: 1, handler: nil) + + func testConfigurationParsingDefaults() { + let configDict: NSDictionary = ["enabled": true] + let config = configDict.asFlagsConfiguration() + + XCTAssertNotNil(config) + XCTAssertEqual(config?.trackExposures, true) + XCTAssertEqual(config?.rumIntegrationEnabled, true) + XCTAssertNil(config?.customFlagsEndpoint) + XCTAssertNil(config?.customExposureEndpoint) } - func testGetStringDetails() { - let implementation = DdFlagsImplementation() - - let expectation = self.expectation(description: "Resolution called") - implementation.getStringDetails("default", key: "test_key", defaultValue: "default", resolve: { result in - guard let dict = result as? [String: Any] else { - XCTFail("Expected dictionary result") - expectation.fulfill() - return - } - XCTAssertEqual(dict["value"] as? String, "default") - expectation.fulfill() - }, reject: { _, _, _ in - XCTFail("Should not reject") - expectation.fulfill() - }) - - waitForExpectations(timeout: 1, handler: nil) + func testConfigurationParsingDisabled() { + let configDict: NSDictionary = ["enabled": false] + let config = configDict.asFlagsConfiguration() + XCTAssertNil(config) } - func testGetNumberDetails() { - let implementation = DdFlagsImplementation() - - let expectation = self.expectation(description: "Resolution called") - implementation.getNumberDetails("default", key: "test_key", defaultValue: 123.45, resolve: { result in - guard let dict = result as? [String: Any] else { - XCTFail("Expected dictionary result") - expectation.fulfill() - return - } - XCTAssertEqual(dict["value"] as? Double, 123.45) - expectation.fulfill() - }, reject: { _, _, _ in - XCTFail("Should not reject") - expectation.fulfill() - }) - - waitForExpectations(timeout: 1, handler: nil) + // MARK: - FlagAssignment Tests + + func testFlagAssignmentToDictionary() { + let assignment = FlagAssignment( + allocationKey: "alloc", + variationKey: "var", + variation: .boolean(true), + reason: "reason", + doLog: true + ) + + let dict = assignment.asDictionary(flagKey: "flag1") + + XCTAssertEqual(dict["key"] as? String, "flag1") + XCTAssertEqual(dict["value"] as? Bool, true) + XCTAssertEqual(dict["allocationKey"] as? String, "alloc") + XCTAssertEqual(dict["variationKey"] as? String, "var") + XCTAssertEqual(dict["reason"] as? String, "reason") + XCTAssertEqual(dict["doLog"] as? Bool, true) + // Check Android parity fields + XCTAssertEqual(dict["variationType"] as? String, "") + XCTAssertEqual(dict["variationValue"] as? String, "") + XCTAssertNotNil(dict["extraLogging"] as? [String: Any]) } - func testGetObjectDetails() { - let implementation = DdFlagsImplementation(core: core) - let defaultValue: [String: Any] = ["foo": "bar"] - - let expectation = self.expectation(description: "Resolution called") - implementation.getObjectDetails("default", key: "test_key", defaultValue: defaultValue, resolve: { result in - guard let dict = result as? [String: Any] else { - XCTFail("Expected dictionary result") - expectation.fulfill() - return - } - guard let value = dict["value"] as? [String: Any] else { - XCTFail("Expected dictionary value") - expectation.fulfill() - return - } - XCTAssertEqual(value["foo"] as? String, "bar") - expectation.fulfill() - }, reject: { _, _, _ in - XCTFail("Should not reject") - expectation.fulfill() - }) - - waitForExpectations(timeout: 1, handler: nil) + func testDictionaryToFlagAssignment() { + let dict: NSDictionary = [ + "allocationKey": "alloc", + "variationKey": "var", + "reason": "reason", + "doLog": true, + "value": "string_value" + ] + + let assignment = dict.asFlagAssignment() + + XCTAssertNotNil(assignment) + XCTAssertEqual(assignment?.allocationKey, "alloc") + XCTAssertEqual(assignment?.variationKey, "var") + if case .string(let v) = assignment?.variation { + XCTAssertEqual(v, "string_value") + } else { + XCTFail("Expected string variation") + } } } private class FlagsTestCore: DatadogCoreProtocol { private var features: [String: DatadogFeature] = [:] - + func register(feature: T) throws where T : DatadogFeature { features[T.name] = feature } - + func feature(named name: String, type: T.Type) -> T? { return features[name] as? T } - + func scope(for featureType: T.Type) -> any FeatureScope where T : DatadogFeature { return NOPFeatureScope() } - + func send(message: FeatureMessage, else fallback: @escaping () -> Void) {} func set(context: @escaping () -> Context?) where Context: AdditionalContext {} func mostRecentModifiedFileAt(before: Date) throws -> Date? { return nil } } + +private class MockFlagsClient: FlagsClientProtocol, FlagsClientInternal { + func getDetails(key: String, defaultValue: T) -> DatadogFlags.FlagDetails where T : DatadogFlags.FlagValue, T : Equatable { + return FlagDetails(key: key, value: defaultValue, variant: nil, reason: nil, error: nil) + } + + var assignments: [String: FlagAssignment]? = [:] + var errorToReturn: FlagsError? + + var lastEvaluationContext: FlagsEvaluationContext? + var trackedEvaluation: (key: String, assignment: FlagAssignment, context: FlagsEvaluationContext)? + + func setEvaluationContext(_ context: DatadogFlags.FlagsEvaluationContext, completion: @escaping (Result) -> Void) { + lastEvaluationContext = context + if let error = errorToReturn { + completion(.failure(error)) + } else { + completion(.success(())) + } + } + + func getFlagAssignments() -> [String: DatadogFlags.FlagAssignment]? { + return assignments + } + + func sendFlagEvaluation(key: String, assignment: DatadogFlags.FlagAssignment, context: DatadogFlags.FlagsEvaluationContext) { + trackedEvaluation = (key, assignment, context) + } +} diff --git a/packages/core/src/__tests__/DdSdkReactNative.test.tsx b/packages/core/src/__tests__/DdSdkReactNative.test.tsx index 74c2637b6..5e6f8c447 100644 --- a/packages/core/src/__tests__/DdSdkReactNative.test.tsx +++ b/packages/core/src/__tests__/DdSdkReactNative.test.tsx @@ -28,14 +28,6 @@ import { version as sdkVersion } from '../version'; jest.mock('../InternalLog'); -jest.mock('../flags/DatadogFlags', () => { - return { - DatadogFlags: { - enable: jest.fn().mockResolvedValue(undefined) - } - }; -}); - jest.mock( '../rum/instrumentation/interactionTracking/DdRumUserInteractionTracking', () => { diff --git a/packages/core/src/flags/FlagsClient.ts b/packages/core/src/flags/FlagsClient.ts index d631aa1bc..3e78adc55 100644 --- a/packages/core/src/flags/FlagsClient.ts +++ b/packages/core/src/flags/FlagsClient.ts @@ -32,7 +32,7 @@ export class FlagsClient { /** * Sets the evaluation context for the client. * - * Should be called before evaluating any flags. + * Should be called before evaluating any flags. Otherwise, the client will fall back to serving default flag values. * * @param context The evaluation context to associate with the current session. * @@ -59,7 +59,7 @@ export class FlagsClient { const result = await this.nativeFlags.setEvaluationContext( this.clientName, processedContext.targetingKey, - processedContext.attributes + processedContext.attributes ?? {} ); this._evaluationContext = processedContext; @@ -74,66 +74,6 @@ export class FlagsClient { } }; - /** - * Returns the value of a boolean feature flag. - * - * @param key The key of the flag to evaluate. - * @param defaultValue The value to return if the flag is not found or evaluation fails. - * - * @example - * ```ts - * const isNewFeatureEnabled = flagsClient.getBooleanValue('new-feature-enabled', false); - * ``` - */ - getBooleanValue = (key: string, defaultValue: boolean): boolean => { - return this.getBooleanDetails(key, defaultValue).value; - }; - - /** - * Returns the value of a string feature flag. - * - * @param key The key of the flag to evaluate. - * @param defaultValue The value to return if the flag is not found or evaluation fails. - * - * @example - * ```ts - * const appTheme = flagsClient.getStringValue('app-theme', 'light'); - * ``` - */ - getStringValue = (key: string, defaultValue: string): string => { - return this.getStringDetails(key, defaultValue).value; - }; - - /** - * Returns the value of a number feature flag. - * - * @param key The key of the flag to evaluate. - * @param defaultValue The value to return if the flag is not found or evaluation fails. - * - * @example - * ```ts - * const ctaButtonSize = flagsClient.getNumberValue('cta-button-size', 16); - * ``` - */ - getNumberValue = (key: string, defaultValue: number): number => { - return this.getNumberDetails(key, defaultValue).value; - }; - - /** - * Returns the value of an object feature flag. - * - * @param key The key of the flag to evaluate. - * @param defaultValue The value to return if the flag is not found or evaluation fails. - * - * @example - * ```ts - * const pageCalloutOptions = flagsClient.getObjectValue('page-callout', { color: 'purple', text: 'Woof!' }); - * ``` - */ - getObjectValue = (key: string, defaultValue: ObjectValue): ObjectValue => { - return this.getObjectDetails(key, defaultValue).value; - }; - private getDetails = (key: string, defaultValue: T): FlagDetails => { // Check whether the evaluation context has already been set. if (!this._evaluationContext) { @@ -173,7 +113,7 @@ export class FlagsClient { key, flagCacheEntry, this._evaluationContext.targetingKey, - this._evaluationContext.attributes + this._evaluationContext.attributes ?? {} ); return details; @@ -270,4 +210,64 @@ export class FlagsClient { return this.getDetails(key, defaultValue); }; + + /** + * Returns the value of a boolean feature flag. + * + * @param key The key of the flag to evaluate. + * @param defaultValue The value to return if the flag is not found or evaluation fails. + * + * @example + * ```ts + * const isNewFeatureEnabled = flagsClient.getBooleanValue('new-feature-enabled', false); + * ``` + */ + getBooleanValue = (key: string, defaultValue: boolean): boolean => { + return this.getBooleanDetails(key, defaultValue).value; + }; + + /** + * Returns the value of a string feature flag. + * + * @param key The key of the flag to evaluate. + * @param defaultValue The value to return if the flag is not found or evaluation fails. + * + * @example + * ```ts + * const appTheme = flagsClient.getStringValue('app-theme', 'light'); + * ``` + */ + getStringValue = (key: string, defaultValue: string): string => { + return this.getStringDetails(key, defaultValue).value; + }; + + /** + * Returns the value of a number feature flag. + * + * @param key The key of the flag to evaluate. + * @param defaultValue The value to return if the flag is not found or evaluation fails. + * + * @example + * ```ts + * const ctaButtonSize = flagsClient.getNumberValue('cta-button-size', 16); + * ``` + */ + getNumberValue = (key: string, defaultValue: number): number => { + return this.getNumberDetails(key, defaultValue).value; + }; + + /** + * Returns the value of an object feature flag. + * + * @param key The key of the flag to evaluate. + * @param defaultValue The value to return if the flag is not found or evaluation fails. + * + * @example + * ```ts + * const pageCalloutOptions = flagsClient.getObjectValue('page-callout', { color: 'purple', text: 'Woof!' }); + * ``` + */ + getObjectValue = (key: string, defaultValue: ObjectValue): ObjectValue => { + return this.getObjectDetails(key, defaultValue).value; + }; } diff --git a/packages/core/src/flags/__tests__/DatadogFlags.test.ts b/packages/core/src/flags/__tests__/DatadogFlags.test.ts index 7e579d533..e1315fbe3 100644 --- a/packages/core/src/flags/__tests__/DatadogFlags.test.ts +++ b/packages/core/src/flags/__tests__/DatadogFlags.test.ts @@ -23,7 +23,10 @@ describe('DatadogFlags', () => { beforeEach(() => { jest.clearAllMocks(); // Reset state of DatadogFlags instance. - Object.assign(DatadogFlags, { isFeatureEnabled: false }); + Object.assign(DatadogFlags, { + isFeatureEnabled: false, + clients: {} + }); }); describe('Initialization', () => { diff --git a/packages/core/src/flags/__tests__/FlagsClient.test.ts b/packages/core/src/flags/__tests__/FlagsClient.test.ts index 8fd535d8d..8aa1f2b69 100644 --- a/packages/core/src/flags/__tests__/FlagsClient.test.ts +++ b/packages/core/src/flags/__tests__/FlagsClient.test.ts @@ -8,14 +8,62 @@ import { NativeModules } from 'react-native'; import { InternalLog } from '../../InternalLog'; import { SdkVerbosity } from '../../SdkVerbosity'; -import { BufferSingleton } from '../../sdk/DatadogProvider/Buffer/BufferSingleton'; import { DatadogFlags } from '../DatadogFlags'; +jest.spyOn(NativeModules.DdFlags, 'setEvaluationContext').mockResolvedValue({ + 'test-boolean-flag': { + key: 'test-boolean-flag', + value: true, + allocationKey: '', + variationKey: 'true', + reason: 'STATIC', + doLog: true, + // Internal fields for Android. + variationType: '', + variationValue: '', + extraLogging: {} + }, + 'test-string-flag': { + key: 'test-string-flag', + value: 'hello world', + allocationKey: '', + variationKey: 'Hello World', + reason: 'STATIC', + doLog: true, + // Internal fields for Android. + variationType: '', + variationValue: '', + extraLogging: {} + }, + 'test-number-flag': { + key: 'test-number-flag', + value: 42, + allocationKey: '', + variationKey: '42', + reason: 'STATIC', + doLog: true, + // Internal fields for Android. + variationType: '', + variationValue: '', + extraLogging: {} + }, + 'test-object-flag': { + key: 'test-object-flag', + value: { greeting: 'Greeting from the native side!' }, + allocationKey: '', + variationKey: 'Native Greeting', + reason: 'STATIC', + doLog: true, + // Internal fields for Android. + variationType: '', + variationValue: '', + extraLogging: {} + } +}); + jest.mock('../../InternalLog', () => { return { - InternalLog: { - log: jest.fn() - }, + InternalLog: { log: jest.fn() }, DATADOG_MESSAGE_PREFIX: 'DATADOG:' }; }); @@ -23,7 +71,12 @@ jest.mock('../../InternalLog', () => { describe('FlagsClient', () => { beforeEach(async () => { jest.clearAllMocks(); - BufferSingleton.onInitialization(); + + // Reset state of the global DatadogFlags instance. + Object.assign(DatadogFlags, { + isFeatureEnabled: false, + clients: {} + }); await DatadogFlags.enable({ enabled: true }); }); @@ -33,9 +86,7 @@ describe('FlagsClient', () => { const flagsClient = DatadogFlags.getClient(); await flagsClient.setEvaluationContext({ targetingKey: 'test-user-1', - attributes: { - country: 'US' - } + attributes: { country: 'US' } }); expect( @@ -44,16 +95,14 @@ describe('FlagsClient', () => { }); it('should print an error if there is an error', async () => { - NativeModules.DdFlags.setEvaluationContext.mockRejectedValue( + NativeModules.DdFlags.setEvaluationContext.mockRejectedValueOnce( new Error('NETWORK_ERROR') ); const flagsClient = DatadogFlags.getClient(); await flagsClient.setEvaluationContext({ targetingKey: 'test-user-1', - attributes: { - country: 'US' - } + attributes: { country: 'US' } }); expect(InternalLog.log).toHaveBeenCalledWith( @@ -63,135 +112,222 @@ describe('FlagsClient', () => { }); }); - describe('getBooleanDetails', () => { - it('should fail the validation if the default value is not valid', async () => { + describe('getDetails', () => { + it('should succesfully return flag details for flags', async () => { + // Flag values are mocked in the __mocks__/react-native.ts file. const flagsClient = DatadogFlags.getClient(); - const details = await flagsClient.getBooleanDetails( - 'test-boolean-flag', - // @ts-expect-error - we want to test the validation - 'true' - ); - - expect(details).toMatchObject({ - value: 'true', // The default value is passed through. - error: 'TYPE_MISMATCH', - reason: null, - variant: null + await flagsClient.setEvaluationContext({ + targetingKey: 'test-user-1', + attributes: { country: 'US' } }); - }); - it('should fetch the boolean details from native side', async () => { - const flagsClient = DatadogFlags.getClient(); - const details = await flagsClient.getBooleanDetails( + const booleanDetails = flagsClient.getBooleanDetails( 'test-boolean-flag', - true + false + ); + const stringDetails = flagsClient.getStringDetails( + 'test-string-flag', + 'Default value' + ); + const numberDetails = flagsClient.getNumberDetails( + 'test-number-flag', + -2 + ); + const objectDetails = flagsClient.getObjectDetails( + 'test-object-flag', + { greeting: 'Default value' } ); - expect(details).toMatchObject({ + expect(booleanDetails).toMatchObject({ value: true, variant: 'true', reason: 'STATIC', error: null }); + expect(stringDetails).toMatchObject({ + value: 'hello world', + variant: 'Hello World', + reason: 'STATIC', + error: null + }); + expect(numberDetails).toMatchObject({ + value: 42, + variant: '42', + reason: 'STATIC', + error: null + }); + expect(objectDetails).toMatchObject({ + value: { greeting: 'Greeting from the native side!' }, + variant: 'Native Greeting', + reason: 'STATIC', + error: null + }); }); - }); - describe('getStringDetails', () => { - it('should fail the validation if the default value is not valid', async () => { + it('should return PROVIDER_NOT_READY if evaluation context is not set', () => { const flagsClient = DatadogFlags.getClient(); - const details = await flagsClient.getStringDetails( - 'test-string-flag', - // @ts-expect-error - we want to test the validation - true + // Skip `setEvaluationContext` call here. + + const details = flagsClient.getBooleanDetails( + 'test-boolean-flag', + false ); expect(details).toMatchObject({ - value: true, // The default value is passed through. - error: 'TYPE_MISMATCH', + value: false, reason: null, - variant: null + error: 'PROVIDER_NOT_READY' }); + expect(InternalLog.log).toHaveBeenCalledWith( + expect.stringContaining('The evaluation context is not set'), + SdkVerbosity.ERROR + ); }); - it('should fetch the string details from native side', async () => { + it('should return FLAG_NOT_FOUND if flag is missing from context', async () => { const flagsClient = DatadogFlags.getClient(); - const details = await flagsClient.getStringDetails( - 'test-string-flag', - 'hello world' + await flagsClient.setEvaluationContext({ + targetingKey: 'test-user-1', + attributes: { country: 'US' } + }); + + // 'unknown-flag' is not defined in the __mocks__/react-native.ts + const details = flagsClient.getBooleanDetails( + 'unknown-flag', + false ); expect(details).toMatchObject({ - value: 'hello world', - variant: 'hello world', - reason: 'STATIC', - error: null + value: false, + reason: null, + error: 'FLAG_NOT_FOUND' }); }); - }); - describe('getNumberDetails', () => { - it('should fail the validation if the default value is not valid', async () => { + it('should return the default value if there is a type mismatch between default value and called method type', async () => { + // Flag values are mocked in the __mocks__/react-native.ts file. const flagsClient = DatadogFlags.getClient(); - const details = await flagsClient.getNumberDetails( + await flagsClient.setEvaluationContext({ + targetingKey: 'test-user-1', + attributes: { country: 'US' } + }); + + const booleanDetails = flagsClient.getBooleanDetails( + 'test-boolean-flag', + // @ts-expect-error - testing validation + 'hello world' + ); + const stringDetails = flagsClient.getStringDetails( + 'test-string-flag', + // @ts-expect-error - testing validation + true + ); + const numberDetails = flagsClient.getNumberDetails( 'test-number-flag', - // @ts-expect-error - we want to test the validation + // @ts-expect-error - testing validation + 'hello world' + ); + const objectDetails = flagsClient.getObjectDetails( + 'test-object-flag', + // @ts-expect-error - testing validation 'hello world' ); - expect(details).toMatchObject({ - value: 'hello world', // The default value is passed through. + // The default value is passed through. + expect(booleanDetails).toMatchObject({ + value: 'hello world', + error: 'TYPE_MISMATCH', + reason: null, + variant: null + }); + expect(stringDetails).toMatchObject({ + value: true, + error: 'TYPE_MISMATCH', + reason: null, + variant: null + }); + expect(numberDetails).toMatchObject({ + value: 'hello world', + error: 'TYPE_MISMATCH', + reason: null, + variant: null + }); + expect(objectDetails).toMatchObject({ + value: 'hello world', error: 'TYPE_MISMATCH', reason: null, variant: null }); }); + }); - it('should fetch the number details from native side', async () => { + describe('getValue', () => { + it('should succesfully return flag values', async () => { + // Flag values are mocked in the __mocks__/react-native.ts file. const flagsClient = DatadogFlags.getClient(); - const details = await flagsClient.getNumberDetails( + await flagsClient.setEvaluationContext({ + targetingKey: 'test-user-1', + attributes: { country: 'US' } + }); + + const booleanValue = flagsClient.getBooleanValue( + 'test-boolean-flag', + false + ); + const stringValue = flagsClient.getStringValue( + 'test-string-flag', + 'Default value' + ); + const numberValue = flagsClient.getNumberValue( 'test-number-flag', - 6 + -2 ); + const objectValue = flagsClient.getObjectValue('test-object-flag', { + greeting: 'Default value' + }); - expect(details).toMatchObject({ - value: 6, - variant: '6', - reason: 'STATIC', - error: null + expect(booleanValue).toBe(true); + expect(stringValue).toBe('hello world'); + expect(numberValue).toBe(42); + expect(objectValue).toStrictEqual({ + greeting: 'Greeting from the native side!' }); }); - }); - describe('getObjectDetails', () => { - it('should fail the validation if the default value is not valid', async () => { + it('should return the default value if there is a type mismatch between default value and called method type', async () => { + // Flag values are mocked in the __mocks__/react-native.ts file. const flagsClient = DatadogFlags.getClient(); - const details = await flagsClient.getObjectDetails( - 'test-object-flag', - // @ts-expect-error - we want to test the validation - 'hello world' - ); - - expect(details).toMatchObject({ - value: 'hello world', // The default value is passed through. - error: 'TYPE_MISMATCH', - reason: null, - variant: null + await flagsClient.setEvaluationContext({ + targetingKey: 'test-user-1', + attributes: { country: 'US' } }); - }); - it('should fetch the object details from native side', async () => { - const flagsClient = DatadogFlags.getClient(); - const details = await flagsClient.getObjectDetails( + const booleanValue = flagsClient.getBooleanValue( + 'test-boolean-flag', + // @ts-expect-error - testing validation + 'hello world' + ); + const stringValue = flagsClient.getStringValue( + 'test-string-flag', + // @ts-expect-error - testing validation + true + ); + const numberValue = flagsClient.getNumberValue( + 'test-number-flag', + // @ts-expect-error - testing validation + 'hello world' + ); + const objectValue = flagsClient.getObjectValue( 'test-object-flag', - { hello: 'world' } + // @ts-expect-error - testing validation + 'hello world' ); - expect(details).toMatchObject({ - value: { hello: 'world' }, - variant: 'hello world', - reason: 'STATIC', - error: null - }); + // The default value is passed through. + expect(booleanValue).toBe('hello world'); + expect(stringValue).toBe(true); + expect(numberValue).toBe('hello world'); + expect(objectValue).toBe('hello world'); }); }); }); diff --git a/packages/core/src/flags/internal.ts b/packages/core/src/flags/internal.ts index 5a39dcfcc..fde6d9d1b 100644 --- a/packages/core/src/flags/internal.ts +++ b/packages/core/src/flags/internal.ts @@ -31,7 +31,7 @@ export const processEvaluationContext = ( context: EvaluationContext ): EvaluationContext => { const { targetingKey } = context; - let { attributes } = context; + let attributes = context.attributes ?? {}; // Filter out object values from attributes because Android doesn't support nested object values in the evaluation context. attributes = Object.fromEntries( diff --git a/packages/core/src/flags/types.ts b/packages/core/src/flags/types.ts index 382488127..e33389d61 100644 --- a/packages/core/src/flags/types.ts +++ b/packages/core/src/flags/types.ts @@ -145,7 +145,7 @@ export interface EvaluationContext { * * NOTE: Nested object values are not supported and will be omitted from the evaluation context. */ - attributes: Record; + attributes?: Record; } export type ObjectValue = { [key: string]: unknown }; From e0765b06a1efe01b60b83159b9bb0ec41d25859b Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Wed, 7 Jan 2026 18:46:32 +0200 Subject: [PATCH 53/64] Remove Datadog package version pins from example apps --- example-new-architecture/ios/Podfile | 12 +--- example-new-architecture/ios/Podfile.lock | 75 +++++------------------ example/ios/Podfile | 12 +--- example/ios/Podfile.lock | 75 +++++------------------ 4 files changed, 32 insertions(+), 142 deletions(-) diff --git a/example-new-architecture/ios/Podfile b/example-new-architecture/ios/Podfile index 5810da564..3c29ed272 100644 --- a/example-new-architecture/ios/Podfile +++ b/example-new-architecture/ios/Podfile @@ -19,16 +19,6 @@ end target 'DdSdkReactNativeExample' do pod 'DatadogSDKReactNative', :path => '../../packages/core/DatadogSDKReactNative.podspec', :testspecs => ['Tests'] - # Pin Datadog* dependencies to a specific reference until they are updated in feature/v3. - pod 'DatadogInternal', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :branch => 'feature/flags' - pod 'DatadogCore', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :branch => 'feature/flags' - pod 'DatadogLogs', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :branch => 'feature/flags' - pod 'DatadogTrace', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :branch => 'feature/flags' - pod 'DatadogRUM', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :branch => 'feature/flags' - pod 'DatadogCrashReporting', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :branch => 'feature/flags' - pod 'DatadogFlags', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :branch => 'feature/flags' - pod 'DatadogWebViewTracking', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :branch => 'feature/flags' - config = use_native_modules! use_react_native!( @@ -36,7 +26,7 @@ target 'DdSdkReactNativeExample' do # An absolute path to your application root. :app_path => "#{Pod::Config.instance.installation_root}/.." ) - + post_install do |installer| # https://github.com/facebook/react-native/blob/main/packages/react-native/scripts/react_native_pods.rb#L197-L202 react_native_post_install( diff --git a/example-new-architecture/ios/Podfile.lock b/example-new-architecture/ios/Podfile.lock index 65bf9a7dc..a4529ce32 100644 --- a/example-new-architecture/ios/Podfile.lock +++ b/example-new-architecture/ios/Podfile.lock @@ -5,6 +5,8 @@ PODS: - DatadogCrashReporting (3.4.0): - DatadogInternal (= 3.4.0) - PLCrashReporter (~> 1.12.0) + - DatadogFlags (3.4.0): + - DatadogInternal (= 3.4.0) - DatadogInternal (3.4.0) - DatadogLogs (3.4.0): - DatadogInternal (= 3.4.0) @@ -13,6 +15,7 @@ PODS: - DatadogSDKReactNative (2.13.2): - DatadogCore (= 3.4.0) - DatadogCrashReporting (= 3.4.0) + - DatadogFlags (= 3.4.0) - DatadogLogs (= 3.4.0) - DatadogRUM (= 3.4.0) - DatadogTrace (= 3.4.0) @@ -40,6 +43,7 @@ PODS: - DatadogSDKReactNative/Tests (2.13.2): - DatadogCore (= 3.4.0) - DatadogCrashReporting (= 3.4.0) + - DatadogFlags (= 3.4.0) - DatadogLogs (= 3.4.0) - DatadogRUM (= 3.4.0) - DatadogTrace (= 3.4.0) @@ -1634,16 +1638,8 @@ PODS: DEPENDENCIES: - boost (from `../node_modules/react-native/third-party-podspecs/boost.podspec`) - - DatadogCore (from `https://github.com/DataDog/dd-sdk-ios.git`, branch `feature/flags`) - - DatadogCrashReporting (from `https://github.com/DataDog/dd-sdk-ios.git`, branch `feature/flags`) - - DatadogFlags (from `https://github.com/DataDog/dd-sdk-ios.git`, branch `feature/flags`) - - DatadogInternal (from `https://github.com/DataDog/dd-sdk-ios.git`, branch `feature/flags`) - - DatadogLogs (from `https://github.com/DataDog/dd-sdk-ios.git`, branch `feature/flags`) - - DatadogRUM (from `https://github.com/DataDog/dd-sdk-ios.git`, branch `feature/flags`) - DatadogSDKReactNative (from `../../packages/core/DatadogSDKReactNative.podspec`) - DatadogSDKReactNative/Tests (from `../../packages/core/DatadogSDKReactNative.podspec`) - - DatadogTrace (from `https://github.com/DataDog/dd-sdk-ios.git`, branch `feature/flags`) - - DatadogWebViewTracking (from `https://github.com/DataDog/dd-sdk-ios.git`, branch `feature/flags`) - DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`) - fast_float (from `../node_modules/react-native/third-party-podspecs/fast_float.podspec`) - FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`) @@ -1712,6 +1708,14 @@ DEPENDENCIES: SPEC REPOS: https://github.com/CocoaPods/Specs.git: + - DatadogCore + - DatadogCrashReporting + - DatadogFlags + - DatadogInternal + - DatadogLogs + - DatadogRUM + - DatadogTrace + - DatadogWebViewTracking - OpenTelemetrySwiftApi - PLCrashReporter - SocketRocket @@ -1719,32 +1723,8 @@ SPEC REPOS: EXTERNAL SOURCES: boost: :podspec: "../node_modules/react-native/third-party-podspecs/boost.podspec" - DatadogCore: - :branch: feature/flags - :git: https://github.com/DataDog/dd-sdk-ios.git - DatadogCrashReporting: - :branch: feature/flags - :git: https://github.com/DataDog/dd-sdk-ios.git - DatadogFlags: - :branch: feature/flags - :git: https://github.com/DataDog/dd-sdk-ios.git - DatadogInternal: - :branch: feature/flags - :git: https://github.com/DataDog/dd-sdk-ios.git - DatadogLogs: - :branch: feature/flags - :git: https://github.com/DataDog/dd-sdk-ios.git - DatadogRUM: - :branch: feature/flags - :git: https://github.com/DataDog/dd-sdk-ios.git DatadogSDKReactNative: :path: "../../packages/core/DatadogSDKReactNative.podspec" - DatadogTrace: - :branch: feature/flags - :git: https://github.com/DataDog/dd-sdk-ios.git - DatadogWebViewTracking: - :branch: feature/flags - :git: https://github.com/DataDog/dd-sdk-ios.git DoubleConversion: :podspec: "../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec" fast_float: @@ -1873,40 +1853,15 @@ EXTERNAL SOURCES: Yoga: :path: "../node_modules/react-native/ReactCommon/yoga" -CHECKOUT OPTIONS: - DatadogCore: - :commit: 18ee3ce825a9806c350133179f5e38168d78e0ba - :git: https://github.com/DataDog/dd-sdk-ios.git - DatadogCrashReporting: - :commit: 18ee3ce825a9806c350133179f5e38168d78e0ba - :git: https://github.com/DataDog/dd-sdk-ios.git - DatadogFlags: - :commit: 18ee3ce825a9806c350133179f5e38168d78e0ba - :git: https://github.com/DataDog/dd-sdk-ios.git - DatadogInternal: - :commit: 18ee3ce825a9806c350133179f5e38168d78e0ba - :git: https://github.com/DataDog/dd-sdk-ios.git - DatadogLogs: - :commit: 18ee3ce825a9806c350133179f5e38168d78e0ba - :git: https://github.com/DataDog/dd-sdk-ios.git - DatadogRUM: - :commit: 18ee3ce825a9806c350133179f5e38168d78e0ba - :git: https://github.com/DataDog/dd-sdk-ios.git - DatadogTrace: - :commit: 18ee3ce825a9806c350133179f5e38168d78e0ba - :git: https://github.com/DataDog/dd-sdk-ios.git - DatadogWebViewTracking: - :commit: 18ee3ce825a9806c350133179f5e38168d78e0ba - :git: https://github.com/DataDog/dd-sdk-ios.git - SPEC CHECKSUMS: boost: 1dca942403ed9342f98334bf4c3621f011aa7946 DatadogCore: 8c384b6338c49534e43fdf7f9a0508b62bf1d426 DatadogCrashReporting: 103bfb4077db2ccee1846f71e53712972732d3b7 + DatadogFlags: fbc8dc0e5e387c6c8e15a0afa39d067107e2e7ce DatadogInternal: b0372935ad8dde5ad06960fe8d88c39b2cc92bcc DatadogLogs: 484bb1bfe0c9a7cb2a7d9733f61614e8ea7b2f3a DatadogRUM: 00069b27918e0ce4a9223b87b4bfa7929d6a0a1f - DatadogSDKReactNative: 02cd4d00e3eecdc8ac57042db50b921054bbe709 + DatadogSDKReactNative: 7625fa42b4600102f01a3eee16162db6e51d86ff DatadogTrace: 852cb80f9370eb1321eb30a73c82c8e3d9e4e980 DatadogWebViewTracking: 32dfeaf7aad47a605a689ed12e0d21ee8eb56141 DoubleConversion: f16ae600a246532c4020132d54af21d0ddb2a385 @@ -1976,6 +1931,6 @@ SPEC CHECKSUMS: SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 Yoga: feb4910aba9742cfedc059e2b2902e22ffe9954a -PODFILE CHECKSUM: 470f1ade1ca669373855527342da02c29dfcdfdf +PODFILE CHECKSUM: 2046fc46dd3311048c09b49573c69b7aba2aab81 COCOAPODS: 1.16.2 diff --git a/example/ios/Podfile b/example/ios/Podfile index da3575afe..7484e6aa7 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -21,16 +21,6 @@ target 'ddSdkReactnativeExample' do pod 'DatadogSDKReactNativeSessionReplay', :path => '../../packages/react-native-session-replay/DatadogSDKReactNativeSessionReplay.podspec', :testspecs => ['Tests'] pod 'DatadogSDKReactNativeWebView', :path => '../../packages/react-native-webview/DatadogSDKReactNativeWebView.podspec', :testspecs => ['Tests'] - # Pin Datadog* dependencies to a specific reference until they are updated in feature/v3. - pod 'DatadogInternal', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :branch => 'feature/flags' - pod 'DatadogCore', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :branch => 'feature/flags' - pod 'DatadogLogs', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :branch => 'feature/flags' - pod 'DatadogTrace', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :branch => 'feature/flags' - pod 'DatadogRUM', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :branch => 'feature/flags' - pod 'DatadogCrashReporting', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :branch => 'feature/flags' - pod 'DatadogFlags', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :branch => 'feature/flags' - pod 'DatadogWebViewTracking', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :branch => 'feature/flags' - config = use_native_modules! use_react_native!( @@ -38,7 +28,7 @@ target 'ddSdkReactnativeExample' do # An absolute path to your application root. :app_path => "#{Pod::Config.instance.installation_root}/.." ) - + # pod 'DatadogSDKReactNative', :path => '../../packages/core/DatadogSDKReactNative.podspec', :testspecs => ['Tests'] post_install do |installer| diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 8cd4ef82b..ba77fae87 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -5,6 +5,8 @@ PODS: - DatadogCrashReporting (3.4.0): - DatadogInternal (= 3.4.0) - PLCrashReporter (~> 1.12.0) + - DatadogFlags (3.4.0): + - DatadogInternal (= 3.4.0) - DatadogInternal (3.4.0) - DatadogLogs (3.4.0): - DatadogInternal (= 3.4.0) @@ -13,6 +15,7 @@ PODS: - DatadogSDKReactNative (2.13.2): - DatadogCore (= 3.4.0) - DatadogCrashReporting (= 3.4.0) + - DatadogFlags (= 3.4.0) - DatadogLogs (= 3.4.0) - DatadogRUM (= 3.4.0) - DatadogTrace (= 3.4.0) @@ -21,6 +24,7 @@ PODS: - DatadogSDKReactNative/Tests (2.13.2): - DatadogCore (= 3.4.0) - DatadogCrashReporting (= 3.4.0) + - DatadogFlags (= 3.4.0) - DatadogLogs (= 3.4.0) - DatadogRUM (= 3.4.0) - DatadogTrace (= 3.4.0) @@ -1741,20 +1745,12 @@ PODS: DEPENDENCIES: - boost (from `../node_modules/react-native/third-party-podspecs/boost.podspec`) - - DatadogCore (from `https://github.com/DataDog/dd-sdk-ios.git`, branch `feature/flags`) - - DatadogCrashReporting (from `https://github.com/DataDog/dd-sdk-ios.git`, branch `feature/flags`) - - DatadogFlags (from `https://github.com/DataDog/dd-sdk-ios.git`, branch `feature/flags`) - - DatadogInternal (from `https://github.com/DataDog/dd-sdk-ios.git`, branch `feature/flags`) - - DatadogLogs (from `https://github.com/DataDog/dd-sdk-ios.git`, branch `feature/flags`) - - DatadogRUM (from `https://github.com/DataDog/dd-sdk-ios.git`, branch `feature/flags`) - DatadogSDKReactNative (from `../../packages/core/DatadogSDKReactNative.podspec`) - DatadogSDKReactNative/Tests (from `../../packages/core/DatadogSDKReactNative.podspec`) - DatadogSDKReactNativeSessionReplay (from `../../packages/react-native-session-replay/DatadogSDKReactNativeSessionReplay.podspec`) - DatadogSDKReactNativeSessionReplay/Tests (from `../../packages/react-native-session-replay/DatadogSDKReactNativeSessionReplay.podspec`) - DatadogSDKReactNativeWebView (from `../../packages/react-native-webview/DatadogSDKReactNativeWebView.podspec`) - DatadogSDKReactNativeWebView/Tests (from `../../packages/react-native-webview/DatadogSDKReactNativeWebView.podspec`) - - DatadogTrace (from `https://github.com/DataDog/dd-sdk-ios.git`, branch `feature/flags`) - - DatadogWebViewTracking (from `https://github.com/DataDog/dd-sdk-ios.git`, branch `feature/flags`) - DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`) - fast_float (from `../node_modules/react-native/third-party-podspecs/fast_float.podspec`) - FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`) @@ -1830,7 +1826,15 @@ DEPENDENCIES: SPEC REPOS: https://github.com/CocoaPods/Specs.git: + - DatadogCore + - DatadogCrashReporting + - DatadogFlags + - DatadogInternal + - DatadogLogs + - DatadogRUM - DatadogSessionReplay + - DatadogTrace + - DatadogWebViewTracking - HMSegmentedControl - OpenTelemetrySwiftApi - PLCrashReporter @@ -1839,36 +1843,12 @@ SPEC REPOS: EXTERNAL SOURCES: boost: :podspec: "../node_modules/react-native/third-party-podspecs/boost.podspec" - DatadogCore: - :branch: feature/flags - :git: https://github.com/DataDog/dd-sdk-ios.git - DatadogCrashReporting: - :branch: feature/flags - :git: https://github.com/DataDog/dd-sdk-ios.git - DatadogFlags: - :branch: feature/flags - :git: https://github.com/DataDog/dd-sdk-ios.git - DatadogInternal: - :branch: feature/flags - :git: https://github.com/DataDog/dd-sdk-ios.git - DatadogLogs: - :branch: feature/flags - :git: https://github.com/DataDog/dd-sdk-ios.git - DatadogRUM: - :branch: feature/flags - :git: https://github.com/DataDog/dd-sdk-ios.git DatadogSDKReactNative: :path: "../../packages/core/DatadogSDKReactNative.podspec" DatadogSDKReactNativeSessionReplay: :path: "../../packages/react-native-session-replay/DatadogSDKReactNativeSessionReplay.podspec" DatadogSDKReactNativeWebView: :path: "../../packages/react-native-webview/DatadogSDKReactNativeWebView.podspec" - DatadogTrace: - :branch: feature/flags - :git: https://github.com/DataDog/dd-sdk-ios.git - DatadogWebViewTracking: - :branch: feature/flags - :git: https://github.com/DataDog/dd-sdk-ios.git DoubleConversion: :podspec: "../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec" fast_float: @@ -2011,40 +1991,15 @@ EXTERNAL SOURCES: Yoga: :path: "../node_modules/react-native/ReactCommon/yoga" -CHECKOUT OPTIONS: - DatadogCore: - :commit: 18ee3ce825a9806c350133179f5e38168d78e0ba - :git: https://github.com/DataDog/dd-sdk-ios.git - DatadogCrashReporting: - :commit: 18ee3ce825a9806c350133179f5e38168d78e0ba - :git: https://github.com/DataDog/dd-sdk-ios.git - DatadogFlags: - :commit: 18ee3ce825a9806c350133179f5e38168d78e0ba - :git: https://github.com/DataDog/dd-sdk-ios.git - DatadogInternal: - :commit: 18ee3ce825a9806c350133179f5e38168d78e0ba - :git: https://github.com/DataDog/dd-sdk-ios.git - DatadogLogs: - :commit: 18ee3ce825a9806c350133179f5e38168d78e0ba - :git: https://github.com/DataDog/dd-sdk-ios.git - DatadogRUM: - :commit: 18ee3ce825a9806c350133179f5e38168d78e0ba - :git: https://github.com/DataDog/dd-sdk-ios.git - DatadogTrace: - :commit: 18ee3ce825a9806c350133179f5e38168d78e0ba - :git: https://github.com/DataDog/dd-sdk-ios.git - DatadogWebViewTracking: - :commit: 18ee3ce825a9806c350133179f5e38168d78e0ba - :git: https://github.com/DataDog/dd-sdk-ios.git - SPEC CHECKSUMS: boost: 1dca942403ed9342f98334bf4c3621f011aa7946 DatadogCore: 8c384b6338c49534e43fdf7f9a0508b62bf1d426 DatadogCrashReporting: 103bfb4077db2ccee1846f71e53712972732d3b7 + DatadogFlags: fbc8dc0e5e387c6c8e15a0afa39d067107e2e7ce DatadogInternal: b0372935ad8dde5ad06960fe8d88c39b2cc92bcc DatadogLogs: 484bb1bfe0c9a7cb2a7d9733f61614e8ea7b2f3a DatadogRUM: 00069b27918e0ce4a9223b87b4bfa7929d6a0a1f - DatadogSDKReactNative: 2d02290e38b30c2f118555ac81f6d767dfd0c95a + DatadogSDKReactNative: a36d1b355267543453c79df773df07cc3012b401 DatadogSDKReactNativeSessionReplay: fcbd7cf17dc515949607e112c56374354c8d08ef DatadogSDKReactNativeWebView: 24fefd471c18d13d950a239a51712c830bf6bf7a DatadogSessionReplay: 462a3a2e39e9e2193528cf572c8d1acfd6cdace1 @@ -2125,6 +2080,6 @@ SPEC CHECKSUMS: SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 Yoga: feb4910aba9742cfedc059e2b2902e22ffe9954a -PODFILE CHECKSUM: 9b10c7cbb4e8f376b26065bb47f577130e22bc52 +PODFILE CHECKSUM: 9a1faac3ae43394b0b86e6fcabf63eced6c66dc2 COCOAPODS: 1.16.2 From f5f529111551056e4eea329807ab6aa6156766d7 Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Wed, 7 Jan 2026 19:04:03 +0200 Subject: [PATCH 54/64] Lock iOS deps to a specific commit --- example-new-architecture/ios/Podfile | 11 ++++ example-new-architecture/ios/Podfile.lock | 78 +++++++++++++++++++---- example/ios/Podfile | 11 ++++ example/ios/Podfile.lock | 78 +++++++++++++++++++---- 4 files changed, 150 insertions(+), 28 deletions(-) diff --git a/example-new-architecture/ios/Podfile b/example-new-architecture/ios/Podfile index 3c29ed272..affd2fd5d 100644 --- a/example-new-architecture/ios/Podfile +++ b/example-new-architecture/ios/Podfile @@ -19,6 +19,17 @@ end target 'DdSdkReactNativeExample' do pod 'DatadogSDKReactNative', :path => '../../packages/core/DatadogSDKReactNative.podspec', :testspecs => ['Tests'] + # TODO: Remove this once the 3.5.0 release is cut. + # Pin Datadog* dependencies to a specific commit. + pod 'DatadogInternal', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :commit => '2f28ab9' + pod 'DatadogCore', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :commit => '2f28ab9' + pod 'DatadogLogs', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :commit => '2f28ab9' + pod 'DatadogTrace', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :commit => '2f28ab9' + pod 'DatadogRUM', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :commit => '2f28ab9' + pod 'DatadogCrashReporting', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :commit => '2f28ab9' + pod 'DatadogFlags', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :commit => '2f28ab9' + pod 'DatadogWebViewTracking', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :commit => '2f28ab9' + config = use_native_modules! use_react_native!( diff --git a/example-new-architecture/ios/Podfile.lock b/example-new-architecture/ios/Podfile.lock index a4529ce32..aa251c95a 100644 --- a/example-new-architecture/ios/Podfile.lock +++ b/example-new-architecture/ios/Podfile.lock @@ -70,7 +70,7 @@ PODS: - Yoga - DatadogTrace (3.4.0): - DatadogInternal (= 3.4.0) - - OpenTelemetrySwiftApi (= 1.13.1) + - OpenTelemetry-Swift-Api (~> 2.3.0) - DatadogWebViewTracking (3.4.0): - DatadogInternal (= 3.4.0) - DoubleConversion (1.1.6) @@ -81,7 +81,7 @@ PODS: - hermes-engine (0.76.9): - hermes-engine/Pre-built (= 0.76.9) - hermes-engine/Pre-built (0.76.9) - - OpenTelemetrySwiftApi (1.13.1) + - OpenTelemetry-Swift-Api (2.3.0) - PLCrashReporter (1.12.0) - RCT-Folly (2024.10.14.00): - boost @@ -1638,8 +1638,16 @@ PODS: DEPENDENCIES: - boost (from `../node_modules/react-native/third-party-podspecs/boost.podspec`) + - DatadogCore (from `https://github.com/DataDog/dd-sdk-ios.git`, commit `2f28ab9`) + - DatadogCrashReporting (from `https://github.com/DataDog/dd-sdk-ios.git`, commit `2f28ab9`) + - DatadogFlags (from `https://github.com/DataDog/dd-sdk-ios.git`, commit `2f28ab9`) + - DatadogInternal (from `https://github.com/DataDog/dd-sdk-ios.git`, commit `2f28ab9`) + - DatadogLogs (from `https://github.com/DataDog/dd-sdk-ios.git`, commit `2f28ab9`) + - DatadogRUM (from `https://github.com/DataDog/dd-sdk-ios.git`, commit `2f28ab9`) - DatadogSDKReactNative (from `../../packages/core/DatadogSDKReactNative.podspec`) - DatadogSDKReactNative/Tests (from `../../packages/core/DatadogSDKReactNative.podspec`) + - DatadogTrace (from `https://github.com/DataDog/dd-sdk-ios.git`, commit `2f28ab9`) + - DatadogWebViewTracking (from `https://github.com/DataDog/dd-sdk-ios.git`, commit `2f28ab9`) - DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`) - fast_float (from `../node_modules/react-native/third-party-podspecs/fast_float.podspec`) - FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`) @@ -1708,23 +1716,39 @@ DEPENDENCIES: SPEC REPOS: https://github.com/CocoaPods/Specs.git: - - DatadogCore - - DatadogCrashReporting - - DatadogFlags - - DatadogInternal - - DatadogLogs - - DatadogRUM - - DatadogTrace - - DatadogWebViewTracking - - OpenTelemetrySwiftApi + - OpenTelemetry-Swift-Api - PLCrashReporter - SocketRocket EXTERNAL SOURCES: boost: :podspec: "../node_modules/react-native/third-party-podspecs/boost.podspec" + DatadogCore: + :commit: 2f28ab9 + :git: https://github.com/DataDog/dd-sdk-ios.git + DatadogCrashReporting: + :commit: 2f28ab9 + :git: https://github.com/DataDog/dd-sdk-ios.git + DatadogFlags: + :commit: 2f28ab9 + :git: https://github.com/DataDog/dd-sdk-ios.git + DatadogInternal: + :commit: 2f28ab9 + :git: https://github.com/DataDog/dd-sdk-ios.git + DatadogLogs: + :commit: 2f28ab9 + :git: https://github.com/DataDog/dd-sdk-ios.git + DatadogRUM: + :commit: 2f28ab9 + :git: https://github.com/DataDog/dd-sdk-ios.git DatadogSDKReactNative: :path: "../../packages/core/DatadogSDKReactNative.podspec" + DatadogTrace: + :commit: 2f28ab9 + :git: https://github.com/DataDog/dd-sdk-ios.git + DatadogWebViewTracking: + :commit: 2f28ab9 + :git: https://github.com/DataDog/dd-sdk-ios.git DoubleConversion: :podspec: "../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec" fast_float: @@ -1853,6 +1877,32 @@ EXTERNAL SOURCES: Yoga: :path: "../node_modules/react-native/ReactCommon/yoga" +CHECKOUT OPTIONS: + DatadogCore: + :commit: 2f28ab9 + :git: https://github.com/DataDog/dd-sdk-ios.git + DatadogCrashReporting: + :commit: 2f28ab9 + :git: https://github.com/DataDog/dd-sdk-ios.git + DatadogFlags: + :commit: 2f28ab9 + :git: https://github.com/DataDog/dd-sdk-ios.git + DatadogInternal: + :commit: 2f28ab9 + :git: https://github.com/DataDog/dd-sdk-ios.git + DatadogLogs: + :commit: 2f28ab9 + :git: https://github.com/DataDog/dd-sdk-ios.git + DatadogRUM: + :commit: 2f28ab9 + :git: https://github.com/DataDog/dd-sdk-ios.git + DatadogTrace: + :commit: 2f28ab9 + :git: https://github.com/DataDog/dd-sdk-ios.git + DatadogWebViewTracking: + :commit: 2f28ab9 + :git: https://github.com/DataDog/dd-sdk-ios.git + SPEC CHECKSUMS: boost: 1dca942403ed9342f98334bf4c3621f011aa7946 DatadogCore: 8c384b6338c49534e43fdf7f9a0508b62bf1d426 @@ -1862,7 +1912,7 @@ SPEC CHECKSUMS: DatadogLogs: 484bb1bfe0c9a7cb2a7d9733f61614e8ea7b2f3a DatadogRUM: 00069b27918e0ce4a9223b87b4bfa7929d6a0a1f DatadogSDKReactNative: 7625fa42b4600102f01a3eee16162db6e51d86ff - DatadogTrace: 852cb80f9370eb1321eb30a73c82c8e3d9e4e980 + DatadogTrace: 68b7cc49378ba492423191c29280e3904978a684 DatadogWebViewTracking: 32dfeaf7aad47a605a689ed12e0d21ee8eb56141 DoubleConversion: f16ae600a246532c4020132d54af21d0ddb2a385 fast_float: 06eeec4fe712a76acc9376682e4808b05ce978b6 @@ -1870,7 +1920,7 @@ SPEC CHECKSUMS: fmt: 01b82d4ca6470831d1cc0852a1af644be019e8f6 glog: 08b301085f15bcbb6ff8632a8ebaf239aae04e6a hermes-engine: 9e868dc7be781364296d6ee2f56d0c1a9ef0bb11 - OpenTelemetrySwiftApi: aaee576ed961e0c348af78df58b61300e95bd104 + OpenTelemetry-Swift-Api: 3d77582ab6837a63b65bf7d2eacc57d8f2595edd PLCrashReporter: db59ef96fa3d25f3650040d02ec2798cffee75f2 RCT-Folly: 7b4f73a92ad9571b9dbdb05bb30fad927fa971e1 RCTDeprecation: ebe712bb05077934b16c6bf25228bdec34b64f83 @@ -1931,6 +1981,6 @@ SPEC CHECKSUMS: SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 Yoga: feb4910aba9742cfedc059e2b2902e22ffe9954a -PODFILE CHECKSUM: 2046fc46dd3311048c09b49573c69b7aba2aab81 +PODFILE CHECKSUM: 211d0907a4e052bce20f507daf19b2cde45a32d6 COCOAPODS: 1.16.2 diff --git a/example/ios/Podfile b/example/ios/Podfile index 7484e6aa7..03c374385 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -21,6 +21,17 @@ target 'ddSdkReactnativeExample' do pod 'DatadogSDKReactNativeSessionReplay', :path => '../../packages/react-native-session-replay/DatadogSDKReactNativeSessionReplay.podspec', :testspecs => ['Tests'] pod 'DatadogSDKReactNativeWebView', :path => '../../packages/react-native-webview/DatadogSDKReactNativeWebView.podspec', :testspecs => ['Tests'] + # TODO: Remove this once the 3.5.0 release is cut. + # Pin Datadog* dependencies to a specific commit. + pod 'DatadogInternal', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :commit => '2f28ab9' + pod 'DatadogCore', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :commit => '2f28ab9' + pod 'DatadogLogs', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :commit => '2f28ab9' + pod 'DatadogTrace', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :commit => '2f28ab9' + pod 'DatadogRUM', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :commit => '2f28ab9' + pod 'DatadogCrashReporting', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :commit => '2f28ab9' + pod 'DatadogFlags', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :commit => '2f28ab9' + pod 'DatadogWebViewTracking', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :commit => '2f28ab9' + config = use_native_modules! use_react_native!( diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index ba77fae87..292cedc78 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -93,7 +93,7 @@ PODS: - DatadogInternal (= 3.4.0) - DatadogTrace (3.4.0): - DatadogInternal (= 3.4.0) - - OpenTelemetrySwiftApi (= 1.13.1) + - OpenTelemetry-Swift-Api (~> 2.3.0) - DatadogWebViewTracking (3.4.0): - DatadogInternal (= 3.4.0) - DoubleConversion (1.1.6) @@ -105,7 +105,7 @@ PODS: - hermes-engine/Pre-built (= 0.76.9) - hermes-engine/Pre-built (0.76.9) - HMSegmentedControl (1.5.6) - - OpenTelemetrySwiftApi (1.13.1) + - OpenTelemetry-Swift-Api (2.3.0) - PLCrashReporter (1.12.0) - RCT-Folly (2024.10.14.00): - boost @@ -1745,12 +1745,20 @@ PODS: DEPENDENCIES: - boost (from `../node_modules/react-native/third-party-podspecs/boost.podspec`) + - DatadogCore (from `https://github.com/DataDog/dd-sdk-ios.git`, commit `2f28ab9`) + - DatadogCrashReporting (from `https://github.com/DataDog/dd-sdk-ios.git`, commit `2f28ab9`) + - DatadogFlags (from `https://github.com/DataDog/dd-sdk-ios.git`, commit `2f28ab9`) + - DatadogInternal (from `https://github.com/DataDog/dd-sdk-ios.git`, commit `2f28ab9`) + - DatadogLogs (from `https://github.com/DataDog/dd-sdk-ios.git`, commit `2f28ab9`) + - DatadogRUM (from `https://github.com/DataDog/dd-sdk-ios.git`, commit `2f28ab9`) - DatadogSDKReactNative (from `../../packages/core/DatadogSDKReactNative.podspec`) - DatadogSDKReactNative/Tests (from `../../packages/core/DatadogSDKReactNative.podspec`) - DatadogSDKReactNativeSessionReplay (from `../../packages/react-native-session-replay/DatadogSDKReactNativeSessionReplay.podspec`) - DatadogSDKReactNativeSessionReplay/Tests (from `../../packages/react-native-session-replay/DatadogSDKReactNativeSessionReplay.podspec`) - DatadogSDKReactNativeWebView (from `../../packages/react-native-webview/DatadogSDKReactNativeWebView.podspec`) - DatadogSDKReactNativeWebView/Tests (from `../../packages/react-native-webview/DatadogSDKReactNativeWebView.podspec`) + - DatadogTrace (from `https://github.com/DataDog/dd-sdk-ios.git`, commit `2f28ab9`) + - DatadogWebViewTracking (from `https://github.com/DataDog/dd-sdk-ios.git`, commit `2f28ab9`) - DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`) - fast_float (from `../node_modules/react-native/third-party-podspecs/fast_float.podspec`) - FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`) @@ -1826,29 +1834,45 @@ DEPENDENCIES: SPEC REPOS: https://github.com/CocoaPods/Specs.git: - - DatadogCore - - DatadogCrashReporting - - DatadogFlags - - DatadogInternal - - DatadogLogs - - DatadogRUM - DatadogSessionReplay - - DatadogTrace - - DatadogWebViewTracking - HMSegmentedControl - - OpenTelemetrySwiftApi + - OpenTelemetry-Swift-Api - PLCrashReporter - SocketRocket EXTERNAL SOURCES: boost: :podspec: "../node_modules/react-native/third-party-podspecs/boost.podspec" + DatadogCore: + :commit: 2f28ab9 + :git: https://github.com/DataDog/dd-sdk-ios.git + DatadogCrashReporting: + :commit: 2f28ab9 + :git: https://github.com/DataDog/dd-sdk-ios.git + DatadogFlags: + :commit: 2f28ab9 + :git: https://github.com/DataDog/dd-sdk-ios.git + DatadogInternal: + :commit: 2f28ab9 + :git: https://github.com/DataDog/dd-sdk-ios.git + DatadogLogs: + :commit: 2f28ab9 + :git: https://github.com/DataDog/dd-sdk-ios.git + DatadogRUM: + :commit: 2f28ab9 + :git: https://github.com/DataDog/dd-sdk-ios.git DatadogSDKReactNative: :path: "../../packages/core/DatadogSDKReactNative.podspec" DatadogSDKReactNativeSessionReplay: :path: "../../packages/react-native-session-replay/DatadogSDKReactNativeSessionReplay.podspec" DatadogSDKReactNativeWebView: :path: "../../packages/react-native-webview/DatadogSDKReactNativeWebView.podspec" + DatadogTrace: + :commit: 2f28ab9 + :git: https://github.com/DataDog/dd-sdk-ios.git + DatadogWebViewTracking: + :commit: 2f28ab9 + :git: https://github.com/DataDog/dd-sdk-ios.git DoubleConversion: :podspec: "../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec" fast_float: @@ -1991,6 +2015,32 @@ EXTERNAL SOURCES: Yoga: :path: "../node_modules/react-native/ReactCommon/yoga" +CHECKOUT OPTIONS: + DatadogCore: + :commit: 2f28ab9 + :git: https://github.com/DataDog/dd-sdk-ios.git + DatadogCrashReporting: + :commit: 2f28ab9 + :git: https://github.com/DataDog/dd-sdk-ios.git + DatadogFlags: + :commit: 2f28ab9 + :git: https://github.com/DataDog/dd-sdk-ios.git + DatadogInternal: + :commit: 2f28ab9 + :git: https://github.com/DataDog/dd-sdk-ios.git + DatadogLogs: + :commit: 2f28ab9 + :git: https://github.com/DataDog/dd-sdk-ios.git + DatadogRUM: + :commit: 2f28ab9 + :git: https://github.com/DataDog/dd-sdk-ios.git + DatadogTrace: + :commit: 2f28ab9 + :git: https://github.com/DataDog/dd-sdk-ios.git + DatadogWebViewTracking: + :commit: 2f28ab9 + :git: https://github.com/DataDog/dd-sdk-ios.git + SPEC CHECKSUMS: boost: 1dca942403ed9342f98334bf4c3621f011aa7946 DatadogCore: 8c384b6338c49534e43fdf7f9a0508b62bf1d426 @@ -2003,7 +2053,7 @@ SPEC CHECKSUMS: DatadogSDKReactNativeSessionReplay: fcbd7cf17dc515949607e112c56374354c8d08ef DatadogSDKReactNativeWebView: 24fefd471c18d13d950a239a51712c830bf6bf7a DatadogSessionReplay: 462a3a2e39e9e2193528cf572c8d1acfd6cdace1 - DatadogTrace: 852cb80f9370eb1321eb30a73c82c8e3d9e4e980 + DatadogTrace: 68b7cc49378ba492423191c29280e3904978a684 DatadogWebViewTracking: 32dfeaf7aad47a605a689ed12e0d21ee8eb56141 DoubleConversion: f16ae600a246532c4020132d54af21d0ddb2a385 fast_float: 06eeec4fe712a76acc9376682e4808b05ce978b6 @@ -2012,7 +2062,7 @@ SPEC CHECKSUMS: glog: 08b301085f15bcbb6ff8632a8ebaf239aae04e6a hermes-engine: 9e868dc7be781364296d6ee2f56d0c1a9ef0bb11 HMSegmentedControl: 34c1f54d822d8308e7b24f5d901ec674dfa31352 - OpenTelemetrySwiftApi: aaee576ed961e0c348af78df58b61300e95bd104 + OpenTelemetry-Swift-Api: 3d77582ab6837a63b65bf7d2eacc57d8f2595edd PLCrashReporter: db59ef96fa3d25f3650040d02ec2798cffee75f2 RCT-Folly: 7b4f73a92ad9571b9dbdb05bb30fad927fa971e1 RCTDeprecation: ebe712bb05077934b16c6bf25228bdec34b64f83 @@ -2080,6 +2130,6 @@ SPEC CHECKSUMS: SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 Yoga: feb4910aba9742cfedc059e2b2902e22ffe9954a -PODFILE CHECKSUM: 9a1faac3ae43394b0b86e6fcabf63eced6c66dc2 +PODFILE CHECKSUM: 6309cef88a0921166cdd8af528b6f360fb4fe89b COCOAPODS: 1.16.2 From e14744925bb9e1972533799fa5d5e50f74bd893e Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Wed, 7 Jan 2026 19:25:10 +0200 Subject: [PATCH 55/64] Cut Android implementation to a separate PR --- packages/core/android/build.gradle | 5 +- .../reactnative/DdFlagsImplementation.kt | 235 ------------ .../com/datadog/reactnative/DdSdkBridgeExt.kt | 336 ++++-------------- .../reactnative/DdSdkReactNativePackage.kt | 4 +- .../kotlin/com/datadog/reactnative/DdFlags.kt | 66 ---- .../kotlin/com/datadog/reactnative/DdFlags.kt | 56 --- .../datadog/reactnative/DdSdkBridgeExtTest.kt | 240 ------------- 7 files changed, 81 insertions(+), 861 deletions(-) delete mode 100644 packages/core/android/src/main/kotlin/com/datadog/reactnative/DdFlagsImplementation.kt delete mode 100644 packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdFlags.kt delete mode 100644 packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdFlags.kt diff --git a/packages/core/android/build.gradle b/packages/core/android/build.gradle index 3d3b9ee70..f58031967 100644 --- a/packages/core/android/build.gradle +++ b/packages/core/android/build.gradle @@ -195,9 +195,9 @@ dependencies { } implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" compileOnly "com.squareup.okhttp3:okhttp:3.12.13" - // dd-sdk-android-rum requires androidx.metrics:metrics-performance. + // dd-sdk-android-rum requires androidx.metrics:metrics-performance. // From 2.21.0, it uses 1.0.0-beta02, which requires Gradle 8.6.0. - // This breaks builds if the React Native target is below 0.76.0. as it relies on Gradle 8.5.0. + // This breaks builds if the React Native target is below 0.76.0. as it relies on Gradle 8.5.0. // To avoid this, we enforce 1.0.0-beta01 on RN < 0.76.0 if (reactNativeMinorVersion < 76) { implementation("com.datadoghq:dd-sdk-android-rum:3.4.0") { @@ -210,7 +210,6 @@ dependencies { implementation "com.datadoghq:dd-sdk-android-logs:3.4.0" implementation "com.datadoghq:dd-sdk-android-trace:3.4.0" implementation "com.datadoghq:dd-sdk-android-webview:3.4.0" - implementation "com.datadoghq:dd-sdk-android-flags:3.4.0" implementation "com.google.code.gson:gson:2.10.0" testImplementation "org.junit.platform:junit-platform-launcher:1.6.2" testImplementation "org.junit.jupiter:junit-jupiter-api:5.6.2" diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdFlagsImplementation.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdFlagsImplementation.kt deleted file mode 100644 index 21acf16af..000000000 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdFlagsImplementation.kt +++ /dev/null @@ -1,235 +0,0 @@ -/* - * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. - * This product includes software developed at Datadog (https://www.datadoghq.com/). - * Copyright 2016-Present Datadog, Inc. - */ - -package com.datadog.reactnative - -import com.datadog.android.Datadog -import com.datadog.android.api.InternalLogger -import com.datadog.android.api.SdkCore -import com.datadog.android.flags._FlagsInternalProxy -import com.datadog.android.flags.Flags -import com.datadog.android.flags.EvaluationContextCallback -import com.datadog.android.flags.FlagsClient -import com.datadog.android.flags.model.FlagsClientState -import com.datadog.android.flags.FlagsConfiguration -import com.datadog.android.flags.model.UnparsedFlag -import com.datadog.android.flags.model.EvaluationContext -import com.facebook.react.bridge.Promise -import com.facebook.react.bridge.ReadableMap -import org.json.JSONObject -import java.util.Locale - -class DdFlagsImplementation( - private val sdkCore: SdkCore = Datadog.getInstance(), -) { - private val clients: MutableMap = mutableMapOf() - - /** - * Enable the Flags feature with the provided configuration. - * @param configuration The configuration for Flags. - */ - fun enable( - configuration: ReadableMap, - promise: Promise, - ) { - val flagsConfig = buildFlagsConfiguration(configuration.toMap()) - if (flagsConfig != null) { - Flags.enable(flagsConfig, sdkCore) - } else { - InternalLogger.UNBOUND.log( - InternalLogger.Level.ERROR, - InternalLogger.Target.USER, - { "Invalid configuration provided for Flags. Feature initialization skipped." }, - ) - } - promise.resolve(null) - } - - /** - * Retrieve or create a FlagsClient instance. - * - * Caches clients by name to avoid repeated Builder().build() calls. On hot reload, the cache is - * cleared and clients are recreated - this is safe because gracefulModeEnabled=true prevents - * crashes on duplicate creation. - */ - private fun getClient(name: String): FlagsClient = clients.getOrPut(name) { FlagsClient.Builder(name, sdkCore).build() } - - /** - * Set the evaluation context for a specific client. - * @param clientName The name of the client. - * @param targetingKey The targeting key. - * @param attributes The attributes for the evaluation context (will be converted to strings). - */ - fun setEvaluationContext( - clientName: String, - targetingKey: String, - attributes: ReadableMap, - promise: Promise, - ) { - val client = getClient(clientName) - val internalClient = _FlagsInternalProxy(client) - - // Set the evaluation context. - val evaluationContext = buildEvaluationContext(targetingKey, attributes) - client.setEvaluationContext(evaluationContext, object : EvaluationContextCallback { - override fun onSuccess() { - val flagsSnapshot = internalClient.getFlagAssignmentsSnapshot() - val serializedFlagsSnapshot = - flagsSnapshot.mapValues { (key, flag) -> - convertUnparsedFlagToMap(key, flag) - }.toWritableMap() - promise.resolve(serializedFlagsSnapshot) - } - - override fun onFailure(error: Throwable) { - // If network request fails and there are cached flags, return them. - if (client.state.getCurrentState() == FlagsClientState.Stale) { - val flagsSnapshot = internalClient.getFlagAssignmentsSnapshot() - val serializedFlagsSnapshot = - flagsSnapshot.mapValues { (key, flag) -> - convertUnparsedFlagToMap(key, flag) - }.toWritableMap() - promise.resolve(serializedFlagsSnapshot) - } else { - promise.reject("CLIENT_NOT_INITIALIZED", error.message, error) - } - } - }) - } - - fun trackEvaluation( - clientName: String, - key: String, - rawFlag: ReadableMap, - targetingKey: String, - attributes: ReadableMap, - promise: Promise, - ) { - val client = getClient(clientName) - val internalClient = _FlagsInternalProxy(client) - - val flag = convertMapToUnparsedFlag(rawFlag.toMap()) - val evaluationContext = buildEvaluationContext(targetingKey, attributes) - internalClient.trackFlagSnapshotEvaluation(key, flag, evaluationContext) - - promise.resolve(null) - } - - internal companion object { - internal const val NAME = "DdFlags" - } -} - -@Suppress("UNCHECKED_CAST") -private fun buildFlagsConfiguration(configuration: Map): FlagsConfiguration? { - val enabled = configuration["enabled"] as? Boolean ?: false - - if (!enabled) { - return null - } - - // Hard set `gracefulModeEnabled` to `true` because SDK misconfigurations are handled on JS - // side. - // This prevents crashes on hot reload when clients are recreated. - val gracefulModeEnabled = true - - val trackExposures = configuration["trackExposures"] as? Boolean ?: true - val rumIntegrationEnabled = configuration["rumIntegrationEnabled"] as? Boolean ?: true - - return FlagsConfiguration - .Builder() - .apply { - gracefulModeEnabled(gracefulModeEnabled) - trackExposures(trackExposures) - rumIntegrationEnabled(rumIntegrationEnabled) - - // The SDK automatically appends endpoint names to the custom endpoints. - // The input config expects a base URL rather than a full URL. - (configuration["customFlagsEndpoint"] as? String)?.let { - useCustomFlagEndpoint("$it/precompute-assignments") - } - (configuration["customExposureEndpoint"] as? String)?.let { - useCustomExposureEndpoint("$it/api/v2/exposures") - } - }.build() -} - -private fun buildEvaluationContext( - targetingKey: String, - attributes: ReadableMap, -): EvaluationContext { - val parsed = mutableMapOf() - - for ((key, value) in attributes.entryIterator) { - parsed[key] = value.toString() - } - - return EvaluationContext(targetingKey, parsed) -} - -/** - * Converts [UnparsedFlag] to [Map] for further React Native bridge transfer. Includes the - * flag key and parses the value based on variationType. - * - * We are using Map instead of WritableMap as an intermediate because it is more handy, and we can - * convert to WritableMap right before sending to React Native. - */ -private fun convertUnparsedFlagToMap( - flagKey: String, - flag: UnparsedFlag, -): Map { - // Parse the value based on variationType - val parsedValue: Any? = - when (flag.variationType) { - "boolean" -> flag.variationValue.lowercase(Locale.US).toBooleanStrictOrNull() - "string" -> flag.variationValue - "integer" -> flag.variationValue.toIntOrNull() - "number", "float" -> flag.variationValue.toDoubleOrNull() - "object" -> try { - JSONObject(flag.variationValue).toMap() - } catch (_: Exception) { - null - } - else -> { - null - } - } - - if (parsedValue == null) { - InternalLogger.UNBOUND.log( - InternalLogger.Level.ERROR, - InternalLogger.Target.USER, - { "Flag '$flagKey': Failed to parse value '${flag.variationValue}' as '${flag.variationType}'" }, - ) - } - - return mapOf( - "key" to flagKey, - "value" to (parsedValue ?: flag.variationValue), - "allocationKey" to flag.allocationKey, - "variationKey" to flag.variationKey, - "variationType" to flag.variationType, - "variationValue" to flag.variationValue, - "reason" to flag.reason, - "doLog" to flag.doLog, - "extraLogging" to flag.extraLogging.toMap(), - ) -} - -/** Converts a [Map] to a [UnparsedFlag]. */ -@Suppress("UNCHECKED_CAST") -private fun convertMapToUnparsedFlag(map: Map): UnparsedFlag = - object : UnparsedFlag { - override val variationType: String = map["variationType"] as? String ?: "" - override val variationValue: String = map["variationValue"] as? String ?: "" - override val doLog: Boolean = map["doLog"] as? Boolean ?: false - override val allocationKey: String = map["allocationKey"] as? String ?: "" - override val variationKey: String = map["variationKey"] as? String ?: "" - override val extraLogging: JSONObject = - (map["extraLogging"] as? Map)?.toJSONObject() - ?: JSONObject() - override val reason: String = map["reason"] as? String ?: "" - } diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkBridgeExt.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkBridgeExt.kt index 3cb754a2c..06841619b 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkBridgeExt.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkBridgeExt.kt @@ -15,17 +15,16 @@ import com.facebook.react.bridge.WritableArray import com.facebook.react.bridge.WritableMap import com.facebook.react.bridge.WritableNativeArray import com.facebook.react.bridge.WritableNativeMap -import org.json.JSONObject -import org.json.JSONArray /** * Converts the [List] to a [WritableNativeArray]. */ -internal fun List<*>.toWritableArray(): WritableArray = - this.toWritableArray( +internal fun List<*>.toWritableArray(): WritableArray { + return this.toWritableArray( createWritableMap = { WritableNativeMap() }, - createWritableArray = { WritableNativeArray() }, + createWritableArray = { WritableNativeArray() } ) +} /** * Converts the [List] to a [WritableArray]. @@ -34,64 +33,35 @@ internal fun List<*>.toWritableArray(): WritableArray = */ internal fun List<*>.toWritableArray( createWritableMap: () -> WritableMap, - createWritableArray: () -> WritableArray, + createWritableArray: () -> WritableArray ): WritableArray { val writableArray = createWritableArray() for (it in iterator()) { when (it) { - null -> { - writableArray.pushNull() - } - - is Int -> { - writableArray.pushInt(it) - } - - is Long -> { - writableArray.pushDouble(it.toDouble()) - } - - is Float -> { - writableArray.pushDouble(it.toDouble()) - } - - is Double -> { - writableArray.pushDouble(it) - } - - is String -> { - writableArray.pushString(it) - } - - is Boolean -> { - writableArray.pushBoolean(it) - } - - is List<*> -> { - writableArray.pushArray( - it.toWritableArray( - createWritableMap, - createWritableArray, - ), + null -> writableArray.pushNull() + is Int -> writableArray.pushInt(it) + is Long -> writableArray.pushDouble(it.toDouble()) + is Float -> writableArray.pushDouble(it.toDouble()) + is Double -> writableArray.pushDouble(it) + is String -> writableArray.pushString(it) + is Boolean -> writableArray.pushBoolean(it) + is List<*> -> writableArray.pushArray( + it.toWritableArray( + createWritableMap, + createWritableArray ) - } - - is Map<*, *> -> { - writableArray.pushMap( - it.toWritableMap( - createWritableMap, - createWritableArray, - ), - ) - } - - else -> { - Log.e( - javaClass.simpleName, - "toWritableArray(): Unhandled type ${it.javaClass.simpleName} has been ignored", + ) + is Map<*, *> -> writableArray.pushMap( + it.toWritableMap( + createWritableMap, + createWritableArray ) - } + ) + else -> Log.e( + javaClass.simpleName, + "toWritableArray(): Unhandled type ${it.javaClass.simpleName} has been ignored" + ) } } @@ -101,11 +71,12 @@ internal fun List<*>.toWritableArray( /** * Converts the [Map] to a [WritableNativeMap]. */ -internal fun Map<*, *>.toWritableMap(): WritableMap = - this.toWritableMap( +internal fun Map<*, *>.toWritableMap(): WritableMap { + return this.toWritableMap( createWritableMap = { WritableNativeMap() }, - createWritableArray = { WritableNativeArray() }, + createWritableArray = { WritableNativeArray() } ) +} /** * Converts the [Map] to a [WritableMap]. @@ -114,67 +85,38 @@ internal fun Map<*, *>.toWritableMap(): WritableMap = */ internal fun Map<*, *>.toWritableMap( createWritableMap: () -> WritableMap, - createWritableArray: () -> WritableArray, + createWritableArray: () -> WritableArray ): WritableMap { val map = createWritableMap() for ((k, v) in iterator()) { val key = (k as? String) ?: k.toString() when (v) { - null -> { - map.putNull(key) - } - - is Int -> { - map.putInt(key, v) - } - - is Long -> { - map.putDouble(key, v.toDouble()) - } - - is Float -> { - map.putDouble(key, v.toDouble()) - } - - is Double -> { - map.putDouble(key, v) - } - - is String -> { - map.putString(key, v) - } - - is Boolean -> { - map.putBoolean(key, v) - } - - is List<*> -> { - map.putArray( - key, - v.toWritableArray( - createWritableMap, - createWritableArray, - ), - ) - } - - is Map<*, *> -> { - map.putMap( - key, - v.toWritableMap( - createWritableMap, - createWritableArray, - ), + null -> map.putNull(key) + is Int -> map.putInt(key, v) + is Long -> map.putDouble(key, v.toDouble()) + is Float -> map.putDouble(key, v.toDouble()) + is Double -> map.putDouble(key, v) + is String -> map.putString(key, v) + is Boolean -> map.putBoolean(key, v) + is List<*> -> map.putArray( + key, + v.toWritableArray( + createWritableMap, + createWritableArray ) - } - - else -> { - Log.e( - javaClass.simpleName, - "toWritableMap(): Unhandled type ${v.javaClass.simpleName} has been ignored", + ) + is Map<*, *> -> map.putMap( + key, + v.toWritableMap( + createWritableMap, + createWritableArray ) - } + ) + else -> Log.e( + javaClass.simpleName, + "toWritableMap(): Unhandled type ${v.javaClass.simpleName} has been ignored" + ) } } @@ -186,25 +128,20 @@ internal fun Map<*, *>.toWritableMap( * such as [List], [Map] and the raw types. */ internal fun ReadableMap.toMap(): Map { - val map = - this - .toHashMap() - .filterValues { it != null } - .mapValues { it.value!! } - .toMap(HashMap()) + val map = this.toHashMap() + .filterValues { it != null } + .mapValues { it.value!! } + .toMap(HashMap()) val iterator = map.keys.iterator() - fun updateMap( - key: String, - value: Any?, - ) { + fun updateMap(key: String, value: Any?) { if (value != null) { map[key] = value } else { map.remove(key) Log.e( javaClass.simpleName, - "toMap(): Cannot convert nested object for key: $key", + "toMap(): Cannot convert nested object for key: $key" ) } } @@ -213,21 +150,14 @@ internal fun ReadableMap.toMap(): Map { val key = iterator.next() try { when (val type = getType(key)) { - ReadableType.Map -> { - updateMap(key, getMap(key)?.toMap()) - } - - ReadableType.Array -> { - updateMap(key, getArray(key)?.toList()) - } - + ReadableType.Map -> updateMap(key, getMap(key)?.toMap()) + ReadableType.Array -> updateMap(key, getArray(key)?.toList()) ReadableType.Null, ReadableType.Boolean, ReadableType.Number, ReadableType.String -> {} - else -> { map.remove(key) Log.e( javaClass.simpleName, - "toMap(): Skipping unhandled type [${type.name}] for key: $key", + "toMap(): Skipping unhandled type [${type.name}] for key: $key" ) } } @@ -236,7 +166,7 @@ internal fun ReadableMap.toMap(): Map { Log.e( javaClass.simpleName, "toMap(): Could not convert object for key: $key", - err, + err ) } } @@ -256,48 +186,32 @@ internal fun ReadableArray.toList(): List<*> { @Suppress("TooGenericExceptionCaught") try { when (val type = getType(i)) { - ReadableType.Null -> { - list.add(null) - } - - ReadableType.Boolean -> { - list.add(getBoolean(i)) - } - - ReadableType.Number -> { - list.add(getDouble(i)) - } - - ReadableType.String -> { - list.add(getString(i)) - } - + ReadableType.Null -> list.add(null) + ReadableType.Boolean -> list.add(getBoolean(i)) + ReadableType.Number -> list.add(getDouble(i)) + ReadableType.String -> list.add(getString(i)) ReadableType.Map -> { // getMap() return type is nullable in previous RN versions @Suppress("USELESS_ELVIS") val readableMap = getMap(i) ?: Arguments.createMap() list.add(readableMap.toMap()) } - ReadableType.Array -> { // getArray() return type is nullable in previous RN versions @Suppress("USELESS_ELVIS") val readableArray = getArray(i) ?: Arguments.createArray() list.add(readableArray.toList()) } - - else -> { - Log.e( - javaClass.simpleName, - "toList(): Unhandled ReadableType: ${type.name}.", - ) - } + else -> Log.e( + javaClass.simpleName, + "toList(): Unhandled ReadableType: ${type.name}." + ) } } catch (err: NullPointerException) { Log.e( javaClass.simpleName, "toList(): Could not convert object at index: $i.", - err, + err ) } } @@ -309,116 +223,22 @@ internal fun ReadableArray.toList(): List<*> { * Returns the boolean for the given key, or null if the entry is * not in the map. */ -internal fun ReadableMap.getBooleanOrNull(key: String): Boolean? = - if (hasKey(key)) { +internal fun ReadableMap.getBooleanOrNull(key: String): Boolean? { + return if (hasKey(key)) { getBoolean(key) } else { null } +} /** * Returns the double for the given key, or null if the entry is * not in the map. */ -internal fun ReadableMap.getDoubleOrNull(key: String): Double? = - if (hasKey(key)) { +internal fun ReadableMap.getDoubleOrNull(key: String): Double? { + return if (hasKey(key)) { getDouble(key) } else { null } - -/** - * Converts a [JSONObject] to a [WritableMap]. - */ -internal fun JSONObject.toWritableMap(): WritableMap = this.toMap().toWritableMap() - -/** - * Converts a [JSONObject] to a [Map]. - */ -internal fun JSONObject.toMap(): Map { - val map = mutableMapOf() - val keys = this.keys() - - while (keys.hasNext()) { - val key = keys.next() - val value = this.opt(key) - - map[key] = - when (value) { - null, JSONObject.NULL -> null - is JSONObject -> value.toMap() - is JSONArray -> value.toList() - else -> value - } - } - - return map -} - -/** - * Converts a [JSONArray] to a [List]. - */ -internal fun JSONArray.toList(): List { - val list = mutableListOf() - - for (i in 0 until this.length()) { - val value = this.opt(i) - - list.add( - when (value) { - null, JSONObject.NULL -> null - is JSONObject -> value.toMap() - is JSONArray -> value.toList() - else -> value - }, - ) - } - - return list -} - -/** - * Converts a [ReadableMap] to a [JSONObject]. - */ -internal fun ReadableMap.toJSONObject(): JSONObject = this.toMap().toJSONObject() - -/** - * Converts a [Map] to a [JSONObject]. - */ -@Suppress("UNCHECKED_CAST") -internal fun Map.toJSONObject(): JSONObject { - val jsonObject = JSONObject() - - for ((key, value) in this) { - jsonObject.put( - key, - when (value) { - is Map<*, *> -> (value as Map).toJSONObject() - is List<*> -> value.toJSONArray() - else -> value - }, - ) - } - - return jsonObject -} - -/** - * Converts a [List] to a [JSONArray]. - */ -@Suppress("UNCHECKED_CAST") -internal fun List<*>.toJSONArray(): JSONArray { - val jsonArray = JSONArray() - - for (value in this) { - jsonArray.put( - when (value) { - is Map<*, *> -> (value as Map).toJSONObject() - is List<*> -> value.toJSONArray() - else -> value - }, - ) - } - - return jsonArray } diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkReactNativePackage.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkReactNativePackage.kt index 98ffa83db..3a5b022c1 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkReactNativePackage.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkReactNativePackage.kt @@ -25,7 +25,6 @@ class DdSdkReactNativePackage : TurboReactPackage() { DdRumImplementation.NAME -> DdRum(reactContext, sdkWrapper) DdTraceImplementation.NAME -> DdTrace(reactContext) DdLogsImplementation.NAME -> DdLogs(reactContext, sdkWrapper) - DdFlagsImplementation.NAME -> DdFlags(reactContext) else -> null } } @@ -37,8 +36,7 @@ class DdSdkReactNativePackage : TurboReactPackage() { DdSdkImplementation.NAME, DdRumImplementation.NAME, DdTraceImplementation.NAME, - DdLogsImplementation.NAME, - DdFlagsImplementation.NAME + DdLogsImplementation.NAME ).associateWith { ReactModuleInfo( it, diff --git a/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdFlags.kt b/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdFlags.kt deleted file mode 100644 index ffb67bf1d..000000000 --- a/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdFlags.kt +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. - * This product includes software developed at Datadog (https://www.datadoghq.com/). - * Copyright 2016-Present Datadog, Inc. - */ - -package com.datadog.reactnative - -import com.facebook.react.bridge.Promise -import com.facebook.react.bridge.ReactApplicationContext -import com.facebook.react.bridge.ReactMethod -import com.facebook.react.bridge.ReadableMap - -/** The entry point to use Datadog's Flags feature. */ -class DdFlags( - reactContext: ReactApplicationContext, -) : NativeDdFlagsSpec(reactContext) { - private val implementation = DdFlagsImplementation() - - override fun getName(): String = DdFlagsImplementation.NAME - - /** - * Enable the Flags feature with the provided configuration. - * @param configuration The configuration for Flags. - */ - @ReactMethod - override fun enable( - configuration: ReadableMap, - promise: Promise, - ) { - implementation.enable(configuration, promise) - } - - /** - * Set the evaluation context for a specific client. - * @param clientName The name of the client. - * @param targetingKey The targeting key. - * @param attributes The attributes for the evaluation context. - */ - @ReactMethod - override fun setEvaluationContext( - clientName: String, - targetingKey: String, - attributes: ReadableMap, - promise: Promise, - ) { - implementation.setEvaluationContext(clientName, targetingKey, attributes, promise) - } - - /** - * Track the evaluation of a flag. - * @param clientName The name of the client. - * @param key The key of the flag. - */ - @ReactMethod - override fun trackEvaluation( - clientName: String, - key: String, - rawFlag: ReadableMap, - targetingKey: String, - attributes: ReadableMap, - promise: Promise, - ) { - implementation.trackEvaluation(clientName, key, rawFlag, targetingKey, attributes, promise) - } -} diff --git a/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdFlags.kt b/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdFlags.kt deleted file mode 100644 index f4131160b..000000000 --- a/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdFlags.kt +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. - * This product includes software developed at Datadog (https://www.datadoghq.com/). - * Copyright 2016-Present Datadog, Inc. - */ - -package com.datadog.reactnative - -import com.facebook.react.bridge.Promise -import com.facebook.react.bridge.ReactApplicationContext -import com.facebook.react.bridge.ReactContextBaseJavaModule -import com.facebook.react.bridge.ReactMethod -import com.facebook.react.bridge.ReadableMap - -/** The entry point to use Datadog's Flags feature. */ -class DdFlags(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) { - - private val implementation = DdFlagsImplementation() - - override fun getName(): String = DdFlagsImplementation.NAME - - /** - * Enable the Flags feature with the provided configuration. - * @param configuration The configuration for Flags. - */ - @ReactMethod - fun enable(configuration: ReadableMap, promise: Promise) { - implementation.enable(configuration, promise) - } - - /** - * Set the evaluation context for a specific client. - * @param clientName The name of the client. - * @param targetingKey The targeting key. - * @param attributes The attributes for the evaluation context. - */ - @ReactMethod - fun setEvaluationContext( - clientName: String, - targetingKey: String, - attributes: ReadableMap, - promise: Promise - ) { - implementation.setEvaluationContext(clientName, targetingKey, attributes, promise) - } - - /** - * Track the evaluation of a flag. - * @param clientName The name of the client. - * @param key The key of the flag. - */ - @ReactMethod - fun trackEvaluation(clientName: String, key: String, rawFlag: ReadableMap, targetingKey: String, attributes: ReadableMap, promise: Promise) { - implementation.trackEvaluation(clientName, key, rawFlag, targetingKey, attributes, promise) - } -} diff --git a/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkBridgeExtTest.kt b/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkBridgeExtTest.kt index b3b73e03e..796936d04 100644 --- a/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkBridgeExtTest.kt +++ b/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkBridgeExtTest.kt @@ -14,8 +14,6 @@ import com.facebook.react.bridge.JavaOnlyMap import com.facebook.react.bridge.WritableArray import com.facebook.react.bridge.WritableMap import org.assertj.core.api.Assertions.assertThat -import org.json.JSONArray -import org.json.JSONObject import org.junit.jupiter.api.Test internal class DdSdkBridgeExtTest { @@ -278,244 +276,6 @@ internal class DdSdkBridgeExtTest { assertThat(value).isNull() } - @Test - fun `M do a proper conversion W JSONObject toMap { with raw types }`() { - // Given - val jsonObject = JSONObject().apply { - put("null", JSONObject.NULL) - put("int", 1) - put("long", 2L) - put("double", 3.0) - put("string", "test") - put("boolean", true) - } - - // When - val map = jsonObject.toMap() - - // Then - assertThat(map).hasSize(6) - assertThat(map["null"]).isNull() - assertThat(map["int"]).isEqualTo(1) - assertThat(map["long"]).isEqualTo(2L) - assertThat(map["double"]).isEqualTo(3.0) - assertThat(map["string"]).isEqualTo("test") - assertThat(map["boolean"]).isEqualTo(true) - } - - @Test - fun `M do a proper conversion W JSONObject toMap { with nested objects }`() { - // Given - val nestedObject = JSONObject().apply { - put("nestedKey", "nestedValue") - } - val nestedArray = JSONArray().apply { - put("item1") - put("item2") - } - val jsonObject = JSONObject().apply { - put("object", nestedObject) - put("array", nestedArray) - } - - // When - val map = jsonObject.toMap() - - // Then - assertThat(map).hasSize(2) - assertThat(map["object"]).isInstanceOf(Map::class.java) - assertThat((map["object"] as Map<*, *>)["nestedKey"]).isEqualTo("nestedValue") - assertThat(map["array"]).isInstanceOf(List::class.java) - assertThat((map["array"] as List<*>)).hasSize(2) - assertThat((map["array"] as List<*>)[0]).isEqualTo("item1") - assertThat((map["array"] as List<*>)[1]).isEqualTo("item2") - } - - @Test - fun `M do a proper conversion W JSONObject toWritableMap { with raw types }`() { - // Given - val jsonObject = JSONObject().apply { - put("int", 1) - put("double", 2.0) - put("string", "test") - put("boolean", true) - } - - // When - val writableMap = jsonObject.toWritableMap() - - // Then - assertThat(writableMap.getInt("int")).isEqualTo(1) - assertThat(writableMap.getDouble("double")).isEqualTo(2.0) - assertThat(writableMap.getString("string")).isEqualTo("test") - assertThat(writableMap.getBoolean("boolean")).isTrue() - } - - @Test - fun `M do a proper conversion W JSONArray toList { with raw types }`() { - // Given - val jsonArray = JSONArray().apply { - put(JSONObject.NULL) - put(1) - put(2.0) - put("test") - put(true) - } - - // When - val list = jsonArray.toList() - - // Then - assertThat(list).hasSize(5) - assertThat(list[0]).isNull() - assertThat(list[1]).isEqualTo(1) - assertThat(list[2]).isEqualTo(2.0) - assertThat(list[3]).isEqualTo("test") - assertThat(list[4]).isEqualTo(true) - } - - @Test - fun `M do a proper conversion W JSONArray toList { with nested objects }`() { - // Given - val nestedObject = JSONObject().apply { - put("key", "value") - } - val nestedArray = JSONArray().apply { - put("nested") - } - val jsonArray = JSONArray().apply { - put(nestedObject) - put(nestedArray) - } - - // When - val list = jsonArray.toList() - - // Then - assertThat(list).hasSize(2) - assertThat(list[0]).isInstanceOf(Map::class.java) - assertThat((list[0] as Map<*, *>)["key"]).isEqualTo("value") - assertThat(list[1]).isInstanceOf(List::class.java) - assertThat((list[1] as List<*>)[0]).isEqualTo("nested") - } - - @Test - fun `M do a proper conversion W ReadableMap toJSONObject { with raw types }`() { - // Given - val readableMap = mapOf( - "int" to 1, - "double" to 2.0, - "string" to "test", - "boolean" to true - ).toReadableMap() - - // When - val jsonObject = readableMap.toJSONObject() - - // Then - assertThat(jsonObject.length()).isEqualTo(4) - assertThat(jsonObject.getInt("int")).isEqualTo(1) - assertThat(jsonObject.getDouble("double")).isEqualTo(2.0) - assertThat(jsonObject.getString("string")).isEqualTo("test") - assertThat(jsonObject.getBoolean("boolean")).isTrue() - } - - @Test - fun `M do a proper conversion W ReadableMap toJSONObject { with nested objects }`() { - // Given - val readableMap = mapOf( - "map" to mapOf("nestedKey" to "nestedValue"), - "list" to listOf("item1", "item2") - ).toReadableMap() - - // When - val jsonObject = readableMap.toJSONObject() - - // Then - assertThat(jsonObject.length()).isEqualTo(2) - assertThat(jsonObject.getJSONObject("map").getString("nestedKey")).isEqualTo("nestedValue") - assertThat(jsonObject.getJSONArray("list").length()).isEqualTo(2) - assertThat(jsonObject.getJSONArray("list").getString(0)).isEqualTo("item1") - assertThat(jsonObject.getJSONArray("list").getString(1)).isEqualTo("item2") - } - - @Test - fun `M do a proper conversion W Map toJSONObject { with raw types }`() { - // Given - val map: Map = mapOf( - "int" to 1, - "double" to 2.0, - "string" to "test", - "boolean" to true - ) - - // When - val jsonObject = map.toJSONObject() - - // Then - assertThat(jsonObject.length()).isEqualTo(4) - assertThat(jsonObject.getInt("int")).isEqualTo(1) - assertThat(jsonObject.getDouble("double")).isEqualTo(2.0) - assertThat(jsonObject.getString("string")).isEqualTo("test") - assertThat(jsonObject.getBoolean("boolean")).isTrue() - } - - @Test - fun `M do a proper conversion W Map toJSONObject { with nested objects }`() { - // Given - val map: Map = mapOf( - "nestedMap" to mapOf("key" to "value"), - "nestedList" to listOf(1, 2, 3) - ) - - // When - val jsonObject = map.toJSONObject() - - // Then - assertThat(jsonObject.length()).isEqualTo(2) - assertThat(jsonObject.getJSONObject("nestedMap").getString("key")).isEqualTo("value") - assertThat(jsonObject.getJSONArray("nestedList").length()).isEqualTo(3) - assertThat(jsonObject.getJSONArray("nestedList").getInt(0)).isEqualTo(1) - assertThat(jsonObject.getJSONArray("nestedList").getInt(1)).isEqualTo(2) - assertThat(jsonObject.getJSONArray("nestedList").getInt(2)).isEqualTo(3) - } - - @Test - fun `M do a proper conversion W List toJSONArray { with raw types }`() { - // Given - val list = listOf(null, 1, 2.0, "test", true) - - // When - val jsonArray = list.toJSONArray() - - // Then - assertThat(jsonArray.length()).isEqualTo(5) - assertThat(jsonArray.isNull(0)).isTrue() - assertThat(jsonArray.getInt(1)).isEqualTo(1) - assertThat(jsonArray.getDouble(2)).isEqualTo(2.0) - assertThat(jsonArray.getString(3)).isEqualTo("test") - assertThat(jsonArray.getBoolean(4)).isTrue() - } - - @Test - fun `M do a proper conversion W List toJSONArray { with nested objects }`() { - // Given - val list = listOf( - mapOf("key" to "value"), - listOf("nested1", "nested2") - ) - - // When - val jsonArray = list.toJSONArray() - - // Then - assertThat(jsonArray.length()).isEqualTo(2) - assertThat(jsonArray.getJSONObject(0).getString("key")).isEqualTo("value") - assertThat(jsonArray.getJSONArray(1).length()).isEqualTo(2) - assertThat(jsonArray.getJSONArray(1).getString(0)).isEqualTo("nested1") - assertThat(jsonArray.getJSONArray(1).getString(1)).isEqualTo("nested2") - } - private fun getTestMap(): MutableMap = mutableMapOf( "null" to null, "int" to 1, From f69f9d4eeb20f84100aabcf096fd08b75b51b0bf Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Wed, 7 Jan 2026 19:27:37 +0200 Subject: [PATCH 56/64] Flags Android implementation This reverts commit e14744925bb9e1972533799fa5d5e50f74bd893e. --- packages/core/android/build.gradle | 5 +- .../reactnative/DdFlagsImplementation.kt | 235 ++++++++++++ .../com/datadog/reactnative/DdSdkBridgeExt.kt | 336 ++++++++++++++---- .../reactnative/DdSdkReactNativePackage.kt | 4 +- .../kotlin/com/datadog/reactnative/DdFlags.kt | 66 ++++ .../kotlin/com/datadog/reactnative/DdFlags.kt | 56 +++ .../datadog/reactnative/DdSdkBridgeExtTest.kt | 240 +++++++++++++ 7 files changed, 861 insertions(+), 81 deletions(-) create mode 100644 packages/core/android/src/main/kotlin/com/datadog/reactnative/DdFlagsImplementation.kt create mode 100644 packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdFlags.kt create mode 100644 packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdFlags.kt diff --git a/packages/core/android/build.gradle b/packages/core/android/build.gradle index f58031967..3d3b9ee70 100644 --- a/packages/core/android/build.gradle +++ b/packages/core/android/build.gradle @@ -195,9 +195,9 @@ dependencies { } implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" compileOnly "com.squareup.okhttp3:okhttp:3.12.13" - // dd-sdk-android-rum requires androidx.metrics:metrics-performance. + // dd-sdk-android-rum requires androidx.metrics:metrics-performance. // From 2.21.0, it uses 1.0.0-beta02, which requires Gradle 8.6.0. - // This breaks builds if the React Native target is below 0.76.0. as it relies on Gradle 8.5.0. + // This breaks builds if the React Native target is below 0.76.0. as it relies on Gradle 8.5.0. // To avoid this, we enforce 1.0.0-beta01 on RN < 0.76.0 if (reactNativeMinorVersion < 76) { implementation("com.datadoghq:dd-sdk-android-rum:3.4.0") { @@ -210,6 +210,7 @@ dependencies { implementation "com.datadoghq:dd-sdk-android-logs:3.4.0" implementation "com.datadoghq:dd-sdk-android-trace:3.4.0" implementation "com.datadoghq:dd-sdk-android-webview:3.4.0" + implementation "com.datadoghq:dd-sdk-android-flags:3.4.0" implementation "com.google.code.gson:gson:2.10.0" testImplementation "org.junit.platform:junit-platform-launcher:1.6.2" testImplementation "org.junit.jupiter:junit-jupiter-api:5.6.2" diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdFlagsImplementation.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdFlagsImplementation.kt new file mode 100644 index 000000000..21acf16af --- /dev/null +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdFlagsImplementation.kt @@ -0,0 +1,235 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.reactnative + +import com.datadog.android.Datadog +import com.datadog.android.api.InternalLogger +import com.datadog.android.api.SdkCore +import com.datadog.android.flags._FlagsInternalProxy +import com.datadog.android.flags.Flags +import com.datadog.android.flags.EvaluationContextCallback +import com.datadog.android.flags.FlagsClient +import com.datadog.android.flags.model.FlagsClientState +import com.datadog.android.flags.FlagsConfiguration +import com.datadog.android.flags.model.UnparsedFlag +import com.datadog.android.flags.model.EvaluationContext +import com.facebook.react.bridge.Promise +import com.facebook.react.bridge.ReadableMap +import org.json.JSONObject +import java.util.Locale + +class DdFlagsImplementation( + private val sdkCore: SdkCore = Datadog.getInstance(), +) { + private val clients: MutableMap = mutableMapOf() + + /** + * Enable the Flags feature with the provided configuration. + * @param configuration The configuration for Flags. + */ + fun enable( + configuration: ReadableMap, + promise: Promise, + ) { + val flagsConfig = buildFlagsConfiguration(configuration.toMap()) + if (flagsConfig != null) { + Flags.enable(flagsConfig, sdkCore) + } else { + InternalLogger.UNBOUND.log( + InternalLogger.Level.ERROR, + InternalLogger.Target.USER, + { "Invalid configuration provided for Flags. Feature initialization skipped." }, + ) + } + promise.resolve(null) + } + + /** + * Retrieve or create a FlagsClient instance. + * + * Caches clients by name to avoid repeated Builder().build() calls. On hot reload, the cache is + * cleared and clients are recreated - this is safe because gracefulModeEnabled=true prevents + * crashes on duplicate creation. + */ + private fun getClient(name: String): FlagsClient = clients.getOrPut(name) { FlagsClient.Builder(name, sdkCore).build() } + + /** + * Set the evaluation context for a specific client. + * @param clientName The name of the client. + * @param targetingKey The targeting key. + * @param attributes The attributes for the evaluation context (will be converted to strings). + */ + fun setEvaluationContext( + clientName: String, + targetingKey: String, + attributes: ReadableMap, + promise: Promise, + ) { + val client = getClient(clientName) + val internalClient = _FlagsInternalProxy(client) + + // Set the evaluation context. + val evaluationContext = buildEvaluationContext(targetingKey, attributes) + client.setEvaluationContext(evaluationContext, object : EvaluationContextCallback { + override fun onSuccess() { + val flagsSnapshot = internalClient.getFlagAssignmentsSnapshot() + val serializedFlagsSnapshot = + flagsSnapshot.mapValues { (key, flag) -> + convertUnparsedFlagToMap(key, flag) + }.toWritableMap() + promise.resolve(serializedFlagsSnapshot) + } + + override fun onFailure(error: Throwable) { + // If network request fails and there are cached flags, return them. + if (client.state.getCurrentState() == FlagsClientState.Stale) { + val flagsSnapshot = internalClient.getFlagAssignmentsSnapshot() + val serializedFlagsSnapshot = + flagsSnapshot.mapValues { (key, flag) -> + convertUnparsedFlagToMap(key, flag) + }.toWritableMap() + promise.resolve(serializedFlagsSnapshot) + } else { + promise.reject("CLIENT_NOT_INITIALIZED", error.message, error) + } + } + }) + } + + fun trackEvaluation( + clientName: String, + key: String, + rawFlag: ReadableMap, + targetingKey: String, + attributes: ReadableMap, + promise: Promise, + ) { + val client = getClient(clientName) + val internalClient = _FlagsInternalProxy(client) + + val flag = convertMapToUnparsedFlag(rawFlag.toMap()) + val evaluationContext = buildEvaluationContext(targetingKey, attributes) + internalClient.trackFlagSnapshotEvaluation(key, flag, evaluationContext) + + promise.resolve(null) + } + + internal companion object { + internal const val NAME = "DdFlags" + } +} + +@Suppress("UNCHECKED_CAST") +private fun buildFlagsConfiguration(configuration: Map): FlagsConfiguration? { + val enabled = configuration["enabled"] as? Boolean ?: false + + if (!enabled) { + return null + } + + // Hard set `gracefulModeEnabled` to `true` because SDK misconfigurations are handled on JS + // side. + // This prevents crashes on hot reload when clients are recreated. + val gracefulModeEnabled = true + + val trackExposures = configuration["trackExposures"] as? Boolean ?: true + val rumIntegrationEnabled = configuration["rumIntegrationEnabled"] as? Boolean ?: true + + return FlagsConfiguration + .Builder() + .apply { + gracefulModeEnabled(gracefulModeEnabled) + trackExposures(trackExposures) + rumIntegrationEnabled(rumIntegrationEnabled) + + // The SDK automatically appends endpoint names to the custom endpoints. + // The input config expects a base URL rather than a full URL. + (configuration["customFlagsEndpoint"] as? String)?.let { + useCustomFlagEndpoint("$it/precompute-assignments") + } + (configuration["customExposureEndpoint"] as? String)?.let { + useCustomExposureEndpoint("$it/api/v2/exposures") + } + }.build() +} + +private fun buildEvaluationContext( + targetingKey: String, + attributes: ReadableMap, +): EvaluationContext { + val parsed = mutableMapOf() + + for ((key, value) in attributes.entryIterator) { + parsed[key] = value.toString() + } + + return EvaluationContext(targetingKey, parsed) +} + +/** + * Converts [UnparsedFlag] to [Map] for further React Native bridge transfer. Includes the + * flag key and parses the value based on variationType. + * + * We are using Map instead of WritableMap as an intermediate because it is more handy, and we can + * convert to WritableMap right before sending to React Native. + */ +private fun convertUnparsedFlagToMap( + flagKey: String, + flag: UnparsedFlag, +): Map { + // Parse the value based on variationType + val parsedValue: Any? = + when (flag.variationType) { + "boolean" -> flag.variationValue.lowercase(Locale.US).toBooleanStrictOrNull() + "string" -> flag.variationValue + "integer" -> flag.variationValue.toIntOrNull() + "number", "float" -> flag.variationValue.toDoubleOrNull() + "object" -> try { + JSONObject(flag.variationValue).toMap() + } catch (_: Exception) { + null + } + else -> { + null + } + } + + if (parsedValue == null) { + InternalLogger.UNBOUND.log( + InternalLogger.Level.ERROR, + InternalLogger.Target.USER, + { "Flag '$flagKey': Failed to parse value '${flag.variationValue}' as '${flag.variationType}'" }, + ) + } + + return mapOf( + "key" to flagKey, + "value" to (parsedValue ?: flag.variationValue), + "allocationKey" to flag.allocationKey, + "variationKey" to flag.variationKey, + "variationType" to flag.variationType, + "variationValue" to flag.variationValue, + "reason" to flag.reason, + "doLog" to flag.doLog, + "extraLogging" to flag.extraLogging.toMap(), + ) +} + +/** Converts a [Map] to a [UnparsedFlag]. */ +@Suppress("UNCHECKED_CAST") +private fun convertMapToUnparsedFlag(map: Map): UnparsedFlag = + object : UnparsedFlag { + override val variationType: String = map["variationType"] as? String ?: "" + override val variationValue: String = map["variationValue"] as? String ?: "" + override val doLog: Boolean = map["doLog"] as? Boolean ?: false + override val allocationKey: String = map["allocationKey"] as? String ?: "" + override val variationKey: String = map["variationKey"] as? String ?: "" + override val extraLogging: JSONObject = + (map["extraLogging"] as? Map)?.toJSONObject() + ?: JSONObject() + override val reason: String = map["reason"] as? String ?: "" + } diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkBridgeExt.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkBridgeExt.kt index 06841619b..3cb754a2c 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkBridgeExt.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkBridgeExt.kt @@ -15,16 +15,17 @@ import com.facebook.react.bridge.WritableArray import com.facebook.react.bridge.WritableMap import com.facebook.react.bridge.WritableNativeArray import com.facebook.react.bridge.WritableNativeMap +import org.json.JSONObject +import org.json.JSONArray /** * Converts the [List] to a [WritableNativeArray]. */ -internal fun List<*>.toWritableArray(): WritableArray { - return this.toWritableArray( +internal fun List<*>.toWritableArray(): WritableArray = + this.toWritableArray( createWritableMap = { WritableNativeMap() }, - createWritableArray = { WritableNativeArray() } + createWritableArray = { WritableNativeArray() }, ) -} /** * Converts the [List] to a [WritableArray]. @@ -33,35 +34,64 @@ internal fun List<*>.toWritableArray(): WritableArray { */ internal fun List<*>.toWritableArray( createWritableMap: () -> WritableMap, - createWritableArray: () -> WritableArray + createWritableArray: () -> WritableArray, ): WritableArray { val writableArray = createWritableArray() for (it in iterator()) { when (it) { - null -> writableArray.pushNull() - is Int -> writableArray.pushInt(it) - is Long -> writableArray.pushDouble(it.toDouble()) - is Float -> writableArray.pushDouble(it.toDouble()) - is Double -> writableArray.pushDouble(it) - is String -> writableArray.pushString(it) - is Boolean -> writableArray.pushBoolean(it) - is List<*> -> writableArray.pushArray( - it.toWritableArray( - createWritableMap, - createWritableArray + null -> { + writableArray.pushNull() + } + + is Int -> { + writableArray.pushInt(it) + } + + is Long -> { + writableArray.pushDouble(it.toDouble()) + } + + is Float -> { + writableArray.pushDouble(it.toDouble()) + } + + is Double -> { + writableArray.pushDouble(it) + } + + is String -> { + writableArray.pushString(it) + } + + is Boolean -> { + writableArray.pushBoolean(it) + } + + is List<*> -> { + writableArray.pushArray( + it.toWritableArray( + createWritableMap, + createWritableArray, + ), ) - ) - is Map<*, *> -> writableArray.pushMap( - it.toWritableMap( - createWritableMap, - createWritableArray + } + + is Map<*, *> -> { + writableArray.pushMap( + it.toWritableMap( + createWritableMap, + createWritableArray, + ), ) - ) - else -> Log.e( - javaClass.simpleName, - "toWritableArray(): Unhandled type ${it.javaClass.simpleName} has been ignored" - ) + } + + else -> { + Log.e( + javaClass.simpleName, + "toWritableArray(): Unhandled type ${it.javaClass.simpleName} has been ignored", + ) + } } } @@ -71,12 +101,11 @@ internal fun List<*>.toWritableArray( /** * Converts the [Map] to a [WritableNativeMap]. */ -internal fun Map<*, *>.toWritableMap(): WritableMap { - return this.toWritableMap( +internal fun Map<*, *>.toWritableMap(): WritableMap = + this.toWritableMap( createWritableMap = { WritableNativeMap() }, - createWritableArray = { WritableNativeArray() } + createWritableArray = { WritableNativeArray() }, ) -} /** * Converts the [Map] to a [WritableMap]. @@ -85,38 +114,67 @@ internal fun Map<*, *>.toWritableMap(): WritableMap { */ internal fun Map<*, *>.toWritableMap( createWritableMap: () -> WritableMap, - createWritableArray: () -> WritableArray + createWritableArray: () -> WritableArray, ): WritableMap { val map = createWritableMap() for ((k, v) in iterator()) { val key = (k as? String) ?: k.toString() when (v) { - null -> map.putNull(key) - is Int -> map.putInt(key, v) - is Long -> map.putDouble(key, v.toDouble()) - is Float -> map.putDouble(key, v.toDouble()) - is Double -> map.putDouble(key, v) - is String -> map.putString(key, v) - is Boolean -> map.putBoolean(key, v) - is List<*> -> map.putArray( - key, - v.toWritableArray( - createWritableMap, - createWritableArray + null -> { + map.putNull(key) + } + + is Int -> { + map.putInt(key, v) + } + + is Long -> { + map.putDouble(key, v.toDouble()) + } + + is Float -> { + map.putDouble(key, v.toDouble()) + } + + is Double -> { + map.putDouble(key, v) + } + + is String -> { + map.putString(key, v) + } + + is Boolean -> { + map.putBoolean(key, v) + } + + is List<*> -> { + map.putArray( + key, + v.toWritableArray( + createWritableMap, + createWritableArray, + ), ) - ) - is Map<*, *> -> map.putMap( - key, - v.toWritableMap( - createWritableMap, - createWritableArray + } + + is Map<*, *> -> { + map.putMap( + key, + v.toWritableMap( + createWritableMap, + createWritableArray, + ), ) - ) - else -> Log.e( - javaClass.simpleName, - "toWritableMap(): Unhandled type ${v.javaClass.simpleName} has been ignored" - ) + } + + else -> { + Log.e( + javaClass.simpleName, + "toWritableMap(): Unhandled type ${v.javaClass.simpleName} has been ignored", + ) + } } } @@ -128,20 +186,25 @@ internal fun Map<*, *>.toWritableMap( * such as [List], [Map] and the raw types. */ internal fun ReadableMap.toMap(): Map { - val map = this.toHashMap() - .filterValues { it != null } - .mapValues { it.value!! } - .toMap(HashMap()) + val map = + this + .toHashMap() + .filterValues { it != null } + .mapValues { it.value!! } + .toMap(HashMap()) val iterator = map.keys.iterator() - fun updateMap(key: String, value: Any?) { + fun updateMap( + key: String, + value: Any?, + ) { if (value != null) { map[key] = value } else { map.remove(key) Log.e( javaClass.simpleName, - "toMap(): Cannot convert nested object for key: $key" + "toMap(): Cannot convert nested object for key: $key", ) } } @@ -150,14 +213,21 @@ internal fun ReadableMap.toMap(): Map { val key = iterator.next() try { when (val type = getType(key)) { - ReadableType.Map -> updateMap(key, getMap(key)?.toMap()) - ReadableType.Array -> updateMap(key, getArray(key)?.toList()) + ReadableType.Map -> { + updateMap(key, getMap(key)?.toMap()) + } + + ReadableType.Array -> { + updateMap(key, getArray(key)?.toList()) + } + ReadableType.Null, ReadableType.Boolean, ReadableType.Number, ReadableType.String -> {} + else -> { map.remove(key) Log.e( javaClass.simpleName, - "toMap(): Skipping unhandled type [${type.name}] for key: $key" + "toMap(): Skipping unhandled type [${type.name}] for key: $key", ) } } @@ -166,7 +236,7 @@ internal fun ReadableMap.toMap(): Map { Log.e( javaClass.simpleName, "toMap(): Could not convert object for key: $key", - err + err, ) } } @@ -186,32 +256,48 @@ internal fun ReadableArray.toList(): List<*> { @Suppress("TooGenericExceptionCaught") try { when (val type = getType(i)) { - ReadableType.Null -> list.add(null) - ReadableType.Boolean -> list.add(getBoolean(i)) - ReadableType.Number -> list.add(getDouble(i)) - ReadableType.String -> list.add(getString(i)) + ReadableType.Null -> { + list.add(null) + } + + ReadableType.Boolean -> { + list.add(getBoolean(i)) + } + + ReadableType.Number -> { + list.add(getDouble(i)) + } + + ReadableType.String -> { + list.add(getString(i)) + } + ReadableType.Map -> { // getMap() return type is nullable in previous RN versions @Suppress("USELESS_ELVIS") val readableMap = getMap(i) ?: Arguments.createMap() list.add(readableMap.toMap()) } + ReadableType.Array -> { // getArray() return type is nullable in previous RN versions @Suppress("USELESS_ELVIS") val readableArray = getArray(i) ?: Arguments.createArray() list.add(readableArray.toList()) } - else -> Log.e( - javaClass.simpleName, - "toList(): Unhandled ReadableType: ${type.name}." - ) + + else -> { + Log.e( + javaClass.simpleName, + "toList(): Unhandled ReadableType: ${type.name}.", + ) + } } } catch (err: NullPointerException) { Log.e( javaClass.simpleName, "toList(): Could not convert object at index: $i.", - err + err, ) } } @@ -223,22 +309,116 @@ internal fun ReadableArray.toList(): List<*> { * Returns the boolean for the given key, or null if the entry is * not in the map. */ -internal fun ReadableMap.getBooleanOrNull(key: String): Boolean? { - return if (hasKey(key)) { +internal fun ReadableMap.getBooleanOrNull(key: String): Boolean? = + if (hasKey(key)) { getBoolean(key) } else { null } -} /** * Returns the double for the given key, or null if the entry is * not in the map. */ -internal fun ReadableMap.getDoubleOrNull(key: String): Double? { - return if (hasKey(key)) { +internal fun ReadableMap.getDoubleOrNull(key: String): Double? = + if (hasKey(key)) { getDouble(key) } else { null } + +/** + * Converts a [JSONObject] to a [WritableMap]. + */ +internal fun JSONObject.toWritableMap(): WritableMap = this.toMap().toWritableMap() + +/** + * Converts a [JSONObject] to a [Map]. + */ +internal fun JSONObject.toMap(): Map { + val map = mutableMapOf() + val keys = this.keys() + + while (keys.hasNext()) { + val key = keys.next() + val value = this.opt(key) + + map[key] = + when (value) { + null, JSONObject.NULL -> null + is JSONObject -> value.toMap() + is JSONArray -> value.toList() + else -> value + } + } + + return map +} + +/** + * Converts a [JSONArray] to a [List]. + */ +internal fun JSONArray.toList(): List { + val list = mutableListOf() + + for (i in 0 until this.length()) { + val value = this.opt(i) + + list.add( + when (value) { + null, JSONObject.NULL -> null + is JSONObject -> value.toMap() + is JSONArray -> value.toList() + else -> value + }, + ) + } + + return list +} + +/** + * Converts a [ReadableMap] to a [JSONObject]. + */ +internal fun ReadableMap.toJSONObject(): JSONObject = this.toMap().toJSONObject() + +/** + * Converts a [Map] to a [JSONObject]. + */ +@Suppress("UNCHECKED_CAST") +internal fun Map.toJSONObject(): JSONObject { + val jsonObject = JSONObject() + + for ((key, value) in this) { + jsonObject.put( + key, + when (value) { + is Map<*, *> -> (value as Map).toJSONObject() + is List<*> -> value.toJSONArray() + else -> value + }, + ) + } + + return jsonObject +} + +/** + * Converts a [List] to a [JSONArray]. + */ +@Suppress("UNCHECKED_CAST") +internal fun List<*>.toJSONArray(): JSONArray { + val jsonArray = JSONArray() + + for (value in this) { + jsonArray.put( + when (value) { + is Map<*, *> -> (value as Map).toJSONObject() + is List<*> -> value.toJSONArray() + else -> value + }, + ) + } + + return jsonArray } diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkReactNativePackage.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkReactNativePackage.kt index 3a5b022c1..98ffa83db 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkReactNativePackage.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkReactNativePackage.kt @@ -25,6 +25,7 @@ class DdSdkReactNativePackage : TurboReactPackage() { DdRumImplementation.NAME -> DdRum(reactContext, sdkWrapper) DdTraceImplementation.NAME -> DdTrace(reactContext) DdLogsImplementation.NAME -> DdLogs(reactContext, sdkWrapper) + DdFlagsImplementation.NAME -> DdFlags(reactContext) else -> null } } @@ -36,7 +37,8 @@ class DdSdkReactNativePackage : TurboReactPackage() { DdSdkImplementation.NAME, DdRumImplementation.NAME, DdTraceImplementation.NAME, - DdLogsImplementation.NAME + DdLogsImplementation.NAME, + DdFlagsImplementation.NAME ).associateWith { ReactModuleInfo( it, diff --git a/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdFlags.kt b/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdFlags.kt new file mode 100644 index 000000000..ffb67bf1d --- /dev/null +++ b/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdFlags.kt @@ -0,0 +1,66 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.reactnative + +import com.facebook.react.bridge.Promise +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.ReactMethod +import com.facebook.react.bridge.ReadableMap + +/** The entry point to use Datadog's Flags feature. */ +class DdFlags( + reactContext: ReactApplicationContext, +) : NativeDdFlagsSpec(reactContext) { + private val implementation = DdFlagsImplementation() + + override fun getName(): String = DdFlagsImplementation.NAME + + /** + * Enable the Flags feature with the provided configuration. + * @param configuration The configuration for Flags. + */ + @ReactMethod + override fun enable( + configuration: ReadableMap, + promise: Promise, + ) { + implementation.enable(configuration, promise) + } + + /** + * Set the evaluation context for a specific client. + * @param clientName The name of the client. + * @param targetingKey The targeting key. + * @param attributes The attributes for the evaluation context. + */ + @ReactMethod + override fun setEvaluationContext( + clientName: String, + targetingKey: String, + attributes: ReadableMap, + promise: Promise, + ) { + implementation.setEvaluationContext(clientName, targetingKey, attributes, promise) + } + + /** + * Track the evaluation of a flag. + * @param clientName The name of the client. + * @param key The key of the flag. + */ + @ReactMethod + override fun trackEvaluation( + clientName: String, + key: String, + rawFlag: ReadableMap, + targetingKey: String, + attributes: ReadableMap, + promise: Promise, + ) { + implementation.trackEvaluation(clientName, key, rawFlag, targetingKey, attributes, promise) + } +} diff --git a/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdFlags.kt b/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdFlags.kt new file mode 100644 index 000000000..f4131160b --- /dev/null +++ b/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdFlags.kt @@ -0,0 +1,56 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.reactnative + +import com.facebook.react.bridge.Promise +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.ReactContextBaseJavaModule +import com.facebook.react.bridge.ReactMethod +import com.facebook.react.bridge.ReadableMap + +/** The entry point to use Datadog's Flags feature. */ +class DdFlags(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) { + + private val implementation = DdFlagsImplementation() + + override fun getName(): String = DdFlagsImplementation.NAME + + /** + * Enable the Flags feature with the provided configuration. + * @param configuration The configuration for Flags. + */ + @ReactMethod + fun enable(configuration: ReadableMap, promise: Promise) { + implementation.enable(configuration, promise) + } + + /** + * Set the evaluation context for a specific client. + * @param clientName The name of the client. + * @param targetingKey The targeting key. + * @param attributes The attributes for the evaluation context. + */ + @ReactMethod + fun setEvaluationContext( + clientName: String, + targetingKey: String, + attributes: ReadableMap, + promise: Promise + ) { + implementation.setEvaluationContext(clientName, targetingKey, attributes, promise) + } + + /** + * Track the evaluation of a flag. + * @param clientName The name of the client. + * @param key The key of the flag. + */ + @ReactMethod + fun trackEvaluation(clientName: String, key: String, rawFlag: ReadableMap, targetingKey: String, attributes: ReadableMap, promise: Promise) { + implementation.trackEvaluation(clientName, key, rawFlag, targetingKey, attributes, promise) + } +} diff --git a/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkBridgeExtTest.kt b/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkBridgeExtTest.kt index 796936d04..b3b73e03e 100644 --- a/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkBridgeExtTest.kt +++ b/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkBridgeExtTest.kt @@ -14,6 +14,8 @@ import com.facebook.react.bridge.JavaOnlyMap import com.facebook.react.bridge.WritableArray import com.facebook.react.bridge.WritableMap import org.assertj.core.api.Assertions.assertThat +import org.json.JSONArray +import org.json.JSONObject import org.junit.jupiter.api.Test internal class DdSdkBridgeExtTest { @@ -276,6 +278,244 @@ internal class DdSdkBridgeExtTest { assertThat(value).isNull() } + @Test + fun `M do a proper conversion W JSONObject toMap { with raw types }`() { + // Given + val jsonObject = JSONObject().apply { + put("null", JSONObject.NULL) + put("int", 1) + put("long", 2L) + put("double", 3.0) + put("string", "test") + put("boolean", true) + } + + // When + val map = jsonObject.toMap() + + // Then + assertThat(map).hasSize(6) + assertThat(map["null"]).isNull() + assertThat(map["int"]).isEqualTo(1) + assertThat(map["long"]).isEqualTo(2L) + assertThat(map["double"]).isEqualTo(3.0) + assertThat(map["string"]).isEqualTo("test") + assertThat(map["boolean"]).isEqualTo(true) + } + + @Test + fun `M do a proper conversion W JSONObject toMap { with nested objects }`() { + // Given + val nestedObject = JSONObject().apply { + put("nestedKey", "nestedValue") + } + val nestedArray = JSONArray().apply { + put("item1") + put("item2") + } + val jsonObject = JSONObject().apply { + put("object", nestedObject) + put("array", nestedArray) + } + + // When + val map = jsonObject.toMap() + + // Then + assertThat(map).hasSize(2) + assertThat(map["object"]).isInstanceOf(Map::class.java) + assertThat((map["object"] as Map<*, *>)["nestedKey"]).isEqualTo("nestedValue") + assertThat(map["array"]).isInstanceOf(List::class.java) + assertThat((map["array"] as List<*>)).hasSize(2) + assertThat((map["array"] as List<*>)[0]).isEqualTo("item1") + assertThat((map["array"] as List<*>)[1]).isEqualTo("item2") + } + + @Test + fun `M do a proper conversion W JSONObject toWritableMap { with raw types }`() { + // Given + val jsonObject = JSONObject().apply { + put("int", 1) + put("double", 2.0) + put("string", "test") + put("boolean", true) + } + + // When + val writableMap = jsonObject.toWritableMap() + + // Then + assertThat(writableMap.getInt("int")).isEqualTo(1) + assertThat(writableMap.getDouble("double")).isEqualTo(2.0) + assertThat(writableMap.getString("string")).isEqualTo("test") + assertThat(writableMap.getBoolean("boolean")).isTrue() + } + + @Test + fun `M do a proper conversion W JSONArray toList { with raw types }`() { + // Given + val jsonArray = JSONArray().apply { + put(JSONObject.NULL) + put(1) + put(2.0) + put("test") + put(true) + } + + // When + val list = jsonArray.toList() + + // Then + assertThat(list).hasSize(5) + assertThat(list[0]).isNull() + assertThat(list[1]).isEqualTo(1) + assertThat(list[2]).isEqualTo(2.0) + assertThat(list[3]).isEqualTo("test") + assertThat(list[4]).isEqualTo(true) + } + + @Test + fun `M do a proper conversion W JSONArray toList { with nested objects }`() { + // Given + val nestedObject = JSONObject().apply { + put("key", "value") + } + val nestedArray = JSONArray().apply { + put("nested") + } + val jsonArray = JSONArray().apply { + put(nestedObject) + put(nestedArray) + } + + // When + val list = jsonArray.toList() + + // Then + assertThat(list).hasSize(2) + assertThat(list[0]).isInstanceOf(Map::class.java) + assertThat((list[0] as Map<*, *>)["key"]).isEqualTo("value") + assertThat(list[1]).isInstanceOf(List::class.java) + assertThat((list[1] as List<*>)[0]).isEqualTo("nested") + } + + @Test + fun `M do a proper conversion W ReadableMap toJSONObject { with raw types }`() { + // Given + val readableMap = mapOf( + "int" to 1, + "double" to 2.0, + "string" to "test", + "boolean" to true + ).toReadableMap() + + // When + val jsonObject = readableMap.toJSONObject() + + // Then + assertThat(jsonObject.length()).isEqualTo(4) + assertThat(jsonObject.getInt("int")).isEqualTo(1) + assertThat(jsonObject.getDouble("double")).isEqualTo(2.0) + assertThat(jsonObject.getString("string")).isEqualTo("test") + assertThat(jsonObject.getBoolean("boolean")).isTrue() + } + + @Test + fun `M do a proper conversion W ReadableMap toJSONObject { with nested objects }`() { + // Given + val readableMap = mapOf( + "map" to mapOf("nestedKey" to "nestedValue"), + "list" to listOf("item1", "item2") + ).toReadableMap() + + // When + val jsonObject = readableMap.toJSONObject() + + // Then + assertThat(jsonObject.length()).isEqualTo(2) + assertThat(jsonObject.getJSONObject("map").getString("nestedKey")).isEqualTo("nestedValue") + assertThat(jsonObject.getJSONArray("list").length()).isEqualTo(2) + assertThat(jsonObject.getJSONArray("list").getString(0)).isEqualTo("item1") + assertThat(jsonObject.getJSONArray("list").getString(1)).isEqualTo("item2") + } + + @Test + fun `M do a proper conversion W Map toJSONObject { with raw types }`() { + // Given + val map: Map = mapOf( + "int" to 1, + "double" to 2.0, + "string" to "test", + "boolean" to true + ) + + // When + val jsonObject = map.toJSONObject() + + // Then + assertThat(jsonObject.length()).isEqualTo(4) + assertThat(jsonObject.getInt("int")).isEqualTo(1) + assertThat(jsonObject.getDouble("double")).isEqualTo(2.0) + assertThat(jsonObject.getString("string")).isEqualTo("test") + assertThat(jsonObject.getBoolean("boolean")).isTrue() + } + + @Test + fun `M do a proper conversion W Map toJSONObject { with nested objects }`() { + // Given + val map: Map = mapOf( + "nestedMap" to mapOf("key" to "value"), + "nestedList" to listOf(1, 2, 3) + ) + + // When + val jsonObject = map.toJSONObject() + + // Then + assertThat(jsonObject.length()).isEqualTo(2) + assertThat(jsonObject.getJSONObject("nestedMap").getString("key")).isEqualTo("value") + assertThat(jsonObject.getJSONArray("nestedList").length()).isEqualTo(3) + assertThat(jsonObject.getJSONArray("nestedList").getInt(0)).isEqualTo(1) + assertThat(jsonObject.getJSONArray("nestedList").getInt(1)).isEqualTo(2) + assertThat(jsonObject.getJSONArray("nestedList").getInt(2)).isEqualTo(3) + } + + @Test + fun `M do a proper conversion W List toJSONArray { with raw types }`() { + // Given + val list = listOf(null, 1, 2.0, "test", true) + + // When + val jsonArray = list.toJSONArray() + + // Then + assertThat(jsonArray.length()).isEqualTo(5) + assertThat(jsonArray.isNull(0)).isTrue() + assertThat(jsonArray.getInt(1)).isEqualTo(1) + assertThat(jsonArray.getDouble(2)).isEqualTo(2.0) + assertThat(jsonArray.getString(3)).isEqualTo("test") + assertThat(jsonArray.getBoolean(4)).isTrue() + } + + @Test + fun `M do a proper conversion W List toJSONArray { with nested objects }`() { + // Given + val list = listOf( + mapOf("key" to "value"), + listOf("nested1", "nested2") + ) + + // When + val jsonArray = list.toJSONArray() + + // Then + assertThat(jsonArray.length()).isEqualTo(2) + assertThat(jsonArray.getJSONObject(0).getString("key")).isEqualTo("value") + assertThat(jsonArray.getJSONArray(1).length()).isEqualTo(2) + assertThat(jsonArray.getJSONArray(1).getString(0)).isEqualTo("nested1") + assertThat(jsonArray.getJSONArray(1).getString(1)).isEqualTo("nested2") + } + private fun getTestMap(): MutableMap = mutableMapOf( "null" to null, "int" to 1, From bb3fcb69843ca35e154f7386d1253848c8a87ada Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Mon, 12 Jan 2026 13:01:28 +0200 Subject: [PATCH 57/64] Rename DatadogFlags -> DdFlags --- example-new-architecture/App.tsx | 6 ++--- example/src/WixApp.tsx | 6 ++--- example/src/ddUtils.tsx | 4 +-- example/src/screens/MainScreen.tsx | 6 ++--- .../src/flags/{DatadogFlags.ts => DdFlags.ts} | 26 +++++++++---------- packages/core/src/flags/FlagsClient.ts | 4 +-- .../{DatadogFlags.test.ts => DdFlags.test.ts} | 24 ++++++++--------- .../src/flags/__tests__/FlagsClient.test.ts | 24 ++++++++--------- packages/core/src/flags/types.ts | 16 ++++++------ packages/core/src/index.tsx | 8 +++--- 10 files changed, 61 insertions(+), 63 deletions(-) rename packages/core/src/flags/{DatadogFlags.ts => DdFlags.ts} (80%) rename packages/core/src/flags/__tests__/{DatadogFlags.test.ts => DdFlags.test.ts} (67%) diff --git a/example-new-architecture/App.tsx b/example-new-architecture/App.tsx index cba7cb733..3e5fc1d80 100644 --- a/example-new-architecture/App.tsx +++ b/example-new-architecture/App.tsx @@ -9,7 +9,7 @@ import { DdLogs, DdTrace, RumConfiguration, - DatadogFlags, + DdFlags, } from '@datadog/mobile-react-native'; import type { FlagDetails } from '@datadog/mobile-react-native'; import React from 'react'; @@ -97,9 +97,9 @@ function App(): React.JSX.Element { const [testFlagValue, setTestFlagValue] = React.useState | null>(null); React.useEffect(() => { (async () => { - await DatadogFlags.enable(); + await DdFlags.enable(); - const flagsClient = DatadogFlags.getClient(); + const flagsClient = DdFlags.getClient(); await flagsClient.setEvaluationContext({ targetingKey: 'test-user-1', attributes: { diff --git a/example/src/WixApp.tsx b/example/src/WixApp.tsx index 0a146e8b6..ada2ae9c6 100644 --- a/example/src/WixApp.tsx +++ b/example/src/WixApp.tsx @@ -11,7 +11,7 @@ import { } from '@datadog/mobile-react-native-navigation'; import styles from './screens/styles'; -import { DatadogFlags } from '@datadog/mobile-react-native'; +import { DdFlags } from '@datadog/mobile-react-native'; import TraceScreen from './screens/TraceScreen'; import { NavigationTrackingOptions, ParamsTrackingPredicate, ViewTrackingPredicate } from '@datadog/mobile-react-native-navigation/src/rum/instrumentation/DdRumReactNativeNavigationTracking'; @@ -73,9 +73,9 @@ const HomeScreen = props => { const [testFlagValue, setTestFlagValue] = useState(false); useEffect(() => { (async () => { - await DatadogFlags.enable(); + await DdFlags.enable(); - const flagsClient = DatadogFlags.getClient(); + const flagsClient = DdFlags.getClient(); await flagsClient.setEvaluationContext({ targetingKey: 'test-user-1', attributes: { diff --git a/example/src/ddUtils.tsx b/example/src/ddUtils.tsx index 9822832a6..ad6702a4a 100644 --- a/example/src/ddUtils.tsx +++ b/example/src/ddUtils.tsx @@ -6,7 +6,7 @@ import { RumConfiguration, SdkVerbosity, TrackingConsent, - DatadogFlags, + DdFlags, } from '@datadog/mobile-react-native'; import {APPLICATION_ID, CLIENT_TOKEN, ENVIRONMENT} from './ddCredentials'; @@ -53,5 +53,5 @@ export function initializeDatadog(trackingConsent: TrackingConsent) { DdSdkReactNative.addAttributes({campaign: "ad-network"}) }); - DatadogFlags.enable() + DdFlags.enable() } diff --git a/example/src/screens/MainScreen.tsx b/example/src/screens/MainScreen.tsx index b66caa2dc..7365faf1b 100644 --- a/example/src/screens/MainScreen.tsx +++ b/example/src/screens/MainScreen.tsx @@ -11,7 +11,7 @@ import { } from 'react-native'; import styles from './styles'; import { APPLICATION_KEY, API_KEY } from '../../src/ddCredentials'; -import { DdLogs, DdSdkReactNative, TrackingConsent, DatadogFlags } from '@datadog/mobile-react-native'; +import { DdLogs, DdSdkReactNative, TrackingConsent, DdFlags } from '@datadog/mobile-react-native'; import { getTrackingConsent, saveTrackingConsent } from '../utils'; import { ConsentModal } from '../components/consent'; import { DdRum } from '../../../packages/core/src/rum/DdRum'; @@ -110,9 +110,9 @@ export default class MainScreen extends Component { fetchBooleanFlag() { (async () => { - await DatadogFlags.enable(); + await DdFlags.enable(); - const flagsClient = DatadogFlags.getClient(); + const flagsClient = DdFlags.getClient(); await flagsClient.setEvaluationContext({ targetingKey: 'test-user-1', attributes: { diff --git a/packages/core/src/flags/DatadogFlags.ts b/packages/core/src/flags/DdFlags.ts similarity index 80% rename from packages/core/src/flags/DatadogFlags.ts rename to packages/core/src/flags/DdFlags.ts index 8c6a41a4e..a2b96f9bf 100644 --- a/packages/core/src/flags/DatadogFlags.ts +++ b/packages/core/src/flags/DdFlags.ts @@ -10,11 +10,11 @@ import type { DdNativeFlagsType } from '../nativeModulesTypes'; import { getGlobalInstance } from '../utils/singletonUtils'; import { FlagsClient } from './FlagsClient'; -import type { DatadogFlagsType, DatadogFlagsConfiguration } from './types'; +import type { DdFlagsType, DdFlagsConfiguration } from './types'; const FLAGS_MODULE = 'com.datadog.reactnative.flags'; -class DatadogFlagsWrapper implements DatadogFlagsType { +class DdFlagsWrapper implements DdFlagsType { // eslint-disable-next-line global-require, @typescript-eslint/no-var-requires private nativeFlags: DdNativeFlagsType = require('../specs/NativeDdFlags') .default; @@ -27,11 +27,11 @@ class DatadogFlagsWrapper implements DatadogFlagsType { * Enables the Datadog Flags feature in your application. * * Call this method after initializing the Datadog SDK to enable feature flag evaluation. - * This method must be called before creating any `FlagsClient` instances via `DatadogFlags.getClient()`. + * This method must be called before creating any `FlagsClient` instances via `DdFlags.getClient()`. * * @example * ```ts - * import { DdSdkReactNativeConfiguration, DdSdkReactNative, DatadogFlags } from '@datadog/mobile-react-native'; + * import { DdSdkReactNativeConfiguration, DdSdkReactNative, DdFlags } from '@datadog/mobile-react-native'; * * // Initialize the Datadog SDK. * await DdSdkReactNative.initialize(...); @@ -42,25 +42,23 @@ class DatadogFlagsWrapper implements DatadogFlagsType { * }; * * // Enable the feature. - * await DatadogFlags.enable(flagsConfig); + * await DdFlags.enable(flagsConfig); * * // Retrieve the client and access feature flags. - * const flagsClient = DatadogFlags.getClient(); + * const flagsClient = DdFlags.getClient(); * const flagValue = await flagsClient.getBooleanValue('new-feature', false); * ``` * * @param configuration Configuration options for the Datadog Flags feature. */ - enable = async ( - configuration?: DatadogFlagsConfiguration - ): Promise => { + enable = async (configuration?: DdFlagsConfiguration): Promise => { if (configuration?.enabled === false) { return; } if (this.isFeatureEnabled) { InternalLog.log( - 'Datadog Flags feature has already been enabled. Skipping this `DatadogFlags.enable()` call.', + 'Datadog Flags feature has already been enabled. Skipping this `DdFlags.enable()` call.', SdkVerbosity.WARN ); } @@ -82,7 +80,7 @@ class DatadogFlagsWrapper implements DatadogFlagsType { * @example * ```ts * // Reminder: you need to initialize the SDK and enable the Flags feature before retrieving the client. - * const flagsClient = DatadogFlags.getClient(); + * const flagsClient = DdFlags.getClient(); * * // Set the evaluation context. * await flagsClient.setEvaluationContext({ @@ -98,7 +96,7 @@ class DatadogFlagsWrapper implements DatadogFlagsType { getClient = (clientName: string = 'default'): FlagsClient => { if (!this.isFeatureEnabled) { InternalLog.log( - '`DatadogFlags.getClient()` called before Datadog Flags feature have been enabled. Client will fall back to serving default flag values.', + '`DdFlags.getClient()` called before Datadog Flags feature have been enabled. Client will fall back to serving default flag values.', SdkVerbosity.ERROR ); } @@ -109,7 +107,7 @@ class DatadogFlagsWrapper implements DatadogFlagsType { }; } -export const DatadogFlags: DatadogFlagsType = getGlobalInstance( +export const DdFlags: DdFlagsType = getGlobalInstance( FLAGS_MODULE, - () => new DatadogFlagsWrapper() + () => new DdFlagsWrapper() ); diff --git a/packages/core/src/flags/FlagsClient.ts b/packages/core/src/flags/FlagsClient.ts index 3e78adc55..a1acecae9 100644 --- a/packages/core/src/flags/FlagsClient.ts +++ b/packages/core/src/flags/FlagsClient.ts @@ -38,7 +38,7 @@ export class FlagsClient { * * @example * ```ts - * const flagsClient = DatadogFlags.getClient(); + * const flagsClient = DdFlags.getClient(); * * await flagsClient.setEvaluationContext({ * targetingKey: 'user-123', @@ -78,7 +78,7 @@ export class FlagsClient { // Check whether the evaluation context has already been set. if (!this._evaluationContext) { InternalLog.log( - `The evaluation context is not set for the client ${this.clientName}. Please, call \`DatadogFlags.setEvaluationContext()\` before evaluating any flags.`, + `The evaluation context is not set for the client ${this.clientName}. Please, call \`DdFlags.setEvaluationContext()\` before evaluating any flags.`, SdkVerbosity.ERROR ); diff --git a/packages/core/src/flags/__tests__/DatadogFlags.test.ts b/packages/core/src/flags/__tests__/DdFlags.test.ts similarity index 67% rename from packages/core/src/flags/__tests__/DatadogFlags.test.ts rename to packages/core/src/flags/__tests__/DdFlags.test.ts index e1315fbe3..2544fac1c 100644 --- a/packages/core/src/flags/__tests__/DatadogFlags.test.ts +++ b/packages/core/src/flags/__tests__/DdFlags.test.ts @@ -8,7 +8,7 @@ import { NativeModules } from 'react-native'; import { InternalLog } from '../../InternalLog'; import { SdkVerbosity } from '../../SdkVerbosity'; -import { DatadogFlags } from '../DatadogFlags'; +import { DdFlags } from '../DdFlags'; jest.mock('../../InternalLog', () => { return { @@ -19,21 +19,21 @@ jest.mock('../../InternalLog', () => { }; }); -describe('DatadogFlags', () => { +describe('DdFlags', () => { beforeEach(() => { jest.clearAllMocks(); - // Reset state of DatadogFlags instance. - Object.assign(DatadogFlags, { + // Reset state of DdFlags instance. + Object.assign(DdFlags, { isFeatureEnabled: false, clients: {} }); }); describe('Initialization', () => { - it('should print an error if calling DatadogFlags.enable() for multiple times', async () => { - await DatadogFlags.enable(); - await DatadogFlags.enable(); - await DatadogFlags.enable(); + it('should print an error if calling DdFlags.enable() for multiple times', async () => { + await DdFlags.enable(); + await DdFlags.enable(); + await DdFlags.enable(); expect(InternalLog.log).toHaveBeenCalledTimes(2); // We let the native part of the SDK handle this gracefully. @@ -41,17 +41,17 @@ describe('DatadogFlags', () => { }); it('should print an error if retrieving the client before the feature is enabled', async () => { - DatadogFlags.getClient(); + DdFlags.getClient(); expect(InternalLog.log).toHaveBeenCalledWith( - '`DatadogFlags.getClient()` called before Datadog Flags feature have been enabled. Client will fall back to serving default flag values.', + '`DdFlags.getClient()` called before Datadog Flags feature have been enabled. Client will fall back to serving default flag values.', SdkVerbosity.ERROR ); }); it('should not print an error if retrieving the client after the feature is enabled', async () => { - await DatadogFlags.enable(); - DatadogFlags.getClient(); + await DdFlags.enable(); + DdFlags.getClient(); expect(InternalLog.log).not.toHaveBeenCalled(); }); diff --git a/packages/core/src/flags/__tests__/FlagsClient.test.ts b/packages/core/src/flags/__tests__/FlagsClient.test.ts index 8aa1f2b69..4af21c638 100644 --- a/packages/core/src/flags/__tests__/FlagsClient.test.ts +++ b/packages/core/src/flags/__tests__/FlagsClient.test.ts @@ -8,7 +8,7 @@ import { NativeModules } from 'react-native'; import { InternalLog } from '../../InternalLog'; import { SdkVerbosity } from '../../SdkVerbosity'; -import { DatadogFlags } from '../DatadogFlags'; +import { DdFlags } from '../DdFlags'; jest.spyOn(NativeModules.DdFlags, 'setEvaluationContext').mockResolvedValue({ 'test-boolean-flag': { @@ -72,18 +72,18 @@ describe('FlagsClient', () => { beforeEach(async () => { jest.clearAllMocks(); - // Reset state of the global DatadogFlags instance. - Object.assign(DatadogFlags, { + // Reset state of the global DdFlags instance. + Object.assign(DdFlags, { isFeatureEnabled: false, clients: {} }); - await DatadogFlags.enable({ enabled: true }); + await DdFlags.enable({ enabled: true }); }); describe('setEvaluationContext', () => { it('should set the evaluation context', async () => { - const flagsClient = DatadogFlags.getClient(); + const flagsClient = DdFlags.getClient(); await flagsClient.setEvaluationContext({ targetingKey: 'test-user-1', attributes: { country: 'US' } @@ -99,7 +99,7 @@ describe('FlagsClient', () => { new Error('NETWORK_ERROR') ); - const flagsClient = DatadogFlags.getClient(); + const flagsClient = DdFlags.getClient(); await flagsClient.setEvaluationContext({ targetingKey: 'test-user-1', attributes: { country: 'US' } @@ -115,7 +115,7 @@ describe('FlagsClient', () => { describe('getDetails', () => { it('should succesfully return flag details for flags', async () => { // Flag values are mocked in the __mocks__/react-native.ts file. - const flagsClient = DatadogFlags.getClient(); + const flagsClient = DdFlags.getClient(); await flagsClient.setEvaluationContext({ targetingKey: 'test-user-1', attributes: { country: 'US' } @@ -165,7 +165,7 @@ describe('FlagsClient', () => { }); it('should return PROVIDER_NOT_READY if evaluation context is not set', () => { - const flagsClient = DatadogFlags.getClient(); + const flagsClient = DdFlags.getClient(); // Skip `setEvaluationContext` call here. const details = flagsClient.getBooleanDetails( @@ -185,7 +185,7 @@ describe('FlagsClient', () => { }); it('should return FLAG_NOT_FOUND if flag is missing from context', async () => { - const flagsClient = DatadogFlags.getClient(); + const flagsClient = DdFlags.getClient(); await flagsClient.setEvaluationContext({ targetingKey: 'test-user-1', attributes: { country: 'US' } @@ -206,7 +206,7 @@ describe('FlagsClient', () => { it('should return the default value if there is a type mismatch between default value and called method type', async () => { // Flag values are mocked in the __mocks__/react-native.ts file. - const flagsClient = DatadogFlags.getClient(); + const flagsClient = DdFlags.getClient(); await flagsClient.setEvaluationContext({ targetingKey: 'test-user-1', attributes: { country: 'US' } @@ -264,7 +264,7 @@ describe('FlagsClient', () => { describe('getValue', () => { it('should succesfully return flag values', async () => { // Flag values are mocked in the __mocks__/react-native.ts file. - const flagsClient = DatadogFlags.getClient(); + const flagsClient = DdFlags.getClient(); await flagsClient.setEvaluationContext({ targetingKey: 'test-user-1', attributes: { country: 'US' } @@ -296,7 +296,7 @@ describe('FlagsClient', () => { it('should return the default value if there is a type mismatch between default value and called method type', async () => { // Flag values are mocked in the __mocks__/react-native.ts file. - const flagsClient = DatadogFlags.getClient(); + const flagsClient = DdFlags.getClient(); await flagsClient.setEvaluationContext({ targetingKey: 'test-user-1', attributes: { country: 'US' } diff --git a/packages/core/src/flags/types.ts b/packages/core/src/flags/types.ts index e33389d61..6b9028b14 100644 --- a/packages/core/src/flags/types.ts +++ b/packages/core/src/flags/types.ts @@ -6,16 +6,16 @@ import type { FlagsClient } from './FlagsClient'; -export type DatadogFlagsType = { +export type DdFlagsType = { /** * Enables the Datadog Flags feature in your application. * * Call this method after initializing the Datadog SDK to enable feature flag evaluation. - * This method must be called before creating any `FlagsClient` instances via `DatadogFlags.getClient()`. + * This method must be called before creating any `FlagsClient` instances via `DdFlags.getClient()`. * * @example * ```ts - * import { DdSdkReactNativeConfiguration, DdSdkReactNative, DatadogFlags } from '@datadog/mobile-react-native'; + * import { DdSdkReactNativeConfiguration, DdSdkReactNative, DdFlags } from '@datadog/mobile-react-native'; * * // Initialize the Datadog SDK. * await DdSdkReactNative.initialize(...); @@ -26,16 +26,16 @@ export type DatadogFlagsType = { * }; * * // Enable the feature. - * await DatadogFlags.enable(flagsConfig); + * await DdFlags.enable(flagsConfig); * * // Retrieve the client and access feature flags. - * const flagsClient = DatadogFlags.getClient(); + * const flagsClient = DdFlags.getClient(); * const flagValue = await flagsClient.getBooleanValue('new-feature', false); * ``` * * @param configuration Configuration options for the Datadog Flags feature. */ - enable: (configuration?: DatadogFlagsConfiguration) => Promise; + enable: (configuration?: DdFlagsConfiguration) => Promise; /** * Returns a `FlagsClient` instance for further feature flag evaluation. * @@ -47,7 +47,7 @@ export type DatadogFlagsType = { * @example * ```ts * // Reminder: you need to initialize the SDK and enable the Flags feature before retrieving the client. - * const flagsClient = DatadogFlags.getClient(); + * const flagsClient = DdFlags.getClient(); * const flagValue = await flagsClient.getBooleanValue('new-feature', false); * ``` */ @@ -60,7 +60,7 @@ export type DatadogFlagsType = { * Use this type to customize the behavior of feature flag evaluation, including custom endpoints, * exposure tracking, and error handling modes. */ -export type DatadogFlagsConfiguration = { +export type DdFlagsConfiguration = { /** * Controls whether the feature flag evaluation feature is enabled. */ diff --git a/packages/core/src/index.tsx b/packages/core/src/index.tsx index 883ed2236..5d7197c1f 100644 --- a/packages/core/src/index.tsx +++ b/packages/core/src/index.tsx @@ -24,8 +24,8 @@ import { InternalLog } from './InternalLog'; import { ProxyConfiguration, ProxyType } from './ProxyConfiguration'; import { SdkVerbosity } from './SdkVerbosity'; import { TrackingConsent } from './TrackingConsent'; -import { DatadogFlags } from './flags/DatadogFlags'; -import type { DatadogFlagsConfiguration, FlagDetails } from './flags/types'; +import { DdFlags } from './flags/DdFlags'; +import type { DdFlagsConfiguration, FlagDetails } from './flags/types'; import { DdLogs } from './logs/DdLogs'; import { DdRum } from './rum/DdRum'; import { DdBabelInteractionTracking } from './rum/instrumentation/interactionTracking/DdBabelInteractionTracking'; @@ -58,7 +58,7 @@ export { FileBasedConfiguration, InitializationMode, DdLogs, - DatadogFlags, + DdFlags, DdTrace, DdRum, RumActionType, @@ -97,6 +97,6 @@ export type { FirstPartyHost, AutoInstrumentationConfiguration, PartialInitializationConfiguration, - DatadogFlagsConfiguration, + DdFlagsConfiguration, FlagDetails }; From 8996a36a26035c91b5e3ad77a81f8ddf7a9e6232 Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Mon, 12 Jan 2026 14:06:52 +0200 Subject: [PATCH 58/64] Update example apps with better usages of flags --- example-new-architecture/App.tsx | 54 +++++++++++++++++++----------- example/src/WixApp.tsx | 51 ++++++++++++++++++---------- example/src/screens/MainScreen.tsx | 43 +++++++++++++++--------- 3 files changed, 94 insertions(+), 54 deletions(-) diff --git a/example-new-architecture/App.tsx b/example-new-architecture/App.tsx index 3e5fc1d80..e21a00e73 100644 --- a/example-new-architecture/App.tsx +++ b/example-new-architecture/App.tsx @@ -11,10 +11,10 @@ import { RumConfiguration, DdFlags, } from '@datadog/mobile-react-native'; -import type { FlagDetails } from '@datadog/mobile-react-native'; import React from 'react'; import type {PropsWithChildren} from 'react'; import { + ActivityIndicator, SafeAreaView, ScrollView, StatusBar, @@ -93,30 +93,43 @@ function Section({children, title}: SectionProps): React.JSX.Element { } function App(): React.JSX.Element { - const testFlagKey = 'rn-sdk-test-json-flag'; - const [testFlagValue, setTestFlagValue] = React.useState | null>(null); + const [isInitialized, setIsInitialized] = React.useState(false); + React.useEffect(() => { - (async () => { - await DdFlags.enable(); - - const flagsClient = DdFlags.getClient(); - await flagsClient.setEvaluationContext({ - targetingKey: 'test-user-1', - attributes: { - country: 'US', - }, - }); - const flag = flagsClient.getObjectDetails(testFlagKey, {default: {hello: 'world'}}); // https://app.datadoghq.com/feature-flags/bcf75cd6-96d8-4182-8871-0b66ad76127a?environmentId=d114cd9a-79ed-4c56-bcf3-bcac9293653b - setTestFlagValue(flag); - })().catch(console.error); + (async () => { + // This is a blocking async app initialization effect. + // It simulates the way most React Native applications are initialized. + await DdFlags.enable(); + const client = DdFlags.getClient(); + + const userId = 'test-user-1'; + const userAttributes = { + country: 'US', + }; + + await client.setEvaluationContext({targetingKey: userId, attributes: userAttributes}); + + setIsInitialized(true); + })().catch(console.error); }, []); const isDarkMode = useColorScheme() === 'dark'; - const backgroundStyle = { backgroundColor: isDarkMode ? Colors.darker : Colors.lighter, }; + if (!isInitialized) { + return ( + + + + ); + } + + // TODO: [FFL-908] Use OpenFeature SDK instead of a manual client call. + const testFlagKey = 'rn-sdk-test-json-flag'; + const testFlag = DdFlags.getClient().getObjectValue(testFlagKey, {greeting: "Default greeting"}); // https://app.datadoghq.com/feature-flags/bcf75cd6-96d8-4182-8871-0b66ad76127a?environmentId=d114cd9a-79ed-4c56-bcf3-bcac9293653b + return (
- - {testFlagKey}: {JSON.stringify(testFlagValue, null, 2)} - +
+ Flag value for {testFlagKey} is{'\n'} + {JSON.stringify(testFlag)} +
Edit App.tsx to change this screen and then come back to see your edits. diff --git a/example/src/WixApp.tsx b/example/src/WixApp.tsx index ada2ae9c6..8664c3e3d 100644 --- a/example/src/WixApp.tsx +++ b/example/src/WixApp.tsx @@ -1,5 +1,5 @@ -import React, { useEffect, useState } from 'react'; -import { View, Text, Button } from 'react-native'; +import React from 'react'; +import { View, Text, Button, ActivityIndicator } from 'react-native'; import MainScreen from './screens/MainScreen'; import ErrorScreen from './screens/ErrorScreen'; import AboutScreen from './screens/AboutScreen'; @@ -70,23 +70,38 @@ function registerScreens() { } const HomeScreen = props => { - const [testFlagValue, setTestFlagValue] = useState(false); - useEffect(() => { - (async () => { - await DdFlags.enable(); - - const flagsClient = DdFlags.getClient(); - await flagsClient.setEvaluationContext({ - targetingKey: 'test-user-1', - attributes: { - country: 'US', - }, - }); - const flag = await flagsClient.getBooleanDetails('rn-sdk-test-boolean-flag', false); // https://app.datadoghq.com/feature-flags/046d0e70-626d-41e1-8314-3f009fb79b7a?environmentId=d114cd9a-79ed-4c56-bcf3-bcac9293653b - setTestFlagValue(flag.value); - })(); + const [isInitialized, setIsInitialized] = React.useState(false); + + React.useEffect(() => { + (async () => { + // This is a blocking async app initialization effect. + // It simulates the way most React Native applications are initialized. + await DdFlags.enable(); + const client = DdFlags.getClient(); + + const userId = 'test-user-1'; + const userAttributes = { + country: 'US', + }; + + await client.setEvaluationContext({targetingKey: userId, attributes: userAttributes}); + + setIsInitialized(true); + })().catch(console.error); }, []); + if (!isInitialized) { + return ( + + + + ) + } + + // TODO: [FFL-908] Use OpenFeature SDK instead of a manual client call. + const testFlagKey = 'rn-sdk-test-json-flag'; + const testFlag = DdFlags.getClient().getObjectValue(testFlagKey, {greeting: "Default greeting"}); // https://app.datadoghq.com/feature-flags/bcf75cd6-96d8-4182-8871-0b66ad76127a?environmentId=d114cd9a-79ed-4c56-bcf3-bcac9293653b + return ( @@ -132,7 +147,7 @@ const HomeScreen = props => { }); }} /> - rn-sdk-test-boolean-flag: {String(testFlagValue)} + {testFlagKey}: {JSON.stringify(testFlag)} ); }; diff --git a/example/src/screens/MainScreen.tsx b/example/src/screens/MainScreen.tsx index 7365faf1b..ebf0360bc 100644 --- a/example/src/screens/MainScreen.tsx +++ b/example/src/screens/MainScreen.tsx @@ -7,7 +7,7 @@ import React, { Component, RefObject } from 'react'; import { View, Text, Button, TouchableOpacity, - TouchableWithoutFeedback, TouchableNativeFeedback + TouchableWithoutFeedback, TouchableNativeFeedback, ActivityIndicator } from 'react-native'; import styles from './styles'; import { APPLICATION_KEY, API_KEY } from '../../src/ddCredentials'; @@ -27,7 +27,7 @@ interface MainScreenState { resultTouchableNativeFeedback: string, trackingConsent: TrackingConsent, trackingConsentModalVisible: boolean - testFlagValue: boolean + flagsInitialized: boolean } export default class MainScreen extends Component { @@ -42,7 +42,7 @@ export default class MainScreen extends Component { resultTouchableOpacityAction: "", trackingConsent: TrackingConsent.PENDING, trackingConsentModalVisible: false, - testFlagValue: false + flagsInitialized: false } as MainScreenState; this.consentModal = React.createRef() } @@ -96,7 +96,7 @@ export default class MainScreen extends Component { componentDidMount() { this.updateTrackingConsent() - this.fetchBooleanFlag(); + this.initializeFlags(); DdLogs.debug("[DATADOG SDK] Test React Native Debug Log"); } @@ -108,20 +108,21 @@ export default class MainScreen extends Component { }) } - fetchBooleanFlag() { + initializeFlags() { (async () => { + // This is a blocking async app initialization effect. + // It simulates the way most React Native applications are initialized. await DdFlags.enable(); + const client = DdFlags.getClient(); - const flagsClient = DdFlags.getClient(); - await flagsClient.setEvaluationContext({ - targetingKey: 'test-user-1', - attributes: { - country: 'US', - }, - }); - const flag = await flagsClient.getBooleanDetails('rn-sdk-test-boolean-flag', false); // https://app.datadoghq.com/feature-flags/046d0e70-626d-41e1-8314-3f009fb79b7a?environmentId=d114cd9a-79ed-4c56-bcf3-bcac9293653b - console.log({flag}) - this.setState({ testFlagValue: flag.value }) + const userId = 'test-user-1'; + const userAttributes = { + country: 'US', + }; + + await client.setEvaluationContext({targetingKey: userId, attributes: userAttributes}); + + this.setState({ flagsInitialized: true }) })(); } @@ -133,6 +134,16 @@ export default class MainScreen extends Component { } render() { + if (!this.state.flagsInitialized) { + return + + + } + + // TODO: [FFL-908] Use OpenFeature SDK instead of a manual client call. + const testFlagKey = 'rn-sdk-test-json-flag'; + const testFlag = DdFlags.getClient().getObjectValue(testFlagKey, {greeting: "Default greeting"}); // https://app.datadoghq.com/feature-flags/bcf75cd6-96d8-4182-8871-0b66ad76127a?environmentId=d114cd9a-79ed-4c56-bcf3-bcac9293653b + return {this.state.welcomeMessage} @@ -225,7 +236,7 @@ export default class MainScreen extends Component { Click me (error) - rn-sdk-test-boolean-flag: {String(this.state.testFlagValue)} + {testFlagKey}: {JSON.stringify(testFlag)} } From 1cb346b8e95a74b7406cf505f6d1b4a43d5d86c6 Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Mon, 12 Jan 2026 15:21:49 +0200 Subject: [PATCH 59/64] Fix React Native hot reload issue --- packages/core/ios/Sources/DdFlagsImplementation.swift | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/core/ios/Sources/DdFlagsImplementation.swift b/packages/core/ios/Sources/DdFlagsImplementation.swift index d2b8e1793..f2e4cc59f 100644 --- a/packages/core/ios/Sources/DdFlagsImplementation.swift +++ b/packages/core/ios/Sources/DdFlagsImplementation.swift @@ -27,6 +27,9 @@ public class DdFlagsImplementation: NSObject { @objc public func enable(_ configuration: NSDictionary, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) { + // Client providers become stale upon subsequent enable calls (which can happen e.g. in case of a React Native hot reload). + clientProviders.removeAll() + if let config = configuration.asFlagsConfiguration() { Flags.enable(with: config) } else { @@ -40,9 +43,6 @@ public class DdFlagsImplementation: NSObject { /// /// We create a simple registry of client providers by client name holding closures for retrieving a client since client references are kept internally in the flagging SDK. /// This is motivated by the fact that it is impossible to create a bridged synchronous `FlagsClient` creation; thus, we create a client instance dynamically on-demand. - /// - /// - Important: Due to specifics of React Native hot reloading, this registry is destroyed upon JS bundle refresh. This leads to`FlagsClient.create` being called several times during development process for the same client. - /// This should not be a problem because `gracefulModeEnabled` is hard set to `true` for the RN SDK. private func getClient(name: String) -> FlagsClientProtocol { if let provider = clientProviders[name] { return provider() @@ -56,7 +56,8 @@ public class DdFlagsImplementation: NSObject { @objc public func setEvaluationContext(_ clientName: String, targetingKey: String, attributes: NSDictionary, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { let client = getClient(name: clientName) - guard let clientInternal = getClient(name: clientName) as? FlagsClientInternal else { + guard let clientInternal = client as? FlagsClientInternal else { + reject(nil, "CLIENT_NOT_INITIALIZED", nil) return } From c6961cc60e1326de46be2795be525dedeeea4b47 Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Mon, 12 Jan 2026 15:28:29 +0200 Subject: [PATCH 60/64] Bump Datadog SDK version to 3.5.0 --- benchmarks/android/app/build.gradle | 2 +- packages/core/android/build.gradle | 12 ++++++------ .../react-native-session-replay/android/build.gradle | 4 ++-- packages/react-native-webview/android/build.gradle | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/benchmarks/android/app/build.gradle b/benchmarks/android/app/build.gradle index 6e111a267..c674c588d 100644 --- a/benchmarks/android/app/build.gradle +++ b/benchmarks/android/app/build.gradle @@ -129,5 +129,5 @@ dependencies { // Benchmark tools from dd-sdk-android are used for vitals recording // Remember to bump thid alongside the main dd-sdk-android dependencies - implementation("com.datadoghq:dd-sdk-android-benchmark-internal:3.4.0") + implementation("com.datadoghq:dd-sdk-android-benchmark-internal:3.5.0") } diff --git a/packages/core/android/build.gradle b/packages/core/android/build.gradle index 3d3b9ee70..3407a6e99 100644 --- a/packages/core/android/build.gradle +++ b/packages/core/android/build.gradle @@ -200,17 +200,17 @@ dependencies { // This breaks builds if the React Native target is below 0.76.0. as it relies on Gradle 8.5.0. // To avoid this, we enforce 1.0.0-beta01 on RN < 0.76.0 if (reactNativeMinorVersion < 76) { - implementation("com.datadoghq:dd-sdk-android-rum:3.4.0") { + implementation("com.datadoghq:dd-sdk-android-rum:3.5.0") { exclude group: "androidx.metrics", module: "metrics-performance" } implementation "androidx.metrics:metrics-performance:1.0.0-beta01" } else { - implementation "com.datadoghq:dd-sdk-android-rum:3.4.0" + implementation "com.datadoghq:dd-sdk-android-rum:3.5.0" } - implementation "com.datadoghq:dd-sdk-android-logs:3.4.0" - implementation "com.datadoghq:dd-sdk-android-trace:3.4.0" - implementation "com.datadoghq:dd-sdk-android-webview:3.4.0" - implementation "com.datadoghq:dd-sdk-android-flags:3.4.0" + implementation "com.datadoghq:dd-sdk-android-logs:3.5.0" + implementation "com.datadoghq:dd-sdk-android-trace:3.5.0" + implementation "com.datadoghq:dd-sdk-android-webview:3.5.0" + implementation "com.datadoghq:dd-sdk-android-flags:3.5.0" implementation "com.google.code.gson:gson:2.10.0" testImplementation "org.junit.platform:junit-platform-launcher:1.6.2" testImplementation "org.junit.jupiter:junit-jupiter-api:5.6.2" diff --git a/packages/react-native-session-replay/android/build.gradle b/packages/react-native-session-replay/android/build.gradle index 745e6e6a9..8c497076a 100644 --- a/packages/react-native-session-replay/android/build.gradle +++ b/packages/react-native-session-replay/android/build.gradle @@ -216,8 +216,8 @@ dependencies { api "com.facebook.react:react-android:$reactNativeVersion" } implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" - implementation "com.datadoghq:dd-sdk-android-session-replay:3.4.0" - implementation "com.datadoghq:dd-sdk-android-internal:3.4.0" + implementation "com.datadoghq:dd-sdk-android-session-replay:3.5.0" + implementation "com.datadoghq:dd-sdk-android-internal:3.5.0" implementation project(path: ':datadog_mobile-react-native') testImplementation "org.junit.platform:junit-platform-launcher:1.6.2" diff --git a/packages/react-native-webview/android/build.gradle b/packages/react-native-webview/android/build.gradle index bebe19aab..8590cd73a 100644 --- a/packages/react-native-webview/android/build.gradle +++ b/packages/react-native-webview/android/build.gradle @@ -190,7 +190,7 @@ dependencies { implementation "com.facebook.react:react-android:$reactNativeVersion" } - implementation "com.datadoghq:dd-sdk-android-webview:3.4.0" + implementation "com.datadoghq:dd-sdk-android-webview:3.5.0" implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation project(path: ':datadog_mobile-react-native') From e766f27a6a8c8292ec6487adda08e960b1a9346e Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Tue, 13 Jan 2026 10:37:08 +0200 Subject: [PATCH 61/64] Fix failing iOS tests --- example-new-architecture/ios/Podfile.lock | 2 +- example/ios/Podfile.lock | 2 +- packages/core/ios/Tests/MockRUMMonitor.swift | 4 ++++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/example-new-architecture/ios/Podfile.lock b/example-new-architecture/ios/Podfile.lock index aa251c95a..705d74223 100644 --- a/example-new-architecture/ios/Podfile.lock +++ b/example-new-architecture/ios/Podfile.lock @@ -1981,6 +1981,6 @@ SPEC CHECKSUMS: SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 Yoga: feb4910aba9742cfedc059e2b2902e22ffe9954a -PODFILE CHECKSUM: 211d0907a4e052bce20f507daf19b2cde45a32d6 +PODFILE CHECKSUM: 59de451ccfcc598b8df4b2959f4ae2bb20a5794b COCOAPODS: 1.16.2 diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 292cedc78..804f35470 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -2130,6 +2130,6 @@ SPEC CHECKSUMS: SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 Yoga: feb4910aba9742cfedc059e2b2902e22ffe9954a -PODFILE CHECKSUM: 6309cef88a0921166cdd8af528b6f360fb4fe89b +PODFILE CHECKSUM: d580bf1cdb494910909c614e1f594c0cdf6cdb3f COCOAPODS: 1.16.2 diff --git a/packages/core/ios/Tests/MockRUMMonitor.swift b/packages/core/ios/Tests/MockRUMMonitor.swift index 4a1d0cd92..3d698c50d 100644 --- a/packages/core/ios/Tests/MockRUMMonitor.swift +++ b/packages/core/ios/Tests/MockRUMMonitor.swift @@ -10,6 +10,10 @@ @testable import DatadogSDKReactNative internal class MockRUMMonitor: RUMMonitorProtocol { + func reportAppFullyDisplayed() { + // not implemented + } + func currentSessionID(completion: @escaping (String?) -> Void) { // not implemented } From 2cc11d65db81afd5421d1066740190b7d40eea20 Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Wed, 14 Jan 2026 15:28:45 +0200 Subject: [PATCH 62/64] Bump iOS SDK to 3.5.0 --- example-new-architecture/ios/Podfile | 11 -- example-new-architecture/ios/Podfile.lock | 163 ++++++--------- example/ios/Podfile | 11 -- example/ios/Podfile.lock | 185 +++++++----------- packages/core/DatadogSDKReactNative.podspec | 14 +- ...DatadogSDKReactNativeSessionReplay.podspec | 2 +- .../DatadogSDKReactNativeWebView.podspec | 4 +- 7 files changed, 145 insertions(+), 245 deletions(-) diff --git a/example-new-architecture/ios/Podfile b/example-new-architecture/ios/Podfile index affd2fd5d..3c29ed272 100644 --- a/example-new-architecture/ios/Podfile +++ b/example-new-architecture/ios/Podfile @@ -19,17 +19,6 @@ end target 'DdSdkReactNativeExample' do pod 'DatadogSDKReactNative', :path => '../../packages/core/DatadogSDKReactNative.podspec', :testspecs => ['Tests'] - # TODO: Remove this once the 3.5.0 release is cut. - # Pin Datadog* dependencies to a specific commit. - pod 'DatadogInternal', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :commit => '2f28ab9' - pod 'DatadogCore', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :commit => '2f28ab9' - pod 'DatadogLogs', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :commit => '2f28ab9' - pod 'DatadogTrace', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :commit => '2f28ab9' - pod 'DatadogRUM', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :commit => '2f28ab9' - pod 'DatadogCrashReporting', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :commit => '2f28ab9' - pod 'DatadogFlags', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :commit => '2f28ab9' - pod 'DatadogWebViewTracking', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :commit => '2f28ab9' - config = use_native_modules! use_react_native!( diff --git a/example-new-architecture/ios/Podfile.lock b/example-new-architecture/ios/Podfile.lock index 705d74223..05c1392ee 100644 --- a/example-new-architecture/ios/Podfile.lock +++ b/example-new-architecture/ios/Podfile.lock @@ -1,25 +1,26 @@ PODS: - boost (1.84.0) - - DatadogCore (3.4.0): - - DatadogInternal (= 3.4.0) - - DatadogCrashReporting (3.4.0): - - DatadogInternal (= 3.4.0) - - PLCrashReporter (~> 1.12.0) - - DatadogFlags (3.4.0): - - DatadogInternal (= 3.4.0) - - DatadogInternal (3.4.0) - - DatadogLogs (3.4.0): - - DatadogInternal (= 3.4.0) - - DatadogRUM (3.4.0): - - DatadogInternal (= 3.4.0) + - DatadogCore (3.5.0): + - DatadogInternal (= 3.5.0) + - DatadogCrashReporting (3.5.0): + - DatadogInternal (= 3.5.0) + - KSCrash/Filters (= 2.5.0) + - KSCrash/Recording (= 2.5.0) + - DatadogFlags (3.5.0): + - DatadogInternal (= 3.5.0) + - DatadogInternal (3.5.0) + - DatadogLogs (3.5.0): + - DatadogInternal (= 3.5.0) + - DatadogRUM (3.5.0): + - DatadogInternal (= 3.5.0) - DatadogSDKReactNative (2.13.2): - - DatadogCore (= 3.4.0) - - DatadogCrashReporting (= 3.4.0) - - DatadogFlags (= 3.4.0) - - DatadogLogs (= 3.4.0) - - DatadogRUM (= 3.4.0) - - DatadogTrace (= 3.4.0) - - DatadogWebViewTracking (= 3.4.0) + - DatadogCore (= 3.5.0) + - DatadogCrashReporting (= 3.5.0) + - DatadogFlags (= 3.5.0) + - DatadogLogs (= 3.5.0) + - DatadogRUM (= 3.5.0) + - DatadogTrace (= 3.5.0) + - DatadogWebViewTracking (= 3.5.0) - DoubleConversion - glog - hermes-engine @@ -41,13 +42,13 @@ PODS: - ReactCommon/turbomodule/core - Yoga - DatadogSDKReactNative/Tests (2.13.2): - - DatadogCore (= 3.4.0) - - DatadogCrashReporting (= 3.4.0) - - DatadogFlags (= 3.4.0) - - DatadogLogs (= 3.4.0) - - DatadogRUM (= 3.4.0) - - DatadogTrace (= 3.4.0) - - DatadogWebViewTracking (= 3.4.0) + - DatadogCore (= 3.5.0) + - DatadogCrashReporting (= 3.5.0) + - DatadogFlags (= 3.5.0) + - DatadogLogs (= 3.5.0) + - DatadogRUM (= 3.5.0) + - DatadogTrace (= 3.5.0) + - DatadogWebViewTracking (= 3.5.0) - DoubleConversion - glog - hermes-engine @@ -68,11 +69,11 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - DatadogTrace (3.4.0): - - DatadogInternal (= 3.4.0) + - DatadogTrace (3.5.0): + - DatadogInternal (= 3.5.0) - OpenTelemetry-Swift-Api (~> 2.3.0) - - DatadogWebViewTracking (3.4.0): - - DatadogInternal (= 3.4.0) + - DatadogWebViewTracking (3.5.0): + - DatadogInternal (= 3.5.0) - DoubleConversion (1.1.6) - fast_float (6.1.4) - FBLazyVector (0.76.9) @@ -81,8 +82,18 @@ PODS: - hermes-engine (0.76.9): - hermes-engine/Pre-built (= 0.76.9) - hermes-engine/Pre-built (0.76.9) + - KSCrash/Core (2.5.0) + - KSCrash/Filters (2.5.0): + - KSCrash/Recording + - KSCrash/RecordingCore + - KSCrash/ReportingCore + - KSCrash/Recording (2.5.0): + - KSCrash/RecordingCore + - KSCrash/RecordingCore (2.5.0): + - KSCrash/Core + - KSCrash/ReportingCore (2.5.0): + - KSCrash/Core - OpenTelemetry-Swift-Api (2.3.0) - - PLCrashReporter (1.12.0) - RCT-Folly (2024.10.14.00): - boost - DoubleConversion @@ -1638,16 +1649,8 @@ PODS: DEPENDENCIES: - boost (from `../node_modules/react-native/third-party-podspecs/boost.podspec`) - - DatadogCore (from `https://github.com/DataDog/dd-sdk-ios.git`, commit `2f28ab9`) - - DatadogCrashReporting (from `https://github.com/DataDog/dd-sdk-ios.git`, commit `2f28ab9`) - - DatadogFlags (from `https://github.com/DataDog/dd-sdk-ios.git`, commit `2f28ab9`) - - DatadogInternal (from `https://github.com/DataDog/dd-sdk-ios.git`, commit `2f28ab9`) - - DatadogLogs (from `https://github.com/DataDog/dd-sdk-ios.git`, commit `2f28ab9`) - - DatadogRUM (from `https://github.com/DataDog/dd-sdk-ios.git`, commit `2f28ab9`) - DatadogSDKReactNative (from `../../packages/core/DatadogSDKReactNative.podspec`) - DatadogSDKReactNative/Tests (from `../../packages/core/DatadogSDKReactNative.podspec`) - - DatadogTrace (from `https://github.com/DataDog/dd-sdk-ios.git`, commit `2f28ab9`) - - DatadogWebViewTracking (from `https://github.com/DataDog/dd-sdk-ios.git`, commit `2f28ab9`) - DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`) - fast_float (from `../node_modules/react-native/third-party-podspecs/fast_float.podspec`) - FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`) @@ -1716,39 +1719,23 @@ DEPENDENCIES: SPEC REPOS: https://github.com/CocoaPods/Specs.git: + - DatadogCore + - DatadogCrashReporting + - DatadogFlags + - DatadogInternal + - DatadogLogs + - DatadogRUM + - DatadogTrace + - DatadogWebViewTracking + - KSCrash - OpenTelemetry-Swift-Api - - PLCrashReporter - SocketRocket EXTERNAL SOURCES: boost: :podspec: "../node_modules/react-native/third-party-podspecs/boost.podspec" - DatadogCore: - :commit: 2f28ab9 - :git: https://github.com/DataDog/dd-sdk-ios.git - DatadogCrashReporting: - :commit: 2f28ab9 - :git: https://github.com/DataDog/dd-sdk-ios.git - DatadogFlags: - :commit: 2f28ab9 - :git: https://github.com/DataDog/dd-sdk-ios.git - DatadogInternal: - :commit: 2f28ab9 - :git: https://github.com/DataDog/dd-sdk-ios.git - DatadogLogs: - :commit: 2f28ab9 - :git: https://github.com/DataDog/dd-sdk-ios.git - DatadogRUM: - :commit: 2f28ab9 - :git: https://github.com/DataDog/dd-sdk-ios.git DatadogSDKReactNative: :path: "../../packages/core/DatadogSDKReactNative.podspec" - DatadogTrace: - :commit: 2f28ab9 - :git: https://github.com/DataDog/dd-sdk-ios.git - DatadogWebViewTracking: - :commit: 2f28ab9 - :git: https://github.com/DataDog/dd-sdk-ios.git DoubleConversion: :podspec: "../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec" fast_float: @@ -1877,51 +1864,25 @@ EXTERNAL SOURCES: Yoga: :path: "../node_modules/react-native/ReactCommon/yoga" -CHECKOUT OPTIONS: - DatadogCore: - :commit: 2f28ab9 - :git: https://github.com/DataDog/dd-sdk-ios.git - DatadogCrashReporting: - :commit: 2f28ab9 - :git: https://github.com/DataDog/dd-sdk-ios.git - DatadogFlags: - :commit: 2f28ab9 - :git: https://github.com/DataDog/dd-sdk-ios.git - DatadogInternal: - :commit: 2f28ab9 - :git: https://github.com/DataDog/dd-sdk-ios.git - DatadogLogs: - :commit: 2f28ab9 - :git: https://github.com/DataDog/dd-sdk-ios.git - DatadogRUM: - :commit: 2f28ab9 - :git: https://github.com/DataDog/dd-sdk-ios.git - DatadogTrace: - :commit: 2f28ab9 - :git: https://github.com/DataDog/dd-sdk-ios.git - DatadogWebViewTracking: - :commit: 2f28ab9 - :git: https://github.com/DataDog/dd-sdk-ios.git - SPEC CHECKSUMS: boost: 1dca942403ed9342f98334bf4c3621f011aa7946 - DatadogCore: 8c384b6338c49534e43fdf7f9a0508b62bf1d426 - DatadogCrashReporting: 103bfb4077db2ccee1846f71e53712972732d3b7 - DatadogFlags: fbc8dc0e5e387c6c8e15a0afa39d067107e2e7ce - DatadogInternal: b0372935ad8dde5ad06960fe8d88c39b2cc92bcc - DatadogLogs: 484bb1bfe0c9a7cb2a7d9733f61614e8ea7b2f3a - DatadogRUM: 00069b27918e0ce4a9223b87b4bfa7929d6a0a1f - DatadogSDKReactNative: 7625fa42b4600102f01a3eee16162db6e51d86ff - DatadogTrace: 68b7cc49378ba492423191c29280e3904978a684 - DatadogWebViewTracking: 32dfeaf7aad47a605a689ed12e0d21ee8eb56141 + DatadogCore: 4cbe2646591d2f96fb3188400863ec93ac411235 + DatadogCrashReporting: e48da3f880a59d2aa2d04e5034e56507177e9d64 + DatadogFlags: f8cf88371460d6c672abfd97fdc9af5be208f33b + DatadogInternal: 63308b529cd87fb2f99c5961d9ff13afb300a3aa + DatadogLogs: be538def1d5204e011f7952915ad0261014a0dd5 + DatadogRUM: cffc65659ce29546fcc2639a74003135259548fc + DatadogSDKReactNative: 5d210f3aa609cec39909ecc3d378d950b69172fe + DatadogTrace: 085e35f9e4889f82f8a747922c58ea4b19728720 + DatadogWebViewTracking: 61b8344da898cbaccffc75bc1a17c86175e8573a DoubleConversion: f16ae600a246532c4020132d54af21d0ddb2a385 fast_float: 06eeec4fe712a76acc9376682e4808b05ce978b6 FBLazyVector: 7605ea4810e0e10ae4815292433c09bf4324ba45 fmt: 01b82d4ca6470831d1cc0852a1af644be019e8f6 glog: 08b301085f15bcbb6ff8632a8ebaf239aae04e6a hermes-engine: 9e868dc7be781364296d6ee2f56d0c1a9ef0bb11 + KSCrash: 80e1e24eaefbe5134934ae11ca8d7746586bc2ed OpenTelemetry-Swift-Api: 3d77582ab6837a63b65bf7d2eacc57d8f2595edd - PLCrashReporter: db59ef96fa3d25f3650040d02ec2798cffee75f2 RCT-Folly: 7b4f73a92ad9571b9dbdb05bb30fad927fa971e1 RCTDeprecation: ebe712bb05077934b16c6bf25228bdec34b64f83 RCTRequired: ca91e5dd26b64f577b528044c962baf171c6b716 @@ -1981,6 +1942,6 @@ SPEC CHECKSUMS: SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 Yoga: feb4910aba9742cfedc059e2b2902e22ffe9954a -PODFILE CHECKSUM: 59de451ccfcc598b8df4b2959f4ae2bb20a5794b +PODFILE CHECKSUM: 2046fc46dd3311048c09b49573c69b7aba2aab81 COCOAPODS: 1.16.2 diff --git a/example/ios/Podfile b/example/ios/Podfile index 03c374385..7484e6aa7 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -21,17 +21,6 @@ target 'ddSdkReactnativeExample' do pod 'DatadogSDKReactNativeSessionReplay', :path => '../../packages/react-native-session-replay/DatadogSDKReactNativeSessionReplay.podspec', :testspecs => ['Tests'] pod 'DatadogSDKReactNativeWebView', :path => '../../packages/react-native-webview/DatadogSDKReactNativeWebView.podspec', :testspecs => ['Tests'] - # TODO: Remove this once the 3.5.0 release is cut. - # Pin Datadog* dependencies to a specific commit. - pod 'DatadogInternal', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :commit => '2f28ab9' - pod 'DatadogCore', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :commit => '2f28ab9' - pod 'DatadogLogs', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :commit => '2f28ab9' - pod 'DatadogTrace', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :commit => '2f28ab9' - pod 'DatadogRUM', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :commit => '2f28ab9' - pod 'DatadogCrashReporting', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :commit => '2f28ab9' - pod 'DatadogFlags', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :commit => '2f28ab9' - pod 'DatadogWebViewTracking', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :commit => '2f28ab9' - config = use_native_modules! use_react_native!( diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 804f35470..a6ff19355 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -1,38 +1,39 @@ PODS: - boost (1.84.0) - - DatadogCore (3.4.0): - - DatadogInternal (= 3.4.0) - - DatadogCrashReporting (3.4.0): - - DatadogInternal (= 3.4.0) - - PLCrashReporter (~> 1.12.0) - - DatadogFlags (3.4.0): - - DatadogInternal (= 3.4.0) - - DatadogInternal (3.4.0) - - DatadogLogs (3.4.0): - - DatadogInternal (= 3.4.0) - - DatadogRUM (3.4.0): - - DatadogInternal (= 3.4.0) + - DatadogCore (3.5.0): + - DatadogInternal (= 3.5.0) + - DatadogCrashReporting (3.5.0): + - DatadogInternal (= 3.5.0) + - KSCrash/Filters (= 2.5.0) + - KSCrash/Recording (= 2.5.0) + - DatadogFlags (3.5.0): + - DatadogInternal (= 3.5.0) + - DatadogInternal (3.5.0) + - DatadogLogs (3.5.0): + - DatadogInternal (= 3.5.0) + - DatadogRUM (3.5.0): + - DatadogInternal (= 3.5.0) - DatadogSDKReactNative (2.13.2): - - DatadogCore (= 3.4.0) - - DatadogCrashReporting (= 3.4.0) - - DatadogFlags (= 3.4.0) - - DatadogLogs (= 3.4.0) - - DatadogRUM (= 3.4.0) - - DatadogTrace (= 3.4.0) - - DatadogWebViewTracking (= 3.4.0) + - DatadogCore (= 3.5.0) + - DatadogCrashReporting (= 3.5.0) + - DatadogFlags (= 3.5.0) + - DatadogLogs (= 3.5.0) + - DatadogRUM (= 3.5.0) + - DatadogTrace (= 3.5.0) + - DatadogWebViewTracking (= 3.5.0) - React-Core - DatadogSDKReactNative/Tests (2.13.2): - - DatadogCore (= 3.4.0) - - DatadogCrashReporting (= 3.4.0) - - DatadogFlags (= 3.4.0) - - DatadogLogs (= 3.4.0) - - DatadogRUM (= 3.4.0) - - DatadogTrace (= 3.4.0) - - DatadogWebViewTracking (= 3.4.0) + - DatadogCore (= 3.5.0) + - DatadogCrashReporting (= 3.5.0) + - DatadogFlags (= 3.5.0) + - DatadogLogs (= 3.5.0) + - DatadogRUM (= 3.5.0) + - DatadogTrace (= 3.5.0) + - DatadogWebViewTracking (= 3.5.0) - React-Core - DatadogSDKReactNativeSessionReplay (2.13.2): - DatadogSDKReactNative - - DatadogSessionReplay (= 3.4.0) + - DatadogSessionReplay (= 3.5.0) - DoubleConversion - glog - hermes-engine @@ -55,7 +56,7 @@ PODS: - Yoga - DatadogSDKReactNativeSessionReplay/Tests (2.13.2): - DatadogSDKReactNative - - DatadogSessionReplay (= 3.4.0) + - DatadogSessionReplay (= 3.5.0) - DoubleConversion - glog - hermes-engine @@ -78,24 +79,24 @@ PODS: - ReactCommon/turbomodule/core - Yoga - DatadogSDKReactNativeWebView (2.13.2): - - DatadogInternal (= 3.4.0) + - DatadogInternal (= 3.5.0) - DatadogSDKReactNative - - DatadogWebViewTracking (= 3.4.0) + - DatadogWebViewTracking (= 3.5.0) - React-Core - DatadogSDKReactNativeWebView/Tests (2.13.2): - - DatadogInternal (= 3.4.0) + - DatadogInternal (= 3.5.0) - DatadogSDKReactNative - - DatadogWebViewTracking (= 3.4.0) + - DatadogWebViewTracking (= 3.5.0) - React-Core - react-native-webview - React-RCTText - - DatadogSessionReplay (3.4.0): - - DatadogInternal (= 3.4.0) - - DatadogTrace (3.4.0): - - DatadogInternal (= 3.4.0) + - DatadogSessionReplay (3.5.0): + - DatadogInternal (= 3.5.0) + - DatadogTrace (3.5.0): + - DatadogInternal (= 3.5.0) - OpenTelemetry-Swift-Api (~> 2.3.0) - - DatadogWebViewTracking (3.4.0): - - DatadogInternal (= 3.4.0) + - DatadogWebViewTracking (3.5.0): + - DatadogInternal (= 3.5.0) - DoubleConversion (1.1.6) - fast_float (6.1.4) - FBLazyVector (0.76.9) @@ -105,8 +106,18 @@ PODS: - hermes-engine/Pre-built (= 0.76.9) - hermes-engine/Pre-built (0.76.9) - HMSegmentedControl (1.5.6) + - KSCrash/Core (2.5.0) + - KSCrash/Filters (2.5.0): + - KSCrash/Recording + - KSCrash/RecordingCore + - KSCrash/ReportingCore + - KSCrash/Recording (2.5.0): + - KSCrash/RecordingCore + - KSCrash/RecordingCore (2.5.0): + - KSCrash/Core + - KSCrash/ReportingCore (2.5.0): + - KSCrash/Core - OpenTelemetry-Swift-Api (2.3.0) - - PLCrashReporter (1.12.0) - RCT-Folly (2024.10.14.00): - boost - DoubleConversion @@ -1745,20 +1756,12 @@ PODS: DEPENDENCIES: - boost (from `../node_modules/react-native/third-party-podspecs/boost.podspec`) - - DatadogCore (from `https://github.com/DataDog/dd-sdk-ios.git`, commit `2f28ab9`) - - DatadogCrashReporting (from `https://github.com/DataDog/dd-sdk-ios.git`, commit `2f28ab9`) - - DatadogFlags (from `https://github.com/DataDog/dd-sdk-ios.git`, commit `2f28ab9`) - - DatadogInternal (from `https://github.com/DataDog/dd-sdk-ios.git`, commit `2f28ab9`) - - DatadogLogs (from `https://github.com/DataDog/dd-sdk-ios.git`, commit `2f28ab9`) - - DatadogRUM (from `https://github.com/DataDog/dd-sdk-ios.git`, commit `2f28ab9`) - DatadogSDKReactNative (from `../../packages/core/DatadogSDKReactNative.podspec`) - DatadogSDKReactNative/Tests (from `../../packages/core/DatadogSDKReactNative.podspec`) - DatadogSDKReactNativeSessionReplay (from `../../packages/react-native-session-replay/DatadogSDKReactNativeSessionReplay.podspec`) - DatadogSDKReactNativeSessionReplay/Tests (from `../../packages/react-native-session-replay/DatadogSDKReactNativeSessionReplay.podspec`) - DatadogSDKReactNativeWebView (from `../../packages/react-native-webview/DatadogSDKReactNativeWebView.podspec`) - DatadogSDKReactNativeWebView/Tests (from `../../packages/react-native-webview/DatadogSDKReactNativeWebView.podspec`) - - DatadogTrace (from `https://github.com/DataDog/dd-sdk-ios.git`, commit `2f28ab9`) - - DatadogWebViewTracking (from `https://github.com/DataDog/dd-sdk-ios.git`, commit `2f28ab9`) - DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`) - fast_float (from `../node_modules/react-native/third-party-podspecs/fast_float.podspec`) - FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`) @@ -1834,45 +1837,29 @@ DEPENDENCIES: SPEC REPOS: https://github.com/CocoaPods/Specs.git: + - DatadogCore + - DatadogCrashReporting + - DatadogFlags + - DatadogInternal + - DatadogLogs + - DatadogRUM - DatadogSessionReplay + - DatadogTrace + - DatadogWebViewTracking - HMSegmentedControl + - KSCrash - OpenTelemetry-Swift-Api - - PLCrashReporter - SocketRocket EXTERNAL SOURCES: boost: :podspec: "../node_modules/react-native/third-party-podspecs/boost.podspec" - DatadogCore: - :commit: 2f28ab9 - :git: https://github.com/DataDog/dd-sdk-ios.git - DatadogCrashReporting: - :commit: 2f28ab9 - :git: https://github.com/DataDog/dd-sdk-ios.git - DatadogFlags: - :commit: 2f28ab9 - :git: https://github.com/DataDog/dd-sdk-ios.git - DatadogInternal: - :commit: 2f28ab9 - :git: https://github.com/DataDog/dd-sdk-ios.git - DatadogLogs: - :commit: 2f28ab9 - :git: https://github.com/DataDog/dd-sdk-ios.git - DatadogRUM: - :commit: 2f28ab9 - :git: https://github.com/DataDog/dd-sdk-ios.git DatadogSDKReactNative: :path: "../../packages/core/DatadogSDKReactNative.podspec" DatadogSDKReactNativeSessionReplay: :path: "../../packages/react-native-session-replay/DatadogSDKReactNativeSessionReplay.podspec" DatadogSDKReactNativeWebView: :path: "../../packages/react-native-webview/DatadogSDKReactNativeWebView.podspec" - DatadogTrace: - :commit: 2f28ab9 - :git: https://github.com/DataDog/dd-sdk-ios.git - DatadogWebViewTracking: - :commit: 2f28ab9 - :git: https://github.com/DataDog/dd-sdk-ios.git DoubleConversion: :podspec: "../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec" fast_float: @@ -2015,46 +2002,20 @@ EXTERNAL SOURCES: Yoga: :path: "../node_modules/react-native/ReactCommon/yoga" -CHECKOUT OPTIONS: - DatadogCore: - :commit: 2f28ab9 - :git: https://github.com/DataDog/dd-sdk-ios.git - DatadogCrashReporting: - :commit: 2f28ab9 - :git: https://github.com/DataDog/dd-sdk-ios.git - DatadogFlags: - :commit: 2f28ab9 - :git: https://github.com/DataDog/dd-sdk-ios.git - DatadogInternal: - :commit: 2f28ab9 - :git: https://github.com/DataDog/dd-sdk-ios.git - DatadogLogs: - :commit: 2f28ab9 - :git: https://github.com/DataDog/dd-sdk-ios.git - DatadogRUM: - :commit: 2f28ab9 - :git: https://github.com/DataDog/dd-sdk-ios.git - DatadogTrace: - :commit: 2f28ab9 - :git: https://github.com/DataDog/dd-sdk-ios.git - DatadogWebViewTracking: - :commit: 2f28ab9 - :git: https://github.com/DataDog/dd-sdk-ios.git - SPEC CHECKSUMS: boost: 1dca942403ed9342f98334bf4c3621f011aa7946 - DatadogCore: 8c384b6338c49534e43fdf7f9a0508b62bf1d426 - DatadogCrashReporting: 103bfb4077db2ccee1846f71e53712972732d3b7 - DatadogFlags: fbc8dc0e5e387c6c8e15a0afa39d067107e2e7ce - DatadogInternal: b0372935ad8dde5ad06960fe8d88c39b2cc92bcc - DatadogLogs: 484bb1bfe0c9a7cb2a7d9733f61614e8ea7b2f3a - DatadogRUM: 00069b27918e0ce4a9223b87b4bfa7929d6a0a1f - DatadogSDKReactNative: a36d1b355267543453c79df773df07cc3012b401 - DatadogSDKReactNativeSessionReplay: fcbd7cf17dc515949607e112c56374354c8d08ef - DatadogSDKReactNativeWebView: 24fefd471c18d13d950a239a51712c830bf6bf7a - DatadogSessionReplay: 462a3a2e39e9e2193528cf572c8d1acfd6cdace1 - DatadogTrace: 68b7cc49378ba492423191c29280e3904978a684 - DatadogWebViewTracking: 32dfeaf7aad47a605a689ed12e0d21ee8eb56141 + DatadogCore: 4cbe2646591d2f96fb3188400863ec93ac411235 + DatadogCrashReporting: e48da3f880a59d2aa2d04e5034e56507177e9d64 + DatadogFlags: f8cf88371460d6c672abfd97fdc9af5be208f33b + DatadogInternal: 63308b529cd87fb2f99c5961d9ff13afb300a3aa + DatadogLogs: be538def1d5204e011f7952915ad0261014a0dd5 + DatadogRUM: cffc65659ce29546fcc2639a74003135259548fc + DatadogSDKReactNative: 876d8c52f225926581bee36e99965b5a7255c1a3 + DatadogSDKReactNativeSessionReplay: 786cf7fd782aa623772f5d12fa8ba4415dbf1f96 + DatadogSDKReactNativeWebView: 56d5b133e6cfea38d605195ac787f6971039c732 + DatadogSessionReplay: eea291df0135ec792177be1ffc4951750a66a011 + DatadogTrace: 085e35f9e4889f82f8a747922c58ea4b19728720 + DatadogWebViewTracking: 61b8344da898cbaccffc75bc1a17c86175e8573a DoubleConversion: f16ae600a246532c4020132d54af21d0ddb2a385 fast_float: 06eeec4fe712a76acc9376682e4808b05ce978b6 FBLazyVector: 7605ea4810e0e10ae4815292433c09bf4324ba45 @@ -2062,8 +2023,8 @@ SPEC CHECKSUMS: glog: 08b301085f15bcbb6ff8632a8ebaf239aae04e6a hermes-engine: 9e868dc7be781364296d6ee2f56d0c1a9ef0bb11 HMSegmentedControl: 34c1f54d822d8308e7b24f5d901ec674dfa31352 + KSCrash: 80e1e24eaefbe5134934ae11ca8d7746586bc2ed OpenTelemetry-Swift-Api: 3d77582ab6837a63b65bf7d2eacc57d8f2595edd - PLCrashReporter: db59ef96fa3d25f3650040d02ec2798cffee75f2 RCT-Folly: 7b4f73a92ad9571b9dbdb05bb30fad927fa971e1 RCTDeprecation: ebe712bb05077934b16c6bf25228bdec34b64f83 RCTRequired: ca91e5dd26b64f577b528044c962baf171c6b716 @@ -2130,6 +2091,6 @@ SPEC CHECKSUMS: SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 Yoga: feb4910aba9742cfedc059e2b2902e22ffe9954a -PODFILE CHECKSUM: d580bf1cdb494910909c614e1f594c0cdf6cdb3f +PODFILE CHECKSUM: 9a1faac3ae43394b0b86e6fcabf63eced6c66dc2 COCOAPODS: 1.16.2 diff --git a/packages/core/DatadogSDKReactNative.podspec b/packages/core/DatadogSDKReactNative.podspec index 37e864e6e..be340c4c1 100644 --- a/packages/core/DatadogSDKReactNative.podspec +++ b/packages/core/DatadogSDKReactNative.podspec @@ -19,15 +19,15 @@ Pod::Spec.new do |s| s.dependency "React-Core" # /!\ Remember to keep the versions in sync with DatadogSDKReactNativeSessionReplay.podspec - s.dependency 'DatadogCore', '3.4.0' - s.dependency 'DatadogLogs', '3.4.0' - s.dependency 'DatadogTrace', '3.4.0' - s.dependency 'DatadogRUM', '3.4.0' - s.dependency 'DatadogCrashReporting', '3.4.0' - s.dependency 'DatadogFlags', '3.4.0' + s.dependency 'DatadogCore', '3.5.0' + s.dependency 'DatadogLogs', '3.5.0' + s.dependency 'DatadogTrace', '3.5.0' + s.dependency 'DatadogRUM', '3.5.0' + s.dependency 'DatadogCrashReporting', '3.5.0' + s.dependency 'DatadogFlags', '3.5.0' # DatadogWebViewTracking is not available for tvOS - s.ios.dependency 'DatadogWebViewTracking', '3.4.0' + s.ios.dependency 'DatadogWebViewTracking', '3.5.0' s.test_spec 'Tests' do |test_spec| test_spec.source_files = 'ios/Tests/**/*.{swift,json}' diff --git a/packages/react-native-session-replay/DatadogSDKReactNativeSessionReplay.podspec b/packages/react-native-session-replay/DatadogSDKReactNativeSessionReplay.podspec index f0fcd3bb3..0ffa6b41c 100644 --- a/packages/react-native-session-replay/DatadogSDKReactNativeSessionReplay.podspec +++ b/packages/react-native-session-replay/DatadogSDKReactNativeSessionReplay.podspec @@ -23,7 +23,7 @@ Pod::Spec.new do |s| s.dependency "React-Core" # /!\ Remember to keep the version in sync with DatadogSDKReactNative.podspec - s.dependency 'DatadogSessionReplay', '3.4.0' + s.dependency 'DatadogSessionReplay', '3.5.0' s.dependency 'DatadogSDKReactNative' s.test_spec 'Tests' do |test_spec| diff --git a/packages/react-native-webview/DatadogSDKReactNativeWebView.podspec b/packages/react-native-webview/DatadogSDKReactNativeWebView.podspec index 28bdab8a0..6fa746beb 100644 --- a/packages/react-native-webview/DatadogSDKReactNativeWebView.podspec +++ b/packages/react-native-webview/DatadogSDKReactNativeWebView.podspec @@ -23,8 +23,8 @@ Pod::Spec.new do |s| end # /!\ Remember to keep the version in sync with DatadogSDKReactNative.podspec - s.dependency 'DatadogWebViewTracking', '3.4.0' - s.dependency 'DatadogInternal', '3.4.0' + s.dependency 'DatadogWebViewTracking', '3.5.0' + s.dependency 'DatadogInternal', '3.5.0' s.dependency 'DatadogSDKReactNative' s.test_spec 'Tests' do |test_spec| From 113759cfc379e286ff27db6d5c99547453bb498c Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Wed, 14 Jan 2026 15:53:45 +0200 Subject: [PATCH 63/64] Fix ktlint and detekt errors --- .../reactnative/DdFlagsImplementation.kt | 74 ++++++++++--------- .../com/datadog/reactnative/DdSdkBridgeExt.kt | 6 +- .../kotlin/com/datadog/reactnative/DdFlags.kt | 17 +++-- 3 files changed, 55 insertions(+), 42 deletions(-) diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdFlagsImplementation.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdFlagsImplementation.kt index 21acf16af..3642e60a0 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdFlagsImplementation.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdFlagsImplementation.kt @@ -9,19 +9,22 @@ package com.datadog.reactnative import com.datadog.android.Datadog import com.datadog.android.api.InternalLogger import com.datadog.android.api.SdkCore -import com.datadog.android.flags._FlagsInternalProxy -import com.datadog.android.flags.Flags import com.datadog.android.flags.EvaluationContextCallback +import com.datadog.android.flags.Flags import com.datadog.android.flags.FlagsClient -import com.datadog.android.flags.model.FlagsClientState import com.datadog.android.flags.FlagsConfiguration -import com.datadog.android.flags.model.UnparsedFlag +import com.datadog.android.flags._FlagsInternalProxy import com.datadog.android.flags.model.EvaluationContext +import com.datadog.android.flags.model.FlagsClientState +import com.datadog.android.flags.model.UnparsedFlag import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReadableMap import org.json.JSONObject import java.util.Locale +/** + * The entry point to use Datadog's Flags feature. + */ class DdFlagsImplementation( private val sdkCore: SdkCore = Datadog.getInstance(), ) { @@ -48,13 +51,6 @@ class DdFlagsImplementation( promise.resolve(null) } - /** - * Retrieve or create a FlagsClient instance. - * - * Caches clients by name to avoid repeated Builder().build() calls. On hot reload, the cache is - * cleared and clients are recreated - this is safe because gracefulModeEnabled=true prevents - * crashes on duplicate creation. - */ private fun getClient(name: String): FlagsClient = clients.getOrPut(name) { FlagsClient.Builder(name, sdkCore).build() } /** @@ -74,32 +70,45 @@ class DdFlagsImplementation( // Set the evaluation context. val evaluationContext = buildEvaluationContext(targetingKey, attributes) - client.setEvaluationContext(evaluationContext, object : EvaluationContextCallback { - override fun onSuccess() { - val flagsSnapshot = internalClient.getFlagAssignmentsSnapshot() - val serializedFlagsSnapshot = - flagsSnapshot.mapValues { (key, flag) -> - convertUnparsedFlagToMap(key, flag) - }.toWritableMap() - promise.resolve(serializedFlagsSnapshot) - } - - override fun onFailure(error: Throwable) { - // If network request fails and there are cached flags, return them. - if (client.state.getCurrentState() == FlagsClientState.Stale) { + client.setEvaluationContext( + evaluationContext, + object : EvaluationContextCallback { + override fun onSuccess() { val flagsSnapshot = internalClient.getFlagAssignmentsSnapshot() val serializedFlagsSnapshot = flagsSnapshot.mapValues { (key, flag) -> convertUnparsedFlagToMap(key, flag) }.toWritableMap() promise.resolve(serializedFlagsSnapshot) - } else { - promise.reject("CLIENT_NOT_INITIALIZED", error.message, error) } - } - }) + + override fun onFailure(error: Throwable) { + // If network request fails and there are cached flags, return them. + if (client.state.getCurrentState() == FlagsClientState.Stale) { + val flagsSnapshot = internalClient.getFlagAssignmentsSnapshot() + val serializedFlagsSnapshot = + flagsSnapshot.mapValues { (key, flag) -> + convertUnparsedFlagToMap(key, flag) + }.toWritableMap() + promise.resolve(serializedFlagsSnapshot) + } else { + promise.reject("CLIENT_NOT_INITIALIZED", error.message, error) + } + } + }, + ) } + /** + * A bridge for tracking feature flag evaluations in React Native. + * @param clientName The name of the client. + * @param key The key of the flag. + * @param rawFlag The raw flag from the JavaScript cache. + * @param targetingKey The targeting key. + * @param attributes The attributes for the evaluation context. + * @param promise The promise to resolve. + */ + @Suppress("LongParameterList") fun trackEvaluation( clientName: String, key: String, @@ -170,13 +179,6 @@ private fun buildEvaluationContext( return EvaluationContext(targetingKey, parsed) } -/** - * Converts [UnparsedFlag] to [Map] for further React Native bridge transfer. Includes the - * flag key and parses the value based on variationType. - * - * We are using Map instead of WritableMap as an intermediate because it is more handy, and we can - * convert to WritableMap right before sending to React Native. - */ private fun convertUnparsedFlagToMap( flagKey: String, flag: UnparsedFlag, @@ -206,6 +208,7 @@ private fun convertUnparsedFlagToMap( ) } + // Return a [Map] as an intermediate because it is easier to use; we can convert it to WritableMap right before sending to React Native. return mapOf( "key" to flagKey, "value" to (parsedValue ?: flag.variationValue), @@ -219,7 +222,6 @@ private fun convertUnparsedFlagToMap( ) } -/** Converts a [Map] to a [UnparsedFlag]. */ @Suppress("UNCHECKED_CAST") private fun convertMapToUnparsedFlag(map: Map): UnparsedFlag = object : UnparsedFlag { diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkBridgeExt.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkBridgeExt.kt index 3cb754a2c..9e33574f0 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkBridgeExt.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkBridgeExt.kt @@ -3,6 +3,7 @@ * This product includes software developed at Datadog (https://www.datadoghq.com/). * Copyright 2016-Present Datadog, Inc. */ +@file:Suppress("TooManyFunctions") package com.datadog.reactnative @@ -15,8 +16,8 @@ import com.facebook.react.bridge.WritableArray import com.facebook.react.bridge.WritableMap import com.facebook.react.bridge.WritableNativeArray import com.facebook.react.bridge.WritableNativeMap -import org.json.JSONObject import org.json.JSONArray +import org.json.JSONObject /** * Converts the [List] to a [WritableNativeArray]. @@ -32,6 +33,7 @@ internal fun List<*>.toWritableArray(): WritableArray = * @param createWritableMap a function to provide a concrete instance of new WritableMap(s) * @param createWritableArray a function to provide a concrete instance of new WritableArray(s) */ +@Suppress("CyclomaticComplexMethod") internal fun List<*>.toWritableArray( createWritableMap: () -> WritableMap, createWritableArray: () -> WritableArray, @@ -112,6 +114,7 @@ internal fun Map<*, *>.toWritableMap(): WritableMap = * @param createWritableMap a function to provide a concrete instance for WritableMap(s) * @param createWritableArray a function to provide a concrete instance for WritableArray(s) */ +@Suppress("CyclomaticComplexMethod") internal fun Map<*, *>.toWritableMap( createWritableMap: () -> WritableMap, createWritableArray: () -> WritableArray, @@ -249,6 +252,7 @@ internal fun ReadableMap.toMap(): Map { * such as [List], [Map] and the raw types. * or [List], instead of [ReadableMap] and [ReadableArray] respectively). */ +@Suppress("CyclomaticComplexMethod") internal fun ReadableArray.toList(): List<*> { val list = mutableListOf() for (i in 0 until size()) { diff --git a/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdFlags.kt b/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdFlags.kt index f4131160b..e9e51fc5d 100644 --- a/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdFlags.kt +++ b/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdFlags.kt @@ -36,10 +36,10 @@ class DdFlags(reactContext: ReactApplicationContext) : ReactContextBaseJavaModul */ @ReactMethod fun setEvaluationContext( - clientName: String, - targetingKey: String, - attributes: ReadableMap, - promise: Promise + clientName: String, + targetingKey: String, + attributes: ReadableMap, + promise: Promise ) { implementation.setEvaluationContext(clientName, targetingKey, attributes, promise) } @@ -50,7 +50,14 @@ class DdFlags(reactContext: ReactApplicationContext) : ReactContextBaseJavaModul * @param key The key of the flag. */ @ReactMethod - fun trackEvaluation(clientName: String, key: String, rawFlag: ReadableMap, targetingKey: String, attributes: ReadableMap, promise: Promise) { + fun trackEvaluation( + clientName: String, + key: String, + rawFlag: ReadableMap, + targetingKey: String, + attributes: ReadableMap, + promise: Promise + ) { implementation.trackEvaluation(clientName, key, rawFlag, targetingKey, attributes, promise) } } From 8eb54c8688c48ce65119c64883204a37651ff8df Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Wed, 14 Jan 2026 16:16:55 +0200 Subject: [PATCH 64/64] Fix failing extension tests --- packages/core/android/build.gradle | 1 + .../datadog/reactnative/DdSdkBridgeExtTest.kt | 8 ++-- .../kotlin/com/datadog/tools/unit/MapExt.kt | 38 +++++++++++++++++++ .../com/datadog/tools/unit/MockRumMonitor.kt | 3 ++ 4 files changed, 47 insertions(+), 3 deletions(-) diff --git a/packages/core/android/build.gradle b/packages/core/android/build.gradle index 3407a6e99..b70c084c5 100644 --- a/packages/core/android/build.gradle +++ b/packages/core/android/build.gradle @@ -236,6 +236,7 @@ unMock { keep("android.os.SystemProperties") keep("android.view.Choreographer") keep("android.view.DisplayEventReceiver") + keepStartingWith("org.json.") } tasks.withType(Test) { diff --git a/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkBridgeExtTest.kt b/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkBridgeExtTest.kt index b3b73e03e..0e0e01627 100644 --- a/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkBridgeExtTest.kt +++ b/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkBridgeExtTest.kt @@ -9,6 +9,7 @@ package com.datadog.reactnative import com.datadog.tools.unit.keys import com.datadog.tools.unit.toReadableArray import com.datadog.tools.unit.toReadableMap +import com.datadog.tools.unit.toReadableMapDeep import com.facebook.react.bridge.JavaOnlyArray import com.facebook.react.bridge.JavaOnlyMap import com.facebook.react.bridge.WritableArray @@ -342,7 +343,8 @@ internal class DdSdkBridgeExtTest { } // When - val writableMap = jsonObject.toWritableMap() + // Use the parameterized version to avoid native library requirements in unit tests + val writableMap = jsonObject.toMap().toWritableMap(createWritableMap, createWritableArray) // Then assertThat(writableMap.getInt("int")).isEqualTo(1) @@ -407,7 +409,7 @@ internal class DdSdkBridgeExtTest { "double" to 2.0, "string" to "test", "boolean" to true - ).toReadableMap() + ).toReadableMapDeep() // When val jsonObject = readableMap.toJSONObject() @@ -426,7 +428,7 @@ internal class DdSdkBridgeExtTest { val readableMap = mapOf( "map" to mapOf("nestedKey" to "nestedValue"), "list" to listOf("item1", "item2") - ).toReadableMap() + ).toReadableMapDeep() // When val jsonObject = readableMap.toJSONObject() diff --git a/packages/core/android/src/test/kotlin/com/datadog/tools/unit/MapExt.kt b/packages/core/android/src/test/kotlin/com/datadog/tools/unit/MapExt.kt index e12dcc919..f881b8648 100644 --- a/packages/core/android/src/test/kotlin/com/datadog/tools/unit/MapExt.kt +++ b/packages/core/android/src/test/kotlin/com/datadog/tools/unit/MapExt.kt @@ -25,6 +25,28 @@ fun Map<*, *>.toReadableMap(): ReadableMap { return JavaOnlyMap.of(*keysAndValues.toTypedArray()) } +/** + * Recursively converts the [Map] to a [ReadableMap], including nested maps and lists. + */ +fun Map<*, *>.toReadableMapDeep(): ReadableMap { + val keysAndValues = mutableListOf() + + entries.forEach { + keysAndValues.add(it.key) + keysAndValues.add( + when (val value = it.value) { + is ReadableMap -> value + is ReadableArray -> value + is Map<*, *> -> value.toReadableMapDeep() + is List<*> -> value.toReadableArrayDeep() + else -> value + } + ) + } + + return JavaOnlyMap.of(*keysAndValues.toTypedArray()) +} + fun Map>.toFirstPartyHostsReadableArray(): ReadableArray { val list = mutableListOf() @@ -48,6 +70,22 @@ fun List<*>.toReadableArray(): ReadableArray { return JavaOnlyArray.from(this) } +/** + * Recursively converts the [List] to a [ReadableArray], including nested maps and lists. + */ +fun List<*>.toReadableArrayDeep(): ReadableArray { + val convertedList = this.map { value -> + when (value) { + is ReadableMap -> value + is ReadableArray -> value + is Map<*, *> -> value.toReadableMapDeep() + is List<*> -> value.toReadableArrayDeep() + else -> value + } + } + return JavaOnlyArray.from(convertedList) +} + fun Set<*>.toReadableArray(): ReadableArray { // this FB implementation is not backed by Android-specific .so library, so ok for unit tests return JavaOnlyArray.from(this.toList()) diff --git a/packages/core/android/src/test/kotlin/com/datadog/tools/unit/MockRumMonitor.kt b/packages/core/android/src/test/kotlin/com/datadog/tools/unit/MockRumMonitor.kt index 13f73d94a..2ef718888 100644 --- a/packages/core/android/src/test/kotlin/com/datadog/tools/unit/MockRumMonitor.kt +++ b/packages/core/android/src/test/kotlin/com/datadog/tools/unit/MockRumMonitor.kt @@ -147,4 +147,7 @@ class MockRumMonitor : RumMonitor { failureReason: FailureReason, attributes: Map ) {} + + @ExperimentalRumApi + override fun reportAppFullyDisplayed() {} }