diff --git a/example-new-architecture/App.tsx b/example-new-architecture/App.tsx
index e21a00e73..ca26a6fe9 100644
--- a/example-new-architecture/App.tsx
+++ b/example-new-architecture/App.tsx
@@ -11,7 +11,13 @@ import {
RumConfiguration,
DdFlags,
} from '@datadog/mobile-react-native';
-import React from 'react';
+import {DatadogProvider} from '@datadog/openfeature-react-native';
+import {
+ OpenFeature,
+ OpenFeatureProvider,
+ useObjectFlagDetails,
+} from '@openfeature/react-sdk';
+import React, {Suspense} from 'react';
import type {PropsWithChildren} from 'react';
import {
ActivityIndicator,
@@ -35,119 +41,91 @@ import {
import {APPLICATION_ID, CLIENT_TOKEN, ENVIRONMENT} from './ddCredentials';
(async () => {
- const config = new CoreConfiguration(
- CLIENT_TOKEN,
- ENVIRONMENT,
- );
+ const config = new CoreConfiguration(CLIENT_TOKEN, ENVIRONMENT);
config.verbosity = SdkVerbosity.DEBUG;
config.uploadFrequency = UploadFrequency.FREQUENT;
config.batchSize = BatchSize.SMALL;
+
+ // Enable RUM.
config.rumConfiguration = new RumConfiguration(
APPLICATION_ID,
true,
true,
- true
- )
+ true,
+ );
config.rumConfiguration.sessionSampleRate = 100;
config.rumConfiguration.telemetrySampleRate = 100;
+ // Initialize the Datadog SDK.
await DdSdkReactNative.initialize(config);
+
+ // Enable Flags.
+ await DdFlags.enable();
+
+ // Usage examples.
await DdRum.startView('main', 'Main');
setTimeout(async () => {
await DdRum.addTiming('one_second');
}, 1000);
await DdRum.addAction(RumActionType.CUSTOM, 'custom action');
+
await DdLogs.info('info log');
+
const spanId = await DdTrace.startSpan('test span');
await DdTrace.finishSpan(spanId);
})();
-type SectionProps = PropsWithChildren<{
- title: string;
-}>;
+function AppWithProviders() {
+ React.useEffect(() => {
+ const userId = 'user-123'
+
+ const evaluationContext = {
+ targetingKey: userId,
+ favoriteFruit: 'apple'
+ }
+
+ const provider = new DatadogProvider();
+ OpenFeature.setProvider(provider, evaluationContext);
+ }, [])
-function Section({children, title}: SectionProps): React.JSX.Element {
- const isDarkMode = useColorScheme() === 'dark';
return (
-
-
- {title}
-
-
- {children}
-
-
+
+
+
+ }>
+
+
+
+
);
}
function App(): React.JSX.Element {
- 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 greetingFlag = useObjectFlagDetails('rn-sdk-test-json-flag', {greeting: 'Default greeting'});
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)}
+
+
+
+ The title of this section is based on the {greetingFlag.flagKey} feature flag.{'\n\n'}
+
+ If it's different from "Default greeting", then it is coming from the feature flag evaluation.
+
Edit App.tsx to change this
screen and then come back to see your edits.
@@ -168,6 +146,36 @@ function App(): React.JSX.Element {
);
}
+type SectionProps = PropsWithChildren<{
+ title: string;
+}>;
+
+function Section({children, title}: SectionProps): React.JSX.Element {
+ const isDarkMode = useColorScheme() === 'dark';
+ return (
+
+
+ {title}
+
+
+ {children}
+
+
+ );
+}
+
const styles = StyleSheet.create({
sectionContainer: {
marginTop: 32,
@@ -187,4 +195,4 @@ const styles = StyleSheet.create({
},
});
-export default App;
+export default AppWithProviders;
diff --git a/example-new-architecture/package.json b/example-new-architecture/package.json
index b72ce69e4..9453e3f78 100644
--- a/example-new-architecture/package.json
+++ b/example-new-architecture/package.json
@@ -9,6 +9,8 @@
},
"dependencies": {
"@datadog/mobile-react-native": "workspace:packages/core",
+ "@datadog/openfeature-react-native": "workspace:packages/react-native-openfeature-provider",
+ "@openfeature/react-sdk": "^1.1.0",
"react": "18.3.1",
"react-native": "0.76.9"
},
diff --git a/packages/core/src/flags/DdFlags.ts b/packages/core/src/flags/DdFlags.ts
index a2b96f9bf..2a10a1de6 100644
--- a/packages/core/src/flags/DdFlags.ts
+++ b/packages/core/src/flags/DdFlags.ts
@@ -10,10 +10,13 @@ import type { DdNativeFlagsType } from '../nativeModulesTypes';
import { getGlobalInstance } from '../utils/singletonUtils';
import { FlagsClient } from './FlagsClient';
-import type { DdFlagsType, DdFlagsConfiguration } from './types';
+import type { DdFlagsType, FlagsConfiguration } from './types';
const FLAGS_MODULE = 'com.datadog.reactnative.flags';
+/**
+ * Implementation class for {@link DdFlagsType}. Please see the interface for documentation.
+ */
class DdFlagsWrapper implements DdFlagsType {
// eslint-disable-next-line global-require, @typescript-eslint/no-var-requires
private nativeFlags: DdNativeFlagsType = require('../specs/NativeDdFlags')
@@ -21,78 +24,19 @@ class DdFlagsWrapper implements DdFlagsType {
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(...);
+ * A map of client names to their corresponding {@link FlagsClient} instances.
*
- * // 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.
+ * Each of these clients hold their own context and flags state.
*/
- 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
- );
- }
+ private clients: Record = {};
- // Default `enabled` to `true`.
+ enable = async (configuration: FlagsConfiguration = {}): Promise => {
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(
diff --git a/packages/core/src/flags/FlagsClient.ts b/packages/core/src/flags/FlagsClient.ts
index a1acecae9..63c380994 100644
--- a/packages/core/src/flags/FlagsClient.ts
+++ b/packages/core/src/flags/FlagsClient.ts
@@ -8,12 +8,9 @@ import { InternalLog } from '../InternalLog';
import { SdkVerbosity } from '../SdkVerbosity';
import type { DdNativeFlagsType } from '../nativeModulesTypes';
-import {
- flagCacheEntryToFlagDetails,
- processEvaluationContext
-} from './internal';
+import { processEvaluationContext } from './internal';
import type { FlagCacheEntry } from './internal';
-import type { ObjectValue, EvaluationContext, FlagDetails } from './types';
+import type { JsonValue, EvaluationContext, FlagDetails } from './types';
export class FlagsClient {
// eslint-disable-next-line global-require, @typescript-eslint/no-var-requires
@@ -22,8 +19,9 @@ export class FlagsClient {
private clientName: string;
- private _evaluationContext: EvaluationContext | undefined = undefined;
- private _flagsCache: Record = {};
+ private evaluationContext: EvaluationContext | undefined = undefined;
+
+ private flagsCache: Record = {};
constructor(clientName: string = 'default') {
this.clientName = clientName;
@@ -34,7 +32,9 @@ export class FlagsClient {
*
* 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.
+ * Throws an error if there is an error setting the evaluation context and logs an error message.
+ *
+ * @param context The evaluation context to associate with the current client.
*
* @example
* ```ts
@@ -53,6 +53,7 @@ export class FlagsClient {
setEvaluationContext = async (
context: EvaluationContext
): Promise => {
+ // Make sure to process the incoming context because we don't support nested object values in context.
const processedContext = processEvaluationContext(context);
try {
@@ -62,8 +63,8 @@ export class FlagsClient {
processedContext.attributes ?? {}
);
- this._evaluationContext = processedContext;
- this._flagsCache = result;
+ this.evaluationContext = processedContext;
+ this.flagsCache = result;
} catch (error) {
if (error instanceof Error) {
InternalLog.log(
@@ -71,59 +72,72 @@ export class FlagsClient {
SdkVerbosity.ERROR
);
}
+
+ throw 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
- );
+ private track = (flag: FlagCacheEntry, context: EvaluationContext) => {
+ // A non-blocking call; don't await this.
+ this.nativeFlags
+ .trackEvaluation(
+ this.clientName,
+ flag.key,
+ flag,
+ context.targetingKey,
+ context.attributes ?? {}
+ )
+ .catch(error => {
+ if (error instanceof Error) {
+ InternalLog.log(
+ `Error tracking flag evaluation: ${error.message}`,
+ SdkVerbosity.WARN
+ );
+ }
+ });
+ };
+ private getDetails = (key: string, defaultValue: T): FlagDetails => {
+ if (!this.evaluationContext) {
return {
key,
value: defaultValue,
- variant: null,
- reason: null,
- error: 'PROVIDER_NOT_READY'
+ reason: 'ERROR',
+ errorCode: 'PROVIDER_NOT_READY',
+ errorMessage: `The evaluation context is not set for '${this.clientName}'. Please, set context before evaluating any flags.`
};
}
// Retrieve the flag from the cache.
- const flagCacheEntry = this._flagsCache[key];
+ const flag = this.flagsCache[key];
- if (!flagCacheEntry) {
+ if (!flag) {
return {
key,
value: defaultValue,
- variant: null,
- reason: null,
- error: 'FLAG_NOT_FOUND'
+ reason: 'ERROR',
+ errorCode: 'FLAG_NOT_FOUND'
};
}
- // Convert to FlagDetails.
- const details = flagCacheEntryToFlagDetails(flagCacheEntry);
+ this.track(flag, this.evaluationContext);
- // Track the flag evaluation. Don't await this; non-blocking.
- this.nativeFlags.trackEvaluation(
- this.clientName,
- key,
- flagCacheEntry,
- this._evaluationContext.targetingKey,
- this._evaluationContext.attributes ?? {}
- );
+ const details: FlagDetails = {
+ key: flag.key,
+ value: flag.value as T,
+ variant: flag.variationKey,
+ allocationKey: flag.allocationKey,
+ reason: flag.reason
+ };
return details;
};
/**
- * Evaluates a boolean feature flag with detailed evaluation information.
+ * Evaluate 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.
+ * @param defaultValue Fallback value for when flag evaluation fails, flag is not found, or the client does not have evaluation context set.
*/
getBooleanDetails = (
key: string,
@@ -133,9 +147,9 @@ export class FlagsClient {
return {
key,
value: defaultValue,
- variant: null,
- reason: null,
- error: 'TYPE_MISMATCH'
+ reason: 'ERROR',
+ errorCode: 'TYPE_MISMATCH',
+ errorMessage: 'Provided `defaultValue` is not a boolean.'
};
}
@@ -143,10 +157,10 @@ export class FlagsClient {
};
/**
- * Evaluates a string feature flag with detailed evaluation information.
+ * Evaluate 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.
+ * @param defaultValue Fallback value for when flag evaluation fails, flag is not found, or the client does not have evaluation context set.
*/
getStringDetails = (
key: string,
@@ -156,9 +170,9 @@ export class FlagsClient {
return {
key,
value: defaultValue,
- variant: null,
- reason: null,
- error: 'TYPE_MISMATCH'
+ reason: 'ERROR',
+ errorCode: 'TYPE_MISMATCH',
+ errorMessage: 'Provided `defaultValue` is not a string.'
};
}
@@ -166,10 +180,10 @@ export class FlagsClient {
};
/**
- * Evaluates a number feature flag with detailed evaluation information.
+ * Evaluate 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.
+ * @param defaultValue Fallback value for when flag evaluation fails, flag is not found, or the client does not have evaluation context set.
*/
getNumberDetails = (
key: string,
@@ -179,9 +193,9 @@ export class FlagsClient {
return {
key,
value: defaultValue,
- variant: null,
- reason: null,
- error: 'TYPE_MISMATCH'
+ reason: 'ERROR',
+ errorCode: 'TYPE_MISMATCH',
+ errorMessage: 'Provided `defaultValue` is not a number.'
};
}
@@ -189,33 +203,25 @@ export class FlagsClient {
};
/**
- * Evaluates an object feature flag with detailed evaluation information.
+ * Evaluate a JSON 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.
+ * @param defaultValue Fallback value for when flag evaluation fails, flag is not found, or the client does not have evaluation context set.
*/
- getObjectDetails = (
+ getObjectDetails = (
key: string,
- defaultValue: ObjectValue
- ): FlagDetails => {
- if (typeof defaultValue !== 'object' || defaultValue === null) {
- return {
- key,
- value: defaultValue,
- variant: null,
- reason: null,
- error: 'TYPE_MISMATCH'
- };
- }
+ defaultValue: T
+ ): FlagDetails => {
+ // OpenFeature provider spec assumes `defaultValue` can be any JSON value (including primitves) so no validation here.
return this.getDetails(key, defaultValue);
};
/**
- * Returns the value of a boolean feature flag.
+ * Evaluate a boolean feature flag value.
*
* @param key The key of the flag to evaluate.
- * @param defaultValue The value to return if the flag is not found or evaluation fails.
+ * @param defaultValue Fallback value for when flag evaluation fails, flag is not found, or the client does not have evaluation context set.
*
* @example
* ```ts
@@ -227,10 +233,10 @@ export class FlagsClient {
};
/**
- * Returns the value of a string feature flag.
+ * Evaluate a string feature flag value.
*
* @param key The key of the flag to evaluate.
- * @param defaultValue The value to return if the flag is not found or evaluation fails.
+ * @param defaultValue Fallback value for when flag evaluation fails, flag is not found, or the client does not have evaluation context set.
*
* @example
* ```ts
@@ -242,10 +248,10 @@ export class FlagsClient {
};
/**
- * Returns the value of a number feature flag.
+ * Evaluate a number feature flag value.
*
* @param key The key of the flag to evaluate.
- * @param defaultValue The value to return if the flag is not found or evaluation fails.
+ * @param defaultValue Fallback value for when flag evaluation fails, flag is not found, or the client does not have evaluation context set.
*
* @example
* ```ts
@@ -257,17 +263,17 @@ export class FlagsClient {
};
/**
- * Returns the value of an object feature flag.
+ * Evaluate an object feature flag value.
*
* @param key The key of the flag to evaluate.
- * @param defaultValue The value to return if the flag is not found or evaluation fails.
+ * @param defaultValue Fallback value for when flag evaluation fails, flag is not found, or the client does not have evaluation context set.
*
* @example
* ```ts
* const pageCalloutOptions = flagsClient.getObjectValue('page-callout', { color: 'purple', text: 'Woof!' });
* ```
*/
- getObjectValue = (key: string, defaultValue: ObjectValue): ObjectValue => {
+ getObjectValue = (key: string, defaultValue: T): T => {
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
index 2544fac1c..e7af6713c 100644
--- a/packages/core/src/flags/__tests__/DdFlags.test.ts
+++ b/packages/core/src/flags/__tests__/DdFlags.test.ts
@@ -29,31 +29,37 @@ describe('DdFlags', () => {
});
});
- 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 always call the native enable method with enabled set to true', async () => {
+ await DdFlags.enable();
+
+ expect(NativeModules.DdFlags.enable).toHaveBeenCalledWith({
+ enabled: true
});
+ });
- it('should print an error if retrieving the client before the feature is enabled', async () => {
- DdFlags.getClient();
+ it('should call the native enable method with the correct configuration', async () => {
+ await DdFlags.enable({
+ customExposureEndpoint: 'https://example.com',
+ customFlagsEndpoint: 'https://example.com',
+ trackExposures: false,
+ rumIntegrationEnabled: false
+ });
- 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
- );
+ expect(NativeModules.DdFlags.enable).toHaveBeenCalledWith({
+ enabled: true,
+ customExposureEndpoint: 'https://example.com',
+ customFlagsEndpoint: 'https://example.com',
+ trackExposures: false,
+ rumIntegrationEnabled: false
});
+ });
- it('should not print an error if retrieving the client after the feature is enabled', async () => {
- await DdFlags.enable();
- DdFlags.getClient();
+ it('should print an error when trying to retrieve a client before DdFlags.enable() was called', async () => {
+ DdFlags.getClient();
- expect(InternalLog.log).not.toHaveBeenCalled();
- });
+ 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
+ );
});
});
diff --git a/packages/core/src/flags/__tests__/FlagsClient.test.ts b/packages/core/src/flags/__tests__/FlagsClient.test.ts
index 4af21c638..24ba74852 100644
--- a/packages/core/src/flags/__tests__/FlagsClient.test.ts
+++ b/packages/core/src/flags/__tests__/FlagsClient.test.ts
@@ -78,7 +78,7 @@ describe('FlagsClient', () => {
clients: {}
});
- await DdFlags.enable({ enabled: true });
+ await DdFlags.enable();
});
describe('setEvaluationContext', () => {
@@ -94,16 +94,19 @@ describe('FlagsClient', () => {
).toHaveBeenCalledWith('default', 'test-user-1', { country: 'US' });
});
- it('should print an error if there is an error', async () => {
+ it('should throw an error if there is an error setting the evaluation context', async () => {
NativeModules.DdFlags.setEvaluationContext.mockRejectedValueOnce(
new Error('NETWORK_ERROR')
);
const flagsClient = DdFlags.getClient();
- await flagsClient.setEvaluationContext({
- targetingKey: 'test-user-1',
- attributes: { country: 'US' }
- });
+
+ await expect(
+ flagsClient.setEvaluationContext({
+ targetingKey: 'test-user-1',
+ attributes: { country: 'US' }
+ })
+ ).rejects.toThrow('NETWORK_ERROR');
expect(InternalLog.log).toHaveBeenCalledWith(
'Error setting flag evaluation context: NETWORK_ERROR',
@@ -141,26 +144,22 @@ describe('FlagsClient', () => {
expect(booleanDetails).toMatchObject({
value: true,
variant: 'true',
- reason: 'STATIC',
- error: null
+ reason: 'STATIC'
});
expect(stringDetails).toMatchObject({
value: 'hello world',
variant: 'Hello World',
- reason: 'STATIC',
- error: null
+ reason: 'STATIC'
});
expect(numberDetails).toMatchObject({
value: 42,
variant: '42',
- reason: 'STATIC',
- error: null
+ reason: 'STATIC'
});
expect(objectDetails).toMatchObject({
value: { greeting: 'Greeting from the native side!' },
variant: 'Native Greeting',
- reason: 'STATIC',
- error: null
+ reason: 'STATIC'
});
});
@@ -175,13 +174,12 @@ describe('FlagsClient', () => {
expect(details).toMatchObject({
value: false,
- reason: null,
- error: 'PROVIDER_NOT_READY'
+ reason: 'ERROR',
+ errorCode: 'PROVIDER_NOT_READY',
+ errorMessage: expect.stringContaining(
+ 'The evaluation context is not set'
+ )
});
- 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 () => {
@@ -199,8 +197,8 @@ describe('FlagsClient', () => {
expect(details).toMatchObject({
value: false,
- reason: null,
- error: 'FLAG_NOT_FOUND'
+ reason: 'ERROR',
+ errorCode: 'FLAG_NOT_FOUND'
});
});
@@ -229,34 +227,29 @@ describe('FlagsClient', () => {
);
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
+ errorCode: 'TYPE_MISMATCH',
+ reason: 'ERROR'
});
expect(stringDetails).toMatchObject({
value: true,
- error: 'TYPE_MISMATCH',
- reason: null,
- variant: null
+ errorCode: 'TYPE_MISMATCH',
+ reason: 'ERROR'
});
expect(numberDetails).toMatchObject({
value: 'hello world',
- error: 'TYPE_MISMATCH',
- reason: null,
- variant: null
+ errorCode: 'TYPE_MISMATCH',
+ reason: 'ERROR'
});
- expect(objectDetails).toMatchObject({
- value: 'hello world',
- error: 'TYPE_MISMATCH',
- reason: null,
- variant: null
+
+ // We don't do validation on the object value as it can hold any JSON value.
+ expect(objectDetails.value).toMatchObject({
+ greeting: 'Greeting from the native side!'
});
});
});
@@ -319,7 +312,6 @@ describe('FlagsClient', () => {
);
const objectValue = flagsClient.getObjectValue(
'test-object-flag',
- // @ts-expect-error - testing validation
'hello world'
);
@@ -327,7 +319,11 @@ describe('FlagsClient', () => {
expect(booleanValue).toBe('hello world');
expect(stringValue).toBe(true);
expect(numberValue).toBe('hello world');
- expect(objectValue).toBe('hello world');
+
+ // We don't do validation on the object value as it can hold any JSON value.
+ expect(objectValue).toMatchObject({
+ greeting: 'Greeting from the native side!'
+ });
});
});
});
diff --git a/packages/core/src/flags/internal.ts b/packages/core/src/flags/internal.ts
index fde6d9d1b..adb6bcfac 100644
--- a/packages/core/src/flags/internal.ts
+++ b/packages/core/src/flags/internal.ts
@@ -1,7 +1,7 @@
import { InternalLog } from '../InternalLog';
import { SdkVerbosity } from '../SdkVerbosity';
-import type { EvaluationContext, FlagDetails } from './types';
+import type { EvaluationContext, PrimitiveValue } from './types';
export interface FlagCacheEntry {
key: string;
@@ -15,41 +15,43 @@ export interface FlagCacheEntry {
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 };
+
+ // We should ignore non-primitive values in the context as per FFE SDK requirements OF.3.
+ const providedAttributes: Record =
+ context.attributes ?? {};
+
+ const attributes: Record = {};
+
+ for (const [key, value] of Object.entries(providedAttributes)) {
+ const isPrimitiveValue =
+ typeof value === 'boolean' ||
+ typeof value === 'string' ||
+ typeof value === 'number' ||
+ value === undefined ||
+ value === null;
+
+ if (!isPrimitiveValue) {
+ InternalLog.log(
+ `Non-primitive context value under "${key}" is not supported. Omitting this atribute from the evaluation context.`,
+ SdkVerbosity.WARN
+ );
+
+ continue;
+ }
+
+ if (value === undefined) {
+ continue;
+ }
+
+ attributes[key] = value;
+ }
+
+ return {
+ targetingKey,
+ attributes
+ };
};
diff --git a/packages/core/src/flags/types.ts b/packages/core/src/flags/types.ts
index 6b9028b14..26b04a35d 100644
--- a/packages/core/src/flags/types.ts
+++ b/packages/core/src/flags/types.ts
@@ -6,7 +6,7 @@
import type { FlagsClient } from './FlagsClient';
-export type DdFlagsType = {
+export interface DdFlagsType {
/**
* Enables the Datadog Flags feature in your application.
*
@@ -35,7 +35,7 @@ export type DdFlagsType = {
*
* @param configuration Configuration options for the Datadog Flags feature.
*/
- enable: (configuration?: DdFlagsConfiguration) => Promise;
+ enable: (configuration?: FlagsConfiguration) => Promise;
/**
* Returns a `FlagsClient` instance for further feature flag evaluation.
*
@@ -46,25 +46,26 @@ export type DdFlagsType = {
*
* @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);
+ *
+ * // Set the evaluation context.
+ * await flagsClient.setEvaluationContext({
+ * targetingKey: 'user-123',
+ * attributes: {
+ * favoriteFruit: 'apple'
+ * }
+ * });
+ *
+ * const flagValue = 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;
+export interface FlagsConfiguration {
/**
* Custom server URL for retrieving flag assignments.
*
@@ -103,22 +104,31 @@ export type DdFlagsConfiguration = {
* @default true
*/
rumIntegrationEnabled?: boolean;
-};
+}
+
+export type PrimitiveValue = null | boolean | string | number;
+type JsonObject = { [key: string]: JsonValue };
+type JsonArray = JsonValue[];
+/**
+ * Represents a JSON node value.
+ */
+export type JsonValue = PrimitiveValue | JsonObject | JsonArray;
/**
* 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.
+ * 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 granular targeting.
*
- * You can create an evaluation context and set it on the client before evaluating flags:
+ * Note: Evaluation context should be set before flag evaluations.
*
+ * @example
* ```ts
* const context: EvaluationContext = {
* targetingKey: "user-123",
* attributes: {
- * "email": "user@example.com",
+ * "region": "US",
* "plan": "premium",
* "age": 25,
* "beta_tester": true
@@ -134,6 +144,8 @@ export interface EvaluationContext {
*
* 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.
+ *
+ * Pass an empty string if you don't have such an identifier.
*/
targetingKey: string;
@@ -143,44 +155,28 @@ export interface EvaluationContext {
* Attributes can include user properties, session data, or any other contextual information
* needed for flag evaluation rules.
*
- * NOTE: Nested object values are not supported and will be omitted from the evaluation context.
+ * NOTE: Nested object values are not supported and will be dropped from the evaluation context.
*/
- attributes?: Record;
+ 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 =
+type FlagErrorCode =
| 'PROVIDER_NOT_READY'
| 'FLAG_NOT_FOUND'
| 'PARSE_ERROR'
- | 'TYPE_MISMATCH';
+ | 'TYPE_MISMATCH'
+ | 'TARGETING_KEY_MISSING';
/**
* Detailed information about a feature flag evaluation.
*
- * `FlagDetails` contains both the evaluated flag value and metadata about the evaluation,
+ * 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 {
/**
@@ -190,33 +186,31 @@ export interface FlagDetails {
/**
* The evaluated flag value.
*
- * This is either the flag's assigned value or the default value if evaluation failed.
+ * Falls back to the default value if evaluation failed.
*/
value: T;
+ /**
+ * The reason why this evaluation result was returned.
+ */
+ reason: string;
/**
* 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'}`);
- * ```
+ * Variants identify which version of the flag was served.
*/
- variant: string | null;
+ variant?: string;
/**
- * The reason why this evaluation result was returned.
+ * The allocation key for the evaluated flag.
*
- * Provides context about how the flag was evaluated, such as "TARGETING_MATCH" or "DEFAULT".
- * Returns `null` if the flag was not found.
+ * Useful for debugging targeting rules.
*/
- reason: string | null;
+ allocationKey?: string;
/**
- * 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.
+ * Code of the error that occurred during evaluation, if any.
+ */
+ errorCode?: FlagErrorCode;
+ /**
+ * Detailed explanation of the occurred error, if any.
*/
- error: FlagEvaluationError | null;
+ errorMessage?: string;
}
diff --git a/packages/core/src/index.tsx b/packages/core/src/index.tsx
index 5d7197c1f..d083e479b 100644
--- a/packages/core/src/index.tsx
+++ b/packages/core/src/index.tsx
@@ -25,7 +25,12 @@ 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 type { FlagsClient } from './flags/FlagsClient';
+import type {
+ FlagsConfiguration,
+ FlagDetails,
+ EvaluationContext
+} from './flags/types';
import { DdLogs } from './logs/DdLogs';
import { DdRum } from './rum/DdRum';
import { DdBabelInteractionTracking } from './rum/instrumentation/interactionTracking/DdBabelInteractionTracking';
@@ -97,6 +102,8 @@ export type {
FirstPartyHost,
AutoInstrumentationConfiguration,
PartialInitializationConfiguration,
- DdFlagsConfiguration,
+ FlagsConfiguration,
+ FlagsClient,
+ EvaluationContext,
FlagDetails
};
diff --git a/packages/react-native-openfeature-provider/babel.config.js b/packages/react-native-openfeature-provider/babel.config.js
new file mode 100644
index 000000000..990d54137
--- /dev/null
+++ b/packages/react-native-openfeature-provider/babel.config.js
@@ -0,0 +1,3 @@
+module.exports = {
+ presets: ['module:@react-native/babel-preset']
+};
diff --git a/packages/react-native-openfeature-provider/package.json b/packages/react-native-openfeature-provider/package.json
new file mode 100644
index 000000000..d506b060b
--- /dev/null
+++ b/packages/react-native-openfeature-provider/package.json
@@ -0,0 +1,86 @@
+{
+ "name": "@datadog/openfeature-react-native",
+ "version": "2.13.2",
+ "description": "A client-side React Native module to provide OpenFeature integration with Datadog Feature Flags",
+ "keywords": [
+ "datadog",
+ "react-native",
+ "ios",
+ "android",
+ "openfeature",
+ "feature-flags"
+ ],
+ "author": "Datadog (https://github.com/DataDog)",
+ "homepage": "https://github.com/DataDog/dd-sdk-reactnative#readme",
+ "repository": {
+ "url": "https://github.com/DataDog/dd-sdk-reactnative",
+ "directory": "packages/react-native-openfeature-provider"
+ },
+ "bugs": {
+ "url": "https://github.com/DataDog/dd-sdk-reactnative/issues"
+ },
+ "license": "Apache-2.0",
+ "main": "lib/commonjs/index",
+ "files": [
+ "src/**",
+ "lib/**"
+ ],
+ "types": "lib/typescript/react-native-openfeature-provider/src/index.d.ts",
+ "react-native": "src/index",
+ "source": "src/index",
+ "module": "lib/module/index",
+ "publishConfig": {
+ "access": "public"
+ },
+ "scripts": {
+ "test": "jest",
+ "lint": "eslint .",
+ "prepare": "rm -rf lib && yarn bob build"
+ },
+ "devDependencies": {
+ "@datadog/mobile-react-native": "^2.13.2",
+ "@openfeature/core": "^1.8.0",
+ "@openfeature/web-sdk": "^1.5.0",
+ "@testing-library/react-native": "7.0.2",
+ "react-native-builder-bob": "0.26.0",
+ "react-native-gesture-handler": "1.10.3"
+ },
+ "peerDependencies": {
+ "@datadog/mobile-react-native": "^2.13.2",
+ "@openfeature/web-sdk": "^1.5.0",
+ "react": ">=16.13.1",
+ "react-native": ">=0.63.4 <1.0"
+ },
+ "jest": {
+ "preset": "react-native",
+ "moduleNameMapper": {
+ "@datadog/mobile-react-native": "../core/src"
+ },
+ "modulePathIgnorePatterns": [
+ "/lib/"
+ ],
+ "setupFiles": [
+ "./../../node_modules/react-native-gesture-handler/jestSetup.js"
+ ],
+ "testPathIgnorePatterns": [
+ "/__utils__/"
+ ],
+ "transformIgnorePatterns": [
+ "jest-runner"
+ ]
+ },
+ "react-native-builder-bob": {
+ "source": "src",
+ "output": "lib",
+ "targets": [
+ "commonjs",
+ "module",
+ [
+ "typescript",
+ {
+ "tsc": "./../../node_modules/.bin/tsc"
+ }
+ ]
+ ]
+ }
+}
diff --git a/packages/react-native-openfeature-provider/src/index.ts b/packages/react-native-openfeature-provider/src/index.ts
new file mode 100644
index 000000000..f6239c2bf
--- /dev/null
+++ b/packages/react-native-openfeature-provider/src/index.ts
@@ -0,0 +1,5 @@
+import { DatadogProvider } from './provider';
+import type { DatadogProviderOptions } from './provider';
+
+export { DatadogProvider };
+export type { DatadogProviderOptions };
diff --git a/packages/react-native-openfeature-provider/src/provider.ts b/packages/react-native-openfeature-provider/src/provider.ts
new file mode 100644
index 000000000..c0faa997e
--- /dev/null
+++ b/packages/react-native-openfeature-provider/src/provider.ts
@@ -0,0 +1,195 @@
+/*
+ * 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 { DdFlags } from '@datadog/mobile-react-native';
+import type {
+ FlagDetails,
+ FlagsClient,
+ EvaluationContext as DdEvaluationContext,
+ FlagsConfiguration
+} from '@datadog/mobile-react-native';
+import { ErrorCode } from '@openfeature/web-sdk';
+import type {
+ EvaluationContext as OFEvaluationContext,
+ JsonValue,
+ Logger,
+ Paradigm,
+ Provider,
+ ProviderMetadata,
+ ResolutionDetails,
+ PrimitiveValue
+} from '@openfeature/web-sdk';
+
+export interface DatadogProviderOptions extends FlagsConfiguration {
+ /**
+ * The name of the Datadog Flags client to use.
+ *
+ * Provide this parameter in order to use different Datadog Flags clients for different OpenFeature domains.
+ *
+ * @default 'default'
+ */
+ clientName?: string;
+}
+
+export class DatadogProvider implements Provider {
+ readonly runsOn: Paradigm = 'client';
+ readonly metadata: ProviderMetadata = {
+ name: 'datadog-react-native'
+ };
+
+ private options: DatadogProviderOptions;
+ private flagsClient: FlagsClient | undefined;
+
+ constructor(options: DatadogProviderOptions = {}) {
+ options.clientName ??= 'default';
+
+ this.options = options;
+ }
+
+ async initialize(context: OFEvaluationContext = {}): Promise {
+ await DdFlags.enable(this.options);
+
+ const flagsClient = DdFlags.getClient(this.options.clientName);
+
+ await flagsClient.setEvaluationContext(toDdContext(context));
+
+ this.flagsClient = flagsClient;
+ }
+
+ async onContextChange(
+ _oldContext: OFEvaluationContext,
+ newContext: OFEvaluationContext
+ ): Promise {
+ if (!this.flagsClient) {
+ throw new Error(
+ 'DatadogProvider not initialized yet. Please wait until `OpenFeature.setProviderAndWait()` completes before setting evaluation context.'
+ );
+ }
+
+ await this.flagsClient.setEvaluationContext(toDdContext(newContext));
+ }
+
+ resolveBooleanEvaluation(
+ flagKey: string,
+ defaultValue: boolean,
+ _context: OFEvaluationContext,
+ _logger: Logger
+ ): ResolutionDetails {
+ if (!this.flagsClient) {
+ return {
+ value: defaultValue,
+ reason: 'ERROR',
+ errorCode: ErrorCode.PROVIDER_NOT_READY
+ };
+ }
+
+ const details = this.flagsClient.getBooleanDetails(
+ flagKey,
+ defaultValue
+ );
+ return toFlagResolution(details);
+ }
+
+ resolveStringEvaluation(
+ flagKey: string,
+ defaultValue: string,
+ _context: OFEvaluationContext,
+ _logger: Logger
+ ): ResolutionDetails {
+ if (!this.flagsClient) {
+ return {
+ value: defaultValue,
+ reason: 'ERROR',
+ errorCode: ErrorCode.PROVIDER_NOT_READY
+ };
+ }
+
+ const details = this.flagsClient.getStringDetails(
+ flagKey,
+ defaultValue
+ );
+ return toFlagResolution(details);
+ }
+
+ resolveNumberEvaluation(
+ flagKey: string,
+ defaultValue: number,
+ _context: OFEvaluationContext,
+ _logger: Logger
+ ): ResolutionDetails {
+ if (!this.flagsClient) {
+ return {
+ value: defaultValue,
+ reason: 'ERROR',
+ errorCode: ErrorCode.PROVIDER_NOT_READY
+ };
+ }
+
+ const details = this.flagsClient.getNumberDetails(
+ flagKey,
+ defaultValue
+ );
+ return toFlagResolution(details);
+ }
+
+ resolveObjectEvaluation(
+ flagKey: string,
+ defaultValue: T,
+ _context: OFEvaluationContext,
+ _logger: Logger
+ ): ResolutionDetails {
+ if (!this.flagsClient) {
+ return {
+ value: defaultValue,
+ reason: 'ERROR',
+ errorCode: ErrorCode.PROVIDER_NOT_READY
+ };
+ }
+
+ const details = this.flagsClient.getObjectDetails(
+ flagKey,
+ defaultValue
+ );
+ return toFlagResolution(details);
+ }
+}
+
+const toDdContext = (context: OFEvaluationContext): DdEvaluationContext => {
+ const { targetingKey, ...attributes } = context;
+
+ // Important ⚠️
+ // The Flags SDK doesn't support nested non-primitive values in the evaluation context as per OF.3 FFE SDK requirement.
+ // However, we let the SDK handle this inside of FlagsClient since it does this processing anyways.
+ const ddContextAttributes = attributes as Record;
+
+ return {
+ // Allow flag evaluations without a provided targeting key.
+ targetingKey: targetingKey ?? '',
+ attributes: ddContextAttributes
+ };
+};
+
+const toFlagResolution = (details: FlagDetails): ResolutionDetails => {
+ const {
+ value,
+ reason,
+ variant,
+ allocationKey,
+ errorCode,
+ errorMessage
+ } = details;
+
+ const result: ResolutionDetails = {
+ value,
+ reason,
+ variant,
+ flagMetadata: allocationKey ? { allocationKey } : undefined,
+ errorCode: errorCode as ErrorCode | undefined,
+ errorMessage
+ };
+
+ return result;
+};
diff --git a/packages/react-native-openfeature-provider/tsconfig.json b/packages/react-native-openfeature-provider/tsconfig.json
new file mode 100644
index 000000000..41716a7dd
--- /dev/null
+++ b/packages/react-native-openfeature-provider/tsconfig.json
@@ -0,0 +1,3 @@
+{
+ "extends": "../../tsconfig"
+}
diff --git a/yarn.lock b/yarn.lock
index dfcec9595..14f2d4402 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3926,6 +3926,24 @@ __metadata:
languageName: node
linkType: hard
+"@datadog/openfeature-react-native@workspace:packages/react-native-openfeature-provider":
+ version: 0.0.0-use.local
+ resolution: "@datadog/openfeature-react-native@workspace:packages/react-native-openfeature-provider"
+ dependencies:
+ "@datadog/mobile-react-native": ^2.13.2
+ "@openfeature/core": ^1.8.0
+ "@openfeature/web-sdk": ^1.5.0
+ "@testing-library/react-native": 7.0.2
+ react-native-builder-bob: 0.26.0
+ react-native-gesture-handler: 1.10.3
+ peerDependencies:
+ "@datadog/mobile-react-native": ^2.13.2
+ "@openfeature/web-sdk": ^1.5.0
+ react: ">=16.13.1"
+ react-native: ">=0.63.4 <1.0"
+ languageName: unknown
+ linkType: soft
+
"@datadog/pprof@npm:5.8.2":
version: 5.8.2
resolution: "@datadog/pprof@npm:5.8.2"
@@ -5628,6 +5646,32 @@ __metadata:
languageName: node
linkType: hard
+"@openfeature/core@npm:^1.8.0":
+ version: 1.9.1
+ resolution: "@openfeature/core@npm:1.9.1"
+ checksum: e69eef52e5467eec5376847f1dd60737610d4e52e592e3957db210efb8d84c6c71d60604a0de14c148687e1caebb6981a85eb18fbf70c92c9d5177f7c76ff047
+ languageName: node
+ linkType: hard
+
+"@openfeature/react-sdk@npm:^1.1.0":
+ version: 1.1.0
+ resolution: "@openfeature/react-sdk@npm:1.1.0"
+ peerDependencies:
+ "@openfeature/web-sdk": ^1.5.0
+ react: ">=16.8.0"
+ checksum: a87854bd200a8eaa79a40708771111b39735616b30a212cc6cb615d899cba6b7e94747c2e5d57c1427bc05c74add24362e411b16a11ef87698f8ca7750bfd295
+ languageName: node
+ linkType: hard
+
+"@openfeature/web-sdk@npm:^1.5.0":
+ version: 1.7.2
+ resolution: "@openfeature/web-sdk@npm:1.7.2"
+ peerDependencies:
+ "@openfeature/core": ^1.9.0
+ checksum: d759c927699e7aa18bcb5790e1c37d6492c128c069f071a51b600ad2118daf7e1cc4b959334f38eff142ae795fc3eeb1dd406ad4205cd7e3d3395a391f6431f6
+ languageName: node
+ linkType: hard
+
"@opentelemetry/api@npm:>=1.0.0 <1.9.0":
version: 1.8.0
resolution: "@opentelemetry/api@npm:1.8.0"
@@ -8649,6 +8693,8 @@ __metadata:
"@babel/preset-env": ^7.25.3
"@babel/runtime": ^7.26.10
"@datadog/mobile-react-native": "workspace:packages/core"
+ "@datadog/openfeature-react-native": "workspace:packages/react-native-openfeature-provider"
+ "@openfeature/react-sdk": ^1.1.0
"@react-native-community/cli": 15.0.1
"@react-native-community/cli-platform-android": 15.0.1
"@react-native-community/cli-platform-ios": 15.0.1