From 75a485c3b01b20ce8c9365b2db22f288d39fd43e Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Thu, 15 Jan 2026 18:18:40 +0200 Subject: [PATCH 1/7] Implement OpenFeature provider for React Native --- example-new-architecture/App.tsx | 156 +++++++------- example-new-architecture/package.json | 2 + packages/core/src/flags/DdFlags.ts | 71 +------ packages/core/src/flags/FlagsClient.ts | 150 +++++++------- packages/core/src/flags/internal.ts | 70 ++++--- packages/core/src/flags/types.ts | 112 +++++----- packages/core/src/index.tsx | 11 +- .../babel.config.js | 3 + .../package.json | 86 ++++++++ .../src/index.ts | 5 + .../src/provider.ts | 195 ++++++++++++++++++ .../tsconfig.json | 3 + yarn.lock | 46 +++++ 13 files changed, 602 insertions(+), 308 deletions(-) create mode 100644 packages/react-native-openfeature-provider/babel.config.js create mode 100644 packages/react-native-openfeature-provider/package.json create mode 100644 packages/react-native-openfeature-provider/src/index.ts create mode 100644 packages/react-native-openfeature-provider/src/provider.ts create mode 100644 packages/react-native-openfeature-provider/tsconfig.json 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..a52428192 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') @@ -23,76 +26,12 @@ class DdFlagsWrapper implements DdFlagsType { 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`. + 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..4ff2f4760 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,7 @@ 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. + * @param context The evaluation context to associate with the current client. * * @example * ```ts @@ -53,6 +51,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 +61,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 +70,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 +145,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 +155,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 +168,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 +178,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 +191,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 +201,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 +231,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 +246,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 +261,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/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 From 8c63004b6216c2caa91974eefa6c5c395593bcdc Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Thu, 15 Jan 2026 18:40:19 +0200 Subject: [PATCH 2/7] Update tests --- packages/core/src/flags/DdFlags.ts | 5 ++ packages/core/src/flags/FlagsClient.ts | 2 + .../core/src/flags/__tests__/DdFlags.test.ts | 46 +++++++----- .../src/flags/__tests__/FlagsClient.test.ts | 74 +++++++++---------- 4 files changed, 68 insertions(+), 59 deletions(-) diff --git a/packages/core/src/flags/DdFlags.ts b/packages/core/src/flags/DdFlags.ts index a52428192..2a10a1de6 100644 --- a/packages/core/src/flags/DdFlags.ts +++ b/packages/core/src/flags/DdFlags.ts @@ -24,6 +24,11 @@ class DdFlagsWrapper implements DdFlagsType { private isFeatureEnabled = false; + /** + * A map of client names to their corresponding {@link FlagsClient} instances. + * + * Each of these clients hold their own context and flags state. + */ private clients: Record = {}; enable = async (configuration: FlagsConfiguration = {}): Promise => { diff --git a/packages/core/src/flags/FlagsClient.ts b/packages/core/src/flags/FlagsClient.ts index 4ff2f4760..63c380994 100644 --- a/packages/core/src/flags/FlagsClient.ts +++ b/packages/core/src/flags/FlagsClient.ts @@ -32,6 +32,8 @@ export class FlagsClient { * * Should be called before evaluating any flags. Otherwise, the client will fall back to serving default flag values. * + * 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 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!' + }); }); }); }); From 8617eb9b01b2587543dd6d33f9ac8a656c19a867 Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Tue, 20 Jan 2026 16:47:34 +0200 Subject: [PATCH 3/7] Update the DatadogProvider initialization logic, update the example new arch app --- example-new-architecture/App.tsx | 37 +++++---- .../src/provider.ts | 77 +++++++------------ 2 files changed, 49 insertions(+), 65 deletions(-) diff --git a/example-new-architecture/App.tsx b/example-new-architecture/App.tsx index ca26a6fe9..900812087 100644 --- a/example-new-architecture/App.tsx +++ b/example-new-architecture/App.tsx @@ -59,10 +59,14 @@ import {APPLICATION_ID, CLIENT_TOKEN, ENVIRONMENT} from './ddCredentials'; // Initialize the Datadog SDK. await DdSdkReactNative.initialize(config); - // Enable Flags. + // Enable Datadog Flags feature. await DdFlags.enable(); - // Usage examples. + // Set the provider with OpenFeature. + const provider = new DatadogProvider(); + OpenFeature.setProvider(provider); + + // Datadog SDK usage examples. await DdRum.startView('main', 'Main'); setTimeout(async () => { await DdRum.addTiming('one_second'); @@ -77,16 +81,16 @@ import {APPLICATION_ID, CLIENT_TOKEN, ENVIRONMENT} from './ddCredentials'; function AppWithProviders() { React.useEffect(() => { - const userId = 'user-123' - - const evaluationContext = { - targetingKey: userId, - favoriteFruit: 'apple' - } + const user = { + id: 'user-123', + favoriteFruit: 'apple', + }; - const provider = new DatadogProvider(); - OpenFeature.setProvider(provider, evaluationContext); - }, []) + OpenFeature.setContext({ + targetingKey: user.id, + favoriteFruit: user.favoriteFruit, + }); + }, []); return (
- 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. + 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.{'\n\n'} + Inspect greetingFlag in{' '} + App.tsx for more evaluation + details.
diff --git a/packages/react-native-openfeature-provider/src/provider.ts b/packages/react-native-openfeature-provider/src/provider.ts index c0faa997e..509bd5953 100644 --- a/packages/react-native-openfeature-provider/src/provider.ts +++ b/packages/react-native-openfeature-provider/src/provider.ts @@ -8,10 +8,9 @@ import { DdFlags } from '@datadog/mobile-react-native'; import type { FlagDetails, FlagsClient, - EvaluationContext as DdEvaluationContext, - FlagsConfiguration + EvaluationContext as DdEvaluationContext } from '@datadog/mobile-react-native'; -import { ErrorCode } from '@openfeature/web-sdk'; +import { OpenFeatureEventEmitter } from '@openfeature/web-sdk'; import type { EvaluationContext as OFEvaluationContext, JsonValue, @@ -20,10 +19,13 @@ import type { Provider, ProviderMetadata, ResolutionDetails, - PrimitiveValue + PrimitiveValue, + ProviderEventEmitter, + ProviderEvents, + ErrorCode } from '@openfeature/web-sdk'; -export interface DatadogProviderOptions extends FlagsConfiguration { +export interface DatadogProviderOptions { /** * The name of the Datadog Flags client to use. * @@ -41,35 +43,40 @@ export class DatadogProvider implements Provider { }; private options: DatadogProviderOptions; - private flagsClient: FlagsClient | undefined; + private flagsClient: FlagsClient; + + readonly events: ProviderEventEmitter = new OpenFeatureEventEmitter(); + private contextChangePromise = Promise.resolve(); constructor(options: DatadogProviderOptions = {}) { options.clientName ??= 'default'; - this.options = options; + + this.flagsClient = DdFlags.getClient(this.options.clientName); } async initialize(context: OFEvaluationContext = {}): Promise { - await DdFlags.enable(this.options); - - const flagsClient = DdFlags.getClient(this.options.clientName); - - await flagsClient.setEvaluationContext(toDdContext(context)); + const ddContext = toDdContext(context); + this.contextChangePromise = this.flagsClient.setEvaluationContext( + ddContext + ); - this.flagsClient = flagsClient; + await this.contextChangePromise; } 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.' - ); - } + const newDdContext = toDdContext(newContext); - await this.flagsClient.setEvaluationContext(toDdContext(newContext)); + // Promise chain in case `onContextChange` is called multiple times. + this.contextChangePromise = this.contextChangePromise.then(() => { + return this.flagsClient.setEvaluationContext(newDdContext); + }); + + // Wait for the current context change to complete. + await this.contextChangePromise; } resolveBooleanEvaluation( @@ -78,14 +85,6 @@ export class DatadogProvider implements Provider { _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 @@ -99,14 +98,6 @@ export class DatadogProvider implements Provider { _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 @@ -120,14 +111,6 @@ export class DatadogProvider implements Provider { _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 @@ -141,14 +124,6 @@ export class DatadogProvider implements Provider { _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 From 9607abe2db51fff91c1a5b8bd626c84d881dd14b Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Tue, 20 Jan 2026 19:07:31 +0200 Subject: [PATCH 4/7] Address PR comments --- example-new-architecture/App.tsx | 4 +++- packages/core/src/flags/internal.ts | 2 +- packages/core/src/flags/types.ts | 2 +- .../react-native-openfeature-provider/src/provider.ts | 10 ++++++---- 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/example-new-architecture/App.tsx b/example-new-architecture/App.tsx index 900812087..8df00cac0 100644 --- a/example-new-architecture/App.tsx +++ b/example-new-architecture/App.tsx @@ -107,7 +107,9 @@ function AppWithProviders() { } function App(): React.JSX.Element { - const greetingFlag = useObjectFlagDetails('rn-sdk-test-json-flag', {greeting: 'Default greeting'}); + const greetingFlag = useObjectFlagDetails('rn-sdk-test-json-flag', { + greeting: 'Default greeting', + }); const isDarkMode = useColorScheme() === 'dark'; const backgroundStyle = { diff --git a/packages/core/src/flags/internal.ts b/packages/core/src/flags/internal.ts index adb6bcfac..05b05cf42 100644 --- a/packages/core/src/flags/internal.ts +++ b/packages/core/src/flags/internal.ts @@ -36,7 +36,7 @@ export const processEvaluationContext = ( if (!isPrimitiveValue) { InternalLog.log( - `Non-primitive context value under "${key}" is not supported. Omitting this atribute from the evaluation context.`, + `Non-primitive context value under "${key}" is not supported. Omitting this attribute from the evaluation context.`, SdkVerbosity.WARN ); diff --git a/packages/core/src/flags/types.ts b/packages/core/src/flags/types.ts index 26b04a35d..f9ff1e003 100644 --- a/packages/core/src/flags/types.ts +++ b/packages/core/src/flags/types.ts @@ -161,7 +161,7 @@ export interface EvaluationContext { } /** - * An error tha occurs during feature flag evaluation. + * An error that occurs during feature flag evaluation. * * Indicates why a flag evaluation may have failed or returned a default value. */ diff --git a/packages/react-native-openfeature-provider/src/provider.ts b/packages/react-native-openfeature-provider/src/provider.ts index 509bd5953..9151c84d9 100644 --- a/packages/react-native-openfeature-provider/src/provider.ts +++ b/packages/react-native-openfeature-provider/src/provider.ts @@ -10,7 +10,7 @@ import type { FlagsClient, EvaluationContext as DdEvaluationContext } from '@datadog/mobile-react-native'; -import { OpenFeatureEventEmitter } from '@openfeature/web-sdk'; +import { OpenFeatureEventEmitter, ErrorCode } from '@openfeature/web-sdk'; import type { EvaluationContext as OFEvaluationContext, JsonValue, @@ -21,8 +21,7 @@ import type { ResolutionDetails, PrimitiveValue, ProviderEventEmitter, - ProviderEvents, - ErrorCode + ProviderEvents } from '@openfeature/web-sdk'; export interface DatadogProviderOptions { @@ -157,12 +156,15 @@ const toFlagResolution = (details: FlagDetails): ResolutionDetails => { errorMessage } = details; + const parsedErrorCode = + errorCode && (ErrorCode[errorCode as ErrorCode] || ErrorCode.GENERAL); + const result: ResolutionDetails = { value, reason, variant, flagMetadata: allocationKey ? { allocationKey } : undefined, - errorCode: errorCode as ErrorCode | undefined, + errorCode: parsedErrorCode, errorMessage }; From 58f50dcd2511b494a8f30dfa713057052b9bb84e Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Tue, 20 Jan 2026 19:56:35 +0200 Subject: [PATCH 5/7] Update 3rd party licenses --- LICENSE-3rdparty.csv | 2 ++ 1 file changed, 2 insertions(+) diff --git a/LICENSE-3rdparty.csv b/LICENSE-3rdparty.csv index 2d12ca9ea..3baff5a5f 100644 --- a/LICENSE-3rdparty.csv +++ b/LICENSE-3rdparty.csv @@ -32,6 +32,8 @@ dev,react-native-webview,MIT,"Copyright (c) 2015-present, Facebook, Inc." dev,react-test-renderer,MIT,"Copyright (c) Facebook, Inc. and its affiliates." dev,typescript,Apache-2.0,"Copyright Microsoft Corporation" dev,genversion,MIT,"Copyright (c) 2021 Akseli Palén" +dev,@openfeature/core,Apache-2.0,"Copyright (c) The OpenFeature Authors" +prod,@openfeature/web-sdk,Apache-2.0,"Copyright (c) The OpenFeature Authors" prod,chokidar,MIT,"Copyright (c) 2012 Paul Miller (https://paulmillr.com), Elan Shanker" prod,fast-glob,MIT,"Copyright (c) Denis Malinochkin" prod,svgo,MIT,"Copyright (c) Kir Belevich" From 89d98912cc8bd798bc4987e8598afa593d902df9 Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Wed, 21 Jan 2026 17:36:52 +0200 Subject: [PATCH 6/7] Rename package to correspond to the repo standards --- example-new-architecture/App.tsx | 2 +- example-new-architecture/package.json | 2 +- .../package.json | 2 +- yarn.lock | 38 +++++++++---------- 4 files changed, 22 insertions(+), 22 deletions(-) diff --git a/example-new-architecture/App.tsx b/example-new-architecture/App.tsx index 8df00cac0..695640c1e 100644 --- a/example-new-architecture/App.tsx +++ b/example-new-architecture/App.tsx @@ -11,7 +11,7 @@ import { RumConfiguration, DdFlags, } from '@datadog/mobile-react-native'; -import {DatadogProvider} from '@datadog/openfeature-react-native'; +import {DatadogProvider} from '@datadog/mobile-react-native-openfeature'; import { OpenFeature, OpenFeatureProvider, diff --git a/example-new-architecture/package.json b/example-new-architecture/package.json index 9453e3f78..765885691 100644 --- a/example-new-architecture/package.json +++ b/example-new-architecture/package.json @@ -9,7 +9,7 @@ }, "dependencies": { "@datadog/mobile-react-native": "workspace:packages/core", - "@datadog/openfeature-react-native": "workspace:packages/react-native-openfeature-provider", + "@datadog/mobile-react-native-openfeature": "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/react-native-openfeature-provider/package.json b/packages/react-native-openfeature-provider/package.json index d506b060b..703b06e29 100644 --- a/packages/react-native-openfeature-provider/package.json +++ b/packages/react-native-openfeature-provider/package.json @@ -1,5 +1,5 @@ { - "name": "@datadog/openfeature-react-native", + "name": "@datadog/mobile-react-native-openfeature", "version": "2.13.2", "description": "A client-side React Native module to provide OpenFeature integration with Datadog Feature Flags", "keywords": [ diff --git a/yarn.lock b/yarn.lock index 14f2d4402..09cf7e335 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3834,6 +3834,24 @@ __metadata: languageName: unknown linkType: soft +"@datadog/mobile-react-native-openfeature@workspace:packages/react-native-openfeature-provider": + version: 0.0.0-use.local + resolution: "@datadog/mobile-react-native-openfeature@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/mobile-react-native-session-replay@workspace:packages/react-native-session-replay": version: 0.0.0-use.local resolution: "@datadog/mobile-react-native-session-replay@workspace:packages/react-native-session-replay" @@ -3926,24 +3944,6 @@ __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" @@ -8693,7 +8693,7 @@ __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" + "@datadog/mobile-react-native-openfeature": "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 From 55edd45c7b2cbc1707a242d41e0118862c5cf43c Mon Sep 17 00:00:00 2001 From: Dmytrii Puzyr Date: Wed, 21 Jan 2026 18:18:43 +0200 Subject: [PATCH 7/7] Update the Old Architecture example app with OpenFeature Flags provider --- example/package.json | 1 + example/src/App.tsx | 61 ++++++++++++++++++------------ example/src/WixApp.tsx | 44 ++++++--------------- example/src/ddUtils.tsx | 9 ++++- example/src/screens/MainScreen.tsx | 42 ++++---------------- yarn.lock | 11 ++++++ 6 files changed, 77 insertions(+), 91 deletions(-) diff --git a/example/package.json b/example/package.json index d114dc49a..32fdb083b 100644 --- a/example/package.json +++ b/example/package.json @@ -15,6 +15,7 @@ "@datadog/mobile-react-native-session-replay": "workspace:packages/react-native-session-replay", "@datadog/mobile-react-native-webview": "workspace:packages/react-native-webview", "@datadog/mobile-react-navigation": "workspace:packages/react-navigation", + "@openfeature/react-sdk": "^1.2.0", "@react-native-async-storage/async-storage": "^2.1.2", "@react-native-community/cli": "15.0.1", "@react-native-community/cli-platform-android": "15.0.1", diff --git a/example/src/App.tsx b/example/src/App.tsx index 7a4de6a47..cf5d9e91d 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -7,11 +7,12 @@ import AboutScreen from './screens/AboutScreen'; import style from './screens/styles'; import { navigationRef } from './NavigationRoot'; import { DdRumReactNavigationTracking, ViewNamePredicate } from '@datadog/mobile-react-navigation'; -import {DatadogProvider, DatadogProviderConfiguration, FileBasedConfiguration, RumConfiguration} from '@datadog/mobile-react-native' +import { DatadogProvider, DatadogProviderConfiguration, FileBasedConfiguration, RumConfiguration, DdFlags, TrackingConsent } from '@datadog/mobile-react-native' +import { DatadogProvider as OpenFeatureDatadogProvider } from '@datadog/mobile-react-native-openfeature'; +import { OpenFeature, OpenFeatureProvider } from '@openfeature/react-sdk'; import { Route } from "@react-navigation/native"; import { NestedNavigator } from './screens/NestedNavigator/NestedNavigator'; import { getDatadogConfig, onDatadogInitialization } from './ddUtils'; -import { TrackingConsent } from '@datadog/mobile-react-native'; import { NavigationTrackingOptions, ParamsTrackingPredicate, ViewTrackingPredicate } from '@datadog/mobile-react-navigation/src/rum/instrumentation/DdRumReactNavigationTracking'; const Tab = createBottomTabNavigator(); @@ -21,7 +22,7 @@ const viewNamePredicate: ViewNamePredicate = function customViewNamePredicate(ro return "Custom RN " + trackedName; } -const viewTrackingPredicate: ViewTrackingPredicate = function customViewTrackingPredicate(route: Route) { +const viewTrackingPredicate: ViewTrackingPredicate = function customViewTrackingPredicate(route: Route) { if (route.name === "AlertModal") { return false; } @@ -29,7 +30,7 @@ const viewTrackingPredicate: ViewTrackingPredicate = function customViewTracking return true; } -const paramsTrackingPredicate: ParamsTrackingPredicate = function customParamsTrackingPredicate(route: Route) { +const paramsTrackingPredicate: ParamsTrackingPredicate = function customParamsTrackingPredicate(route: Route) { const filteredParams: any = {}; if (route.params?.creditCardNumber) { filteredParams["creditCardNumber"] = "XXXX XXXX XXXX XXXX"; @@ -57,9 +58,9 @@ const configuration = getDatadogConfig(TrackingConsent.GRANTED) // 3.- File based configuration from .json and custom mapper setup // const configuration = new FileBasedConfiguration( { -// configuration: require("../datadog-configuration.json").configuration, -// errorEventMapper: (event) => event, -// resourceEventMapper: (event) => event, +// configuration: require("../datadog-configuration.json").configuration, +// errorEventMapper: (event) => event, +// resourceEventMapper: (event) => event, // actionEventMapper: (event) => event}); // 4.- File based configuration from the native side (using initFromNative) @@ -68,26 +69,38 @@ const configuration = getDatadogConfig(TrackingConsent.GRANTED) // const configuration = new DatadogProviderConfiguration("fake_value", "fake_value"); // configuration.rumConfiguration = new RumConfiguration("fake_value") -export default function App() { +const handleDatadogInitialization = async () => { + onDatadogInitialization(); + + // Enable Datadog Flags feature. + await DdFlags.enable(); + // Set the provider with OpenFeature. + const provider = new OpenFeatureDatadogProvider(); + OpenFeature.setProvider(provider); +} + +export default function App() { return ( - - { - DdRumReactNavigationTracking.startTrackingViews( - navigationRef.current, - navigationTrackingOptions) - }}> - null + + + { + DdRumReactNavigationTracking.startTrackingViews( + navigationRef.current, + navigationTrackingOptions) }}> - - - - - - + null + }}> + + + + + + + ) } diff --git a/example/src/WixApp.tsx b/example/src/WixApp.tsx index 8664c3e3d..af685b934 100644 --- a/example/src/WixApp.tsx +++ b/example/src/WixApp.tsx @@ -14,6 +14,7 @@ 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'; +import { OpenFeatureProvider, useFlag } from '@openfeature/react-sdk'; // === Navigation Tracking custom predicates const viewNamePredicate: ViewNamePredicate = function customViewNamePredicate(_event: ComponentDidAppearEvent, trackedName: string) { @@ -62,45 +63,24 @@ function startReactNativeNavigation() { } function registerScreens() { - Navigation.registerComponent('Home', () => HomeScreen); + Navigation.registerComponent('Home', () => HomeScreenWithProviders); Navigation.registerComponent('Main', () => MainScreen); Navigation.registerComponent('Error', () => ErrorScreen); Navigation.registerComponent('Trace', () => TraceScreen); Navigation.registerComponent('About', () => AboutScreen); } -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 ( - - - - ) - } +const HomeScreenWithProviders = () => { + return ( + + + + ) +} - // TODO: [FFL-908] Use OpenFeature SDK instead of a manual client call. +const HomeScreen = props => { 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 + const flag = useFlag(testFlagKey, {greeting: "Default greeting"}); return ( @@ -147,7 +127,7 @@ const HomeScreen = props => { }); }} /> - {testFlagKey}: {JSON.stringify(testFlag)} + {testFlagKey}: {JSON.stringify(flag.value)} ); }; diff --git a/example/src/ddUtils.tsx b/example/src/ddUtils.tsx index ad6702a4a..5c0c38e73 100644 --- a/example/src/ddUtils.tsx +++ b/example/src/ddUtils.tsx @@ -8,6 +8,8 @@ import { TrackingConsent, DdFlags, } from '@datadog/mobile-react-native'; +import { DatadogProvider as OpenFeatureDatadogProvider } from '@datadog/mobile-react-native-openfeature'; +import { OpenFeature } from '@openfeature/react-sdk'; import {APPLICATION_ID, CLIENT_TOKEN, ENVIRONMENT} from './ddCredentials'; @@ -53,5 +55,10 @@ export function initializeDatadog(trackingConsent: TrackingConsent) { DdSdkReactNative.addAttributes({campaign: "ad-network"}) }); - DdFlags.enable() + // Enable the Flags feature. + DdFlags.enable().then(() => { + // Set the provider with OpenFeature. + const provider = new OpenFeatureDatadogProvider(); + OpenFeature.setProvider(provider); + }) } diff --git a/example/src/screens/MainScreen.tsx b/example/src/screens/MainScreen.tsx index ebf0360bc..bf9737254 100644 --- a/example/src/screens/MainScreen.tsx +++ b/example/src/screens/MainScreen.tsx @@ -9,9 +9,10 @@ import { View, Text, Button, TouchableOpacity, TouchableWithoutFeedback, TouchableNativeFeedback, ActivityIndicator } from 'react-native'; +import { DdLogs, DdSdkReactNative, TrackingConsent, DdFlags } from '@datadog/mobile-react-native'; +import { FeatureFlag } from '@openfeature/react-sdk'; import styles from './styles'; import { APPLICATION_KEY, API_KEY } from '../../src/ddCredentials'; -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,7 +28,6 @@ interface MainScreenState { resultTouchableNativeFeedback: string, trackingConsent: TrackingConsent, trackingConsentModalVisible: boolean - flagsInitialized: boolean } export default class MainScreen extends Component { @@ -41,8 +41,7 @@ export default class MainScreen extends Component { resultButtonAction: "", resultTouchableOpacityAction: "", trackingConsent: TrackingConsent.PENDING, - trackingConsentModalVisible: false, - flagsInitialized: false + trackingConsentModalVisible: false } as MainScreenState; this.consentModal = React.createRef() } @@ -96,7 +95,6 @@ export default class MainScreen extends Component { componentDidMount() { this.updateTrackingConsent() - this.initializeFlags(); DdLogs.debug("[DATADOG SDK] Test React Native Debug Log"); } @@ -108,24 +106,6 @@ 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) @@ -134,18 +114,13 @@ export default class MainScreen extends Component { } render() { - if (!this.state.flagsInitialized) { - return - - - } + return + Welcome!}> + Greetings from the Feature Flags! + - // 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 + The above greeting is being controlled by the{'\n'}`rn-sdk-test-boolean-flag` feature flag. - return - {this.state.welcomeMessage}