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/example-new-architecture/App.tsx b/example-new-architecture/App.tsx
index 20e0c3eda..e21a00e73 100644
--- a/example-new-architecture/App.tsx
+++ b/example-new-architecture/App.tsx
@@ -9,10 +9,12 @@ import {
DdLogs,
DdTrace,
RumConfiguration,
+ DdFlags,
} from '@datadog/mobile-react-native';
import React from 'react';
import type {PropsWithChildren} from 'react';
import {
+ ActivityIndicator,
SafeAreaView,
ScrollView,
StatusBar,
@@ -48,7 +50,7 @@ import {APPLICATION_ID, CLIENT_TOKEN, ENVIRONMENT} from './ddCredentials';
)
config.rumConfiguration.sessionSampleRate = 100;
config.rumConfiguration.telemetrySampleRate = 100;
-
+
await DdSdkReactNative.initialize(config);
await DdRum.startView('main', 'Main');
setTimeout(async () => {
@@ -91,12 +93,43 @@ function Section({children, title}: SectionProps): React.JSX.Element {
}
function App(): React.JSX.Element {
- const isDarkMode = useColorScheme() === 'dark';
+ 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);
+ }, []);
+
+ 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 (
+
+ 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-new-architecture/ios/Podfile b/example-new-architecture/ios/Podfile
index f2f0fa09f..3c29ed272 100644
--- a/example-new-architecture/ios/Podfile
+++ b/example-new-architecture/ios/Podfile
@@ -26,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 8c3eede06..05c1392ee 100644
--- a/example-new-architecture/ios/Podfile.lock
+++ b/example-new-architecture/ios/Podfile.lock
@@ -1,22 +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)
- - 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)
- - 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
@@ -38,12 +42,13 @@ PODS:
- ReactCommon/turbomodule/core
- Yoga
- DatadogSDKReactNative/Tests (2.13.2):
- - DatadogCore (= 3.4.0)
- - DatadogCrashReporting (= 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
@@ -64,11 +69,11 @@ PODS:
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Yoga
- - DatadogTrace (3.4.0):
- - DatadogInternal (= 3.4.0)
- - OpenTelemetrySwiftApi (= 1.13.1)
- - DatadogWebViewTracking (3.4.0):
- - DatadogInternal (= 3.4.0)
+ - DatadogTrace (3.5.0):
+ - DatadogInternal (= 3.5.0)
+ - OpenTelemetry-Swift-Api (~> 2.3.0)
+ - DatadogWebViewTracking (3.5.0):
+ - DatadogInternal (= 3.5.0)
- DoubleConversion (1.1.6)
- fast_float (6.1.4)
- FBLazyVector (0.76.9)
@@ -77,8 +82,18 @@ PODS:
- hermes-engine (0.76.9):
- hermes-engine/Pre-built (= 0.76.9)
- hermes-engine/Pre-built (0.76.9)
- - OpenTelemetrySwiftApi (1.13.1)
- - PLCrashReporter (1.12.0)
+ - 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)
- RCT-Folly (2024.10.14.00):
- boost
- DoubleConversion
@@ -1706,13 +1721,14 @@ SPEC REPOS:
https://github.com/CocoaPods/Specs.git:
- DatadogCore
- DatadogCrashReporting
+ - DatadogFlags
- DatadogInternal
- DatadogLogs
- DatadogRUM
- DatadogTrace
- DatadogWebViewTracking
- - OpenTelemetrySwiftApi
- - PLCrashReporter
+ - KSCrash
+ - OpenTelemetry-Swift-Api
- SocketRocket
EXTERNAL SOURCES:
@@ -1850,22 +1866,23 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
boost: 1dca942403ed9342f98334bf4c3621f011aa7946
- DatadogCore: 8c384b6338c49534e43fdf7f9a0508b62bf1d426
- DatadogCrashReporting: 103bfb4077db2ccee1846f71e53712972732d3b7
- DatadogInternal: b0372935ad8dde5ad06960fe8d88c39b2cc92bcc
- DatadogLogs: 484bb1bfe0c9a7cb2a7d9733f61614e8ea7b2f3a
- DatadogRUM: 00069b27918e0ce4a9223b87b4bfa7929d6a0a1f
- DatadogSDKReactNative: 02cd4d00e3eecdc8ac57042db50b921054bbe709
- DatadogTrace: 852cb80f9370eb1321eb30a73c82c8e3d9e4e980
- 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
- OpenTelemetrySwiftApi: aaee576ed961e0c348af78df58b61300e95bd104
- PLCrashReporter: db59ef96fa3d25f3650040d02ec2798cffee75f2
+ KSCrash: 80e1e24eaefbe5134934ae11ca8d7746586bc2ed
+ OpenTelemetry-Swift-Api: 3d77582ab6837a63b65bf7d2eacc57d8f2595edd
RCT-Folly: 7b4f73a92ad9571b9dbdb05bb30fad927fa971e1
RCTDeprecation: ebe712bb05077934b16c6bf25228bdec34b64f83
RCTRequired: ca91e5dd26b64f577b528044c962baf171c6b716
@@ -1925,6 +1942,6 @@ SPEC CHECKSUMS:
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
Yoga: feb4910aba9742cfedc059e2b2902e22ffe9954a
-PODFILE CHECKSUM: d9d720c99b6fffec4dd489d565a544a358a52b83
+PODFILE CHECKSUM: 2046fc46dd3311048c09b49573c69b7aba2aab81
COCOAPODS: 1.16.2
diff --git a/example/ios/Podfile b/example/ios/Podfile
index 1736870c9..7484e6aa7 100644
--- a/example/ios/Podfile
+++ b/example/ios/Podfile
@@ -28,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 f302e30e8..a6ff19355 100644
--- a/example/ios/Podfile.lock
+++ b/example/ios/Podfile.lock
@@ -1,34 +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)
- - 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)
- - 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)
- - 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
@@ -51,7 +56,7 @@ PODS:
- Yoga
- DatadogSDKReactNativeSessionReplay/Tests (2.13.2):
- DatadogSDKReactNative
- - DatadogSessionReplay (= 3.4.0)
+ - DatadogSessionReplay (= 3.5.0)
- DoubleConversion
- glog
- hermes-engine
@@ -74,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)
- - OpenTelemetrySwiftApi (= 1.13.1)
- - DatadogWebViewTracking (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.5.0):
+ - DatadogInternal (= 3.5.0)
- DoubleConversion (1.1.6)
- fast_float (6.1.4)
- FBLazyVector (0.76.9)
@@ -101,8 +106,18 @@ PODS:
- hermes-engine/Pre-built (= 0.76.9)
- hermes-engine/Pre-built (0.76.9)
- HMSegmentedControl (1.5.6)
- - OpenTelemetrySwiftApi (1.13.1)
- - PLCrashReporter (1.12.0)
+ - 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)
- RCT-Folly (2024.10.14.00):
- boost
- DoubleConversion
@@ -1824,6 +1839,7 @@ SPEC REPOS:
https://github.com/CocoaPods/Specs.git:
- DatadogCore
- DatadogCrashReporting
+ - DatadogFlags
- DatadogInternal
- DatadogLogs
- DatadogRUM
@@ -1831,8 +1847,8 @@ SPEC REPOS:
- DatadogTrace
- DatadogWebViewTracking
- HMSegmentedControl
- - OpenTelemetrySwiftApi
- - PLCrashReporter
+ - KSCrash
+ - OpenTelemetry-Swift-Api
- SocketRocket
EXTERNAL SOURCES:
@@ -1988,17 +2004,18 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
boost: 1dca942403ed9342f98334bf4c3621f011aa7946
- DatadogCore: 8c384b6338c49534e43fdf7f9a0508b62bf1d426
- DatadogCrashReporting: 103bfb4077db2ccee1846f71e53712972732d3b7
- DatadogInternal: b0372935ad8dde5ad06960fe8d88c39b2cc92bcc
- DatadogLogs: 484bb1bfe0c9a7cb2a7d9733f61614e8ea7b2f3a
- DatadogRUM: 00069b27918e0ce4a9223b87b4bfa7929d6a0a1f
- DatadogSDKReactNative: 2d02290e38b30c2f118555ac81f6d767dfd0c95a
- DatadogSDKReactNativeSessionReplay: fcbd7cf17dc515949607e112c56374354c8d08ef
- DatadogSDKReactNativeWebView: 24fefd471c18d13d950a239a51712c830bf6bf7a
- DatadogSessionReplay: 462a3a2e39e9e2193528cf572c8d1acfd6cdace1
- DatadogTrace: 852cb80f9370eb1321eb30a73c82c8e3d9e4e980
- 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
@@ -2006,8 +2023,8 @@ SPEC CHECKSUMS:
glog: 08b301085f15bcbb6ff8632a8ebaf239aae04e6a
hermes-engine: 9e868dc7be781364296d6ee2f56d0c1a9ef0bb11
HMSegmentedControl: 34c1f54d822d8308e7b24f5d901ec674dfa31352
- OpenTelemetrySwiftApi: aaee576ed961e0c348af78df58b61300e95bd104
- PLCrashReporter: db59ef96fa3d25f3650040d02ec2798cffee75f2
+ KSCrash: 80e1e24eaefbe5134934ae11ca8d7746586bc2ed
+ OpenTelemetry-Swift-Api: 3d77582ab6837a63b65bf7d2eacc57d8f2595edd
RCT-Folly: 7b4f73a92ad9571b9dbdb05bb30fad927fa971e1
RCTDeprecation: ebe712bb05077934b16c6bf25228bdec34b64f83
RCTRequired: ca91e5dd26b64f577b528044c962baf171c6b716
@@ -2074,6 +2091,6 @@ SPEC CHECKSUMS:
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
Yoga: feb4910aba9742cfedc059e2b2902e22ffe9954a
-PODFILE CHECKSUM: 2be76f6ff2a88869ff51bdbf48edb79d7d863c79
+PODFILE CHECKSUM: 9a1faac3ae43394b0b86e6fcabf63eced6c66dc2
COCOAPODS: 1.16.2
diff --git a/example/src/WixApp.tsx b/example/src/WixApp.tsx
index 809912843..8664c3e3d 100644
--- a/example/src/WixApp.tsx
+++ b/example/src/WixApp.tsx
@@ -1,5 +1,5 @@
-import React, { 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';
@@ -11,6 +11,7 @@ import {
} from '@datadog/mobile-react-native-navigation';
import styles from './screens/styles';
+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';
@@ -19,7 +20,7 @@ const viewNamePredicate: ViewNamePredicate = function customViewNamePredicate(_e
return "Custom RN " + trackedName;
}
-const viewTrackingPredicate: ViewTrackingPredicate = function customViewTrackingPredicate(event: ComponentDidAppearEvent) {
+const viewTrackingPredicate: ViewTrackingPredicate = function customViewTrackingPredicate(event: ComponentDidAppearEvent) {
if (event.name === "AlertModal") {
return false;
}
@@ -27,7 +28,7 @@ const viewTrackingPredicate: ViewTrackingPredicate = function customViewTracking
return true;
}
-const paramsTrackingPredicate: ParamsTrackingPredicate = function customParamsTrackingPredicate(event: ComponentDidAppearEvent) {
+const paramsTrackingPredicate: ParamsTrackingPredicate = function customParamsTrackingPredicate(event: ComponentDidAppearEvent) {
const filteredParams: any = {};
if (event.passProps?.creditCardNumber) {
filteredParams["creditCardNumber"] = "XXXX XXXX XXXX XXXX";
@@ -69,6 +70,38 @@ function registerScreens() {
}
const HomeScreen = props => {
+ 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 (
@@ -109,11 +142,12 @@ const HomeScreen = props => {
passProps: {
username: "test",
creditCardNumber: "4242 4242 4242 4242"
- }
+ }
}
});
}}
/>
+ {testFlagKey}: {JSON.stringify(testFlag)}
);
};
diff --git a/example/src/ddUtils.tsx b/example/src/ddUtils.tsx
index 8c75dea94..ad6702a4a 100644
--- a/example/src/ddUtils.tsx
+++ b/example/src/ddUtils.tsx
@@ -5,7 +5,8 @@ import {
CoreConfiguration,
RumConfiguration,
SdkVerbosity,
- TrackingConsent
+ TrackingConsent,
+ DdFlags,
} from '@datadog/mobile-react-native';
import {APPLICATION_ID, CLIENT_TOKEN, ENVIRONMENT} from './ddCredentials';
@@ -51,4 +52,6 @@ export function initializeDatadog(trackingConsent: TrackingConsent) {
DdSdkReactNative.setUserInfo({id: "1337", name: "Xavier", email: "xg@example.com", extraInfo: { type: "premium" } })
DdSdkReactNative.addAttributes({campaign: "ad-network"})
});
+
+ DdFlags.enable()
}
diff --git a/example/src/screens/MainScreen.tsx b/example/src/screens/MainScreen.tsx
index 7f0fbdec2..ebf0360bc 100644
--- a/example/src/screens/MainScreen.tsx
+++ b/example/src/screens/MainScreen.tsx
@@ -7,11 +7,11 @@
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';
-import { DdLogs, DdSdkReactNative, TrackingConsent } 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';
@@ -27,6 +27,7 @@ interface MainScreenState {
resultTouchableNativeFeedback: string,
trackingConsent: TrackingConsent,
trackingConsentModalVisible: boolean
+ flagsInitialized: 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,
+ flagsInitialized: false
} as MainScreenState;
this.consentModal = React.createRef()
}
@@ -94,6 +96,7 @@ export default class MainScreen extends Component {
componentDidMount() {
this.updateTrackingConsent()
+ this.initializeFlags();
DdLogs.debug("[DATADOG SDK] Test React Native Debug Log");
}
@@ -105,6 +108,24 @@ export default class MainScreen extends Component {
})
}
+ 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 userId = 'test-user-1';
+ const userAttributes = {
+ country: 'US',
+ };
+
+ await client.setEvaluationContext({targetingKey: userId, attributes: userAttributes});
+
+ this.setState({ flagsInitialized: true })
+ })();
+ }
+
setTrackingConsentModalVisible(visible: boolean) {
if (visible) {
this.consentModal.current.setConsent(this.state.trackingConsent)
@@ -113,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}
@@ -205,6 +236,7 @@ export default class MainScreen extends Component {
Click me (error)
+ {testFlagKey}: {JSON.stringify(testFlag)}
}
diff --git a/packages/core/DatadogSDKReactNative.podspec b/packages/core/DatadogSDKReactNative.podspec
index a9f8fb7ac..be340c4c1 100644
--- a/packages/core/DatadogSDKReactNative.podspec
+++ b/packages/core/DatadogSDKReactNative.podspec
@@ -13,21 +13,22 @@ Pod::Spec.new do |s|
s.platforms = { :ios => "12.0", :tvos => "12.0" }
s.source = { :git => "https://github.com/DataDog/dd-sdk-reactnative.git", :tag => "#{s.version}" }
-
+
s.source_files = "ios/Sources/*.{h,m,mm,swift}"
-
+
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 '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}'
test_spec.resources = 'ios/Tests/Fixtures'
diff --git a/packages/core/__mocks__/react-native.ts b/packages/core/__mocks__/react-native.ts
index a4622b6b9..db10d7f31 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';
@@ -167,4 +168,11 @@ actualRN.NativeModules.DdRum = {
) as jest.MockedFunction
};
+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/android/build.gradle b/packages/core/android/build.gradle
index f58031967..b70c084c5 100644
--- a/packages/core/android/build.gradle
+++ b/packages/core/android/build.gradle
@@ -195,21 +195,22 @@ 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") {
+ 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-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"
@@ -235,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/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..3642e60a0
--- /dev/null
+++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdFlagsImplementation.kt
@@ -0,0 +1,237 @@
+/*
+ * 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.EvaluationContextCallback
+import com.datadog.android.flags.Flags
+import com.datadog.android.flags.FlagsClient
+import com.datadog.android.flags.FlagsConfiguration
+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(),
+) {
+ 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)
+ }
+
+ 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)
+ }
+ }
+ },
+ )
+ }
+
+ /**
+ * 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,
+ 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)
+}
+
+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 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),
+ "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(),
+ )
+}
+
+@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..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,53 +16,84 @@ 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.JSONArray
+import org.json.JSONObject
/**
* 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].
* @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
+ 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,52 +103,81 @@ 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].
* @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
+ 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 +189,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 +216,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 +239,7 @@ internal fun ReadableMap.toMap(): Map {
Log.e(
javaClass.simpleName,
"toMap(): Could not convert object for key: $key",
- err
+ err,
)
}
}
@@ -179,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()) {
@@ -186,32 +260,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 +313,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..e9e51fc5d
--- /dev/null
+++ b/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdFlags.kt
@@ -0,0 +1,63 @@
+/*
+ * 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..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,11 +9,14 @@ 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
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 +279,245 @@ 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
+ // 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)
+ 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
+ ).toReadableMapDeep()
+
+ // 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")
+ ).toReadableMapDeep()
+
+ // 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,
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() {}
}
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.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..8c6a1d0e2
--- /dev/null
+++ b/packages/core/ios/Sources/DdFlags.mm
@@ -0,0 +1,82 @@
+/*
+ * 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()
+
+RCT_EXPORT_METHOD(enable:(NSDictionary *)configuration
+ withResolver:(RCTPromiseResolveBlock)resolve
+ withRejecter:(RCTPromiseRejectBlock)reject)
+{
+ [self enable:configuration resolve:resolve reject: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_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 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.
+#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)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];
+}
+
+- (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
new file mode 100644
index 000000000..f2e4cc59f
--- /dev/null
+++ b/packages/core/ios/Sources/DdFlagsImplementation.swift
@@ -0,0 +1,257 @@
+/*
+ * 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
+import DatadogInternal
+@_spi(Internal)
+import DatadogFlags
+
+@objc
+public class DdFlagsImplementation: NSObject {
+ private let core: DatadogCoreProtocol
+
+ internal var clientProviders: [String: () -> FlagsClientProtocol] = [:]
+
+ /// Exposing this initializer for testing purposes. React Native will always use the default initializer.
+ internal init(core: DatadogCoreProtocol) {
+ self.core = core
+ }
+
+ @objc
+ public override convenience init() {
+ self.init(core: CoreRegistry.default)
+ }
+
+ @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 {
+ 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.
+ /// 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.
+ private func getClient(name: String) -> FlagsClientProtocol {
+ if let provider = clientProviders[name] {
+ return provider()
+ }
+
+ let client = FlagsClient.create(name: name, in: self.core)
+ clientProviders[name] = { FlagsClient.shared(named: name, in: self.core) }
+ return client
+ }
+
+ @objc
+ public func setEvaluationContext(_ clientName: String, targetingKey: String, attributes: NSDictionary, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
+ let client = getClient(name: clientName)
+ guard let clientInternal = client as? FlagsClientInternal else {
+ reject(nil, "CLIENT_NOT_INITIALIZED", nil)
+ return
+ }
+
+ let evaluationContext = buildEvaluationContext(targetingKey: targetingKey, attributes: attributes)
+
+ client.setEvaluationContext(evaluationContext) { result in
+ switch result {
+ case .success:
+ guard let flagsSnapshot = clientInternal.getFlagAssignments() else {
+ reject(nil, "CLIENT_NOT_INITIALIZED", nil)
+ return
+ }
+
+ let serializedFlagsSnapshot = Dictionary(
+ uniqueKeysWithValues: flagsSnapshot.map { key, flagAssignment in
+ (key, flagAssignment.asDictionary(flagKey: key))
+ }
+ )
+
+ resolve(serializedFlagsSnapshot)
+ case .failure(let 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)
+ }
+ }
+ }
+
+ @objc
+ 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
+ }
+
+ let evaluationContext = buildEvaluationContext(targetingKey: targetingKey, attributes: attributes)
+
+ client.sendFlagEvaluation(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 {
+ public 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 {
+ 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()
+ }
+ }
+}
diff --git a/packages/core/ios/Sources/RNDdSdkConfiguration.swift b/packages/core/ios/Sources/RNDdSdkConfiguration.swift
index 9322377e8..954767ff6 100644
--- a/packages/core/ios/Sources/RNDdSdkConfiguration.swift
+++ b/packages/core/ios/Sources/RNDdSdkConfiguration.swift
@@ -5,6 +5,7 @@
*/
import DatadogCore
+import DatadogFlags
import DatadogInternal
import DatadogRUM
import Foundation
@@ -180,6 +181,39 @@ 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 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,
+ customFlagsEndpoint: customFlagsEndpointURL,
+ customFlagsHeaders: customFlagsHeaders,
+ customExposureEndpoint: customExposureEndpointURL,
+ trackExposures: trackExposures ?? true,
+ rumIntegrationEnabled: rumIntegrationEnabled ?? true
+ )
+ }
+
func asProxyConfiguration() -> [AnyHashable: Any]? {
guard let address = object(forKey: "address") as? String else {
return nil
diff --git a/packages/core/ios/Tests/DdFlagsTests.swift b/packages/core/ios/Tests/DdFlagsTests.swift
new file mode 100644
index 000000000..45849c8d8
--- /dev/null
+++ b/packages/core/ios/Tests/DdFlagsTests.swift
@@ -0,0 +1,439 @@
+/*
+ * 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
+@_spi(Internal)
+import DatadogFlags
+import DatadogInternal
+@_spi(Internal)
+@testable
+import DatadogSDKReactNative
+
+class DdFlagsTests: XCTestCase {
+
+ private var core: FlagsTestCore!
+ private var implementation: DdFlagsImplementation!
+
+ 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)
+ 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 {
+ 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: - Configuration Tests
+
+ func testConfigurationParsing() {
+ let configDict: NSDictionary = [
+ "enabled": true,
+ "trackExposures": false,
+ "rumIntegrationEnabled": false,
+ "customFlagsEndpoint": "https://flags.example.com",
+ "customExposureEndpoint": "https://exposure.example.com"
+ ]
+
+ 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")
+ }
+
+ 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 testConfigurationParsingDisabled() {
+ let configDict: NSDictionary = ["enabled": false]
+ let config = configDict.asFlagsConfiguration()
+ XCTAssertNil(config)
+ }
+
+ // 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 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/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
}
diff --git a/packages/core/src/flags/DdFlags.ts b/packages/core/src/flags/DdFlags.ts
new file mode 100644
index 000000000..a2b96f9bf
--- /dev/null
+++ b/packages/core/src/flags/DdFlags.ts
@@ -0,0 +1,113 @@
+/*
+ * 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 { getGlobalInstance } from '../utils/singletonUtils';
+
+import { FlagsClient } from './FlagsClient';
+import type { DdFlagsType, DdFlagsConfiguration } from './types';
+
+const FLAGS_MODULE = 'com.datadog.reactnative.flags';
+
+class DdFlagsWrapper implements DdFlagsType {
+ // eslint-disable-next-line global-require, @typescript-eslint/no-var-requires
+ private nativeFlags: DdNativeFlagsType = require('../specs/NativeDdFlags')
+ .default;
+
+ private isFeatureEnabled = false;
+
+ private clients: Record = {};
+
+ /**
+ * 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 `DdFlags.getClient()`.
+ *
+ * @example
+ * ```ts
+ * import { DdSdkReactNativeConfiguration, DdSdkReactNative, DdFlags } 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 DdFlags.enable(flagsConfig);
+ *
+ * // Retrieve the client and access feature flags.
+ * const flagsClient = DdFlags.getClient();
+ * const flagValue = await flagsClient.getBooleanValue('new-feature', false);
+ * ```
+ *
+ * @param configuration Configuration options for the Datadog Flags feature.
+ */
+ enable = async (configuration?: DdFlagsConfiguration): Promise => {
+ if (configuration?.enabled === false) {
+ return;
+ }
+
+ if (this.isFeatureEnabled) {
+ InternalLog.log(
+ 'Datadog Flags feature has already been enabled. Skipping this `DdFlags.enable()` call.',
+ SdkVerbosity.WARN
+ );
+ }
+
+ // Default `enabled` to `true`.
+ await this.nativeFlags.enable({ enabled: true, ...configuration });
+
+ 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 = DdFlags.getClient();
+ *
+ * // 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 => {
+ if (!this.isFeatureEnabled) {
+ InternalLog.log(
+ '`DdFlags.getClient()` called before Datadog Flags feature have been enabled. Client will fall back to serving default flag values.',
+ SdkVerbosity.ERROR
+ );
+ }
+
+ this.clients[clientName] ??= new FlagsClient(clientName);
+
+ return this.clients[clientName];
+ };
+}
+
+export const DdFlags: DdFlagsType = getGlobalInstance(
+ FLAGS_MODULE,
+ () => new DdFlagsWrapper()
+);
diff --git a/packages/core/src/flags/FlagsClient.ts b/packages/core/src/flags/FlagsClient.ts
new file mode 100644
index 000000000..a1acecae9
--- /dev/null
+++ b/packages/core/src/flags/FlagsClient.ts
@@ -0,0 +1,273 @@
+/*
+ * 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 {
+ 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
+ private nativeFlags: DdNativeFlagsType = require('../specs/NativeDdFlags')
+ .default;
+
+ 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. Otherwise, the client will fall back to serving default flag values.
+ *
+ * @param context The evaluation context to associate with the current session.
+ *
+ * @example
+ * ```ts
+ * const flagsClient = DdFlags.getClient();
+ *
+ * await flagsClient.setEvaluationContext({
+ * targetingKey: 'user-123',
+ * attributes: {
+ * favoriteFruit: 'apple'
+ * }
+ * });
+ *
+ * const flagValue = flagsClient.getBooleanValue('new-feature', false);
+ * ```
+ */
+ setEvaluationContext = async (
+ context: EvaluationContext
+ ): Promise => {
+ const processedContext = processEvaluationContext(context);
+
+ try {
+ const result = await this.nativeFlags.setEvaluationContext(
+ this.clientName,
+ processedContext.targetingKey,
+ processedContext.attributes ?? {}
+ );
+
+ this._evaluationContext = processedContext;
+ this._flagsCache = result;
+ } catch (error) {
+ if (error instanceof Error) {
+ InternalLog.log(
+ `Error setting flag evaluation context: ${error.message}`,
+ SdkVerbosity.ERROR
+ );
+ }
+ }
+ };
+
+ private getDetails = (key: string, defaultValue: T): FlagDetails => {
+ // 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 \`DdFlags.setEvaluationContext()\` before evaluating any flags.`,
+ SdkVerbosity.ERROR
+ );
+
+ return {
+ key,
+ value: defaultValue,
+ variant: null,
+ reason: null,
+ error: 'PROVIDER_NOT_READY'
+ };
+ }
+
+ // Retrieve the flag from the cache.
+ const flagCacheEntry = this._flagsCache[key];
+
+ if (!flagCacheEntry) {
+ return {
+ key,
+ value: defaultValue,
+ variant: null,
+ reason: null,
+ error: 'FLAG_NOT_FOUND'
+ };
+ }
+
+ // 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;
+ };
+
+ /**
+ * 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
+ ): FlagDetails => {
+ if (typeof defaultValue !== 'boolean') {
+ return {
+ key,
+ value: defaultValue,
+ variant: null,
+ reason: null,
+ error: 'TYPE_MISMATCH'
+ };
+ }
+
+ return this.getDetails(key, defaultValue);
+ };
+
+ /**
+ * 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
+ ): FlagDetails => {
+ if (typeof defaultValue !== 'string') {
+ return {
+ key,
+ value: defaultValue,
+ variant: null,
+ reason: null,
+ error: 'TYPE_MISMATCH'
+ };
+ }
+
+ return this.getDetails(key, defaultValue);
+ };
+
+ /**
+ * 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
+ ): FlagDetails => {
+ if (typeof defaultValue !== 'number') {
+ return {
+ key,
+ value: defaultValue,
+ variant: null,
+ reason: null,
+ error: 'TYPE_MISMATCH'
+ };
+ }
+
+ return this.getDetails(key, defaultValue);
+ };
+
+ /**
+ * 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: ObjectValue
+ ): FlagDetails => {
+ if (typeof defaultValue !== 'object' || defaultValue === null) {
+ return {
+ key,
+ value: defaultValue,
+ variant: null,
+ reason: null,
+ error: 'TYPE_MISMATCH'
+ };
+ }
+
+ 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__/DdFlags.test.ts b/packages/core/src/flags/__tests__/DdFlags.test.ts
new file mode 100644
index 000000000..2544fac1c
--- /dev/null
+++ b/packages/core/src/flags/__tests__/DdFlags.test.ts
@@ -0,0 +1,59 @@
+/*
+ * 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 { DdFlags } from '../DdFlags';
+
+jest.mock('../../InternalLog', () => {
+ return {
+ InternalLog: {
+ log: jest.fn()
+ },
+ DATADOG_MESSAGE_PREFIX: 'DATADOG:'
+ };
+});
+
+describe('DdFlags', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ // Reset state of DdFlags instance.
+ Object.assign(DdFlags, {
+ isFeatureEnabled: false,
+ clients: {}
+ });
+ });
+
+ describe('Initialization', () => {
+ 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.
+ expect(NativeModules.DdFlags.enable).toHaveBeenCalledTimes(3);
+ });
+
+ it('should print an error if retrieving the client before the feature is enabled', async () => {
+ DdFlags.getClient();
+
+ expect(InternalLog.log).toHaveBeenCalledWith(
+ '`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 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
new file mode 100644
index 000000000..4af21c638
--- /dev/null
+++ b/packages/core/src/flags/__tests__/FlagsClient.test.ts
@@ -0,0 +1,333 @@
+/*
+ * 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 { DdFlags } from '../DdFlags';
+
+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() },
+ DATADOG_MESSAGE_PREFIX: 'DATADOG:'
+ };
+});
+
+describe('FlagsClient', () => {
+ beforeEach(async () => {
+ jest.clearAllMocks();
+
+ // Reset state of the global DdFlags instance.
+ Object.assign(DdFlags, {
+ isFeatureEnabled: false,
+ clients: {}
+ });
+
+ await DdFlags.enable({ enabled: true });
+ });
+
+ describe('setEvaluationContext', () => {
+ it('should set the evaluation context', async () => {
+ const flagsClient = DdFlags.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.mockRejectedValueOnce(
+ new Error('NETWORK_ERROR')
+ );
+
+ const flagsClient = DdFlags.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('getDetails', () => {
+ it('should succesfully return flag details for flags', async () => {
+ // Flag values are mocked in the __mocks__/react-native.ts file.
+ const flagsClient = DdFlags.getClient();
+ await flagsClient.setEvaluationContext({
+ targetingKey: 'test-user-1',
+ attributes: { country: 'US' }
+ });
+
+ const booleanDetails = flagsClient.getBooleanDetails(
+ 'test-boolean-flag',
+ 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(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
+ });
+ });
+
+ it('should return PROVIDER_NOT_READY if evaluation context is not set', () => {
+ const flagsClient = DdFlags.getClient();
+ // Skip `setEvaluationContext` call here.
+
+ const details = flagsClient.getBooleanDetails(
+ 'test-boolean-flag',
+ false
+ );
+
+ expect(details).toMatchObject({
+ value: false,
+ reason: null,
+ error: 'PROVIDER_NOT_READY'
+ });
+ expect(InternalLog.log).toHaveBeenCalledWith(
+ expect.stringContaining('The evaluation context is not set'),
+ SdkVerbosity.ERROR
+ );
+ });
+
+ it('should return FLAG_NOT_FOUND if flag is missing from context', async () => {
+ const flagsClient = DdFlags.getClient();
+ 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: false,
+ reason: null,
+ error: 'FLAG_NOT_FOUND'
+ });
+ });
+
+ 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 = DdFlags.getClient();
+ 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 - testing validation
+ 'hello world'
+ );
+ const objectDetails = flagsClient.getObjectDetails(
+ 'test-object-flag',
+ // @ts-expect-error - testing validation
+ 'hello world'
+ );
+
+ // 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
+ });
+ });
+ });
+
+ describe('getValue', () => {
+ it('should succesfully return flag values', async () => {
+ // Flag values are mocked in the __mocks__/react-native.ts file.
+ const flagsClient = DdFlags.getClient();
+ 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',
+ -2
+ );
+ const objectValue = flagsClient.getObjectValue('test-object-flag', {
+ greeting: 'Default value'
+ });
+
+ expect(booleanValue).toBe(true);
+ expect(stringValue).toBe('hello world');
+ expect(numberValue).toBe(42);
+ expect(objectValue).toStrictEqual({
+ greeting: 'Greeting from the native side!'
+ });
+ });
+
+ 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 = DdFlags.getClient();
+ await flagsClient.setEvaluationContext({
+ targetingKey: 'test-user-1',
+ attributes: { country: 'US' }
+ });
+
+ 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',
+ // @ts-expect-error - testing validation
+ 'hello world'
+ );
+
+ // 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
new file mode 100644
index 000000000..fde6d9d1b
--- /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;
+ allocationKey: string;
+ variationKey: string;
+ variationType: string;
+ variationValue: string;
+ reason: string;
+ doLog: boolean;
+ extraLogging: Record;
+}
+
+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.attributes ?? {};
+
+ // 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
new file mode 100644
index 000000000..6b9028b14
--- /dev/null
+++ b/packages/core/src/flags/types.ts
@@ -0,0 +1,222 @@
+/*
+ * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
+ * This product includes software developed at Datadog (https://www.datadoghq.com/).
+ * Copyright 2016-Present Datadog, Inc.
+ */
+
+import type { FlagsClient } from './FlagsClient';
+
+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 `DdFlags.getClient()`.
+ *
+ * @example
+ * ```ts
+ * import { DdSdkReactNativeConfiguration, DdSdkReactNative, DdFlags } 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 DdFlags.enable(flagsConfig);
+ *
+ * // Retrieve the client and access feature flags.
+ * const flagsClient = DdFlags.getClient();
+ * const flagValue = await flagsClient.getBooleanValue('new-feature', false);
+ * ```
+ *
+ * @param configuration Configuration options for the Datadog Flags feature.
+ */
+ enable: (configuration?: DdFlagsConfiguration) => Promise;
+ /**
+ * 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 = DdFlags.getClient();
+ * const flagValue = await flagsClient.getBooleanValue('new-feature', false);
+ * ```
+ */
+ getClient: (clientName?: string) => FlagsClient;
+};
+
+/**
+ * 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 type DdFlagsConfiguration = {
+ /**
+ * Controls whether the feature flag evaluation feature is enabled.
+ */
+ enabled: boolean;
+ /**
+ * 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
+ */
+ customFlagsEndpoint?: string;
+ /**
+ * 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
+ */
+ 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;
+};
+
+/**
+ * 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.
+ *
+ * NOTE: Nested object values are not supported and will be omitted from the evaluation context.
+ */
+ attributes?: Record;
+}
+
+export type ObjectValue = { [key: string]: unknown };
+
+/**
+ * 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'
+ | 'PARSE_ERROR'
+ | '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;
+}
diff --git a/packages/core/src/index.tsx b/packages/core/src/index.tsx
index 8dcd5ed63..5d7197c1f 100644
--- a/packages/core/src/index.tsx
+++ b/packages/core/src/index.tsx
@@ -24,6 +24,8 @@ import { InternalLog } from './InternalLog';
import { ProxyConfiguration, ProxyType } from './ProxyConfiguration';
import { SdkVerbosity } from './SdkVerbosity';
import { TrackingConsent } from './TrackingConsent';
+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';
@@ -56,6 +58,7 @@ export {
FileBasedConfiguration,
InitializationMode,
DdLogs,
+ DdFlags,
DdTrace,
DdRum,
RumActionType,
@@ -93,5 +96,7 @@ export type {
Timestamp,
FirstPartyHost,
AutoInstrumentationConfiguration,
- PartialInitializationConfiguration
+ PartialInitializationConfiguration,
+ DdFlagsConfiguration,
+ FlagDetails
};
diff --git a/packages/core/src/nativeModulesTypes.ts b/packages/core/src/nativeModulesTypes.ts
index f7ac0f1f4..50544d6b8 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';
@@ -25,6 +26,11 @@ export type DdNativeLogsType = NativeDdLogs;
*/
export type DdNativeTraceType = NativeDdTrace;
+/**
+ * The entry point to use Datadog's Flags feature.
+ */
+export type DdNativeFlagsType = NativeDdFlags;
+
/**
* The entry point 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..5c8afd010
--- /dev/null
+++ b/packages/core/src/specs/NativeDdFlags.ts
@@ -0,0 +1,35 @@
+/*
+ * 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';
+
+import type { FlagCacheEntry } from '../flags/internal';
+
+/**
+ * Do not import this Spec directly, use DdNativeFlagsType instead.
+ */
+export interface Spec extends TurboModule {
+ readonly enable: (configuration: Object) => Promise;
+
+ readonly setEvaluationContext: (
+ clientName: string,
+ targetingKey: string,
+ attributes: Object
+ ) => Promise<{ [key: string]: FlagCacheEntry }>;
+
+ readonly trackEvaluation: (
+ clientName: string,
+ key: string,
+ rawFlag: Object,
+ targetingKey: string,
+ attributes: Object
+ ) => Promise;
+}
+
+// eslint-disable-next-line import/no-default-export
+export default TurboModuleRegistry.get('DdFlags');
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-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/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|
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')