From 0227ce3411113502ffdad641721f41f7be04c3f6 Mon Sep 17 00:00:00 2001 From: typotter Date: Thu, 11 Jun 2026 15:59:18 -0600 Subject: [PATCH 1/4] FFL-2510: Thread extraLogging into FlagDetails and OpenFeature flagMetadata - Add extraLogging field to FlagDetails typed as Record - Add buildExtraLogging helper in FlagsClient that filters to primitives and drops allocationKey key - Populate extraLogging in getDetails from flag cache entry - Update toFlagResolution in provider.ts to spread extraLogging into flagMetadata, with allocationKey winning on collision - Add __mocks__/react-native.ts to openfeature package for test infrastructure - Add provider.test.ts covering extraLogging threading and allocationKey collision semantics --- packages/core/src/flags/FlagsClient.ts | 25 ++- .../src/flags/__tests__/FlagsClient.test.ts | 66 ++++++++ packages/core/src/flags/types.ts | 6 + .../__mocks__/react-native.ts | 16 ++ .../src/__tests__/provider.test.ts | 146 ++++++++++++++++++ .../react-native-openfeature/src/provider.ts | 14 +- 6 files changed, 270 insertions(+), 3 deletions(-) create mode 100644 packages/react-native-openfeature/__mocks__/react-native.ts create mode 100644 packages/react-native-openfeature/src/__tests__/provider.test.ts diff --git a/packages/core/src/flags/FlagsClient.ts b/packages/core/src/flags/FlagsClient.ts index 62284cc40..c82a00f40 100644 --- a/packages/core/src/flags/FlagsClient.ts +++ b/packages/core/src/flags/FlagsClient.ts @@ -10,7 +10,7 @@ import type { DdNativeFlagsType } from '../nativeModulesTypes'; import { processEvaluationContext } from './internal'; import type { FlagCacheEntry } from './internal'; -import type { JsonValue, EvaluationContext, FlagDetails } from './types'; +import type { JsonValue, EvaluationContext, FlagDetails, PrimitiveValue } from './types'; export class FlagsClient { // eslint-disable-next-line global-require, @typescript-eslint/no-var-requires @@ -143,7 +143,8 @@ export class FlagsClient { value: flag.value as T, variant: flag.variationKey, allocationKey: flag.allocationKey, - reason: flag.reason + reason: flag.reason, + extraLogging: buildExtraLogging(flag.extraLogging) }; return details; @@ -264,3 +265,23 @@ export class FlagsClient { return this.getObjectDetails(key, defaultValue).value; }; } + +const buildExtraLogging = ( + raw: Record +): Record | undefined => { + if (!raw || Object.keys(raw).length === 0) return undefined; + + const result: Record = {}; + for (const [key, value] of Object.entries(raw)) { + if (key === 'allocationKey') continue; // typed field wins + if ( + typeof value === 'boolean' || + typeof value === 'string' || + typeof value === 'number' || + value === null + ) { + result[key] = value as PrimitiveValue; + } + } + return Object.keys(result).length > 0 ? result : undefined; +}; diff --git a/packages/core/src/flags/__tests__/FlagsClient.test.ts b/packages/core/src/flags/__tests__/FlagsClient.test.ts index 06903099c..ed5882898 100644 --- a/packages/core/src/flags/__tests__/FlagsClient.test.ts +++ b/packages/core/src/flags/__tests__/FlagsClient.test.ts @@ -58,6 +58,34 @@ jest.spyOn(NativeModules.DdFlags, 'setEvaluationContext').mockResolvedValue({ variationType: '', variationValue: '', extraLogging: {} + }, + 'test-flag-with-extra-logging': { + key: 'test-flag-with-extra-logging', + value: true, + allocationKey: 'alloc-abc', + variationKey: 'true', + reason: 'TARGETED', + doLog: true, + variationType: '', + variationValue: '', + extraLogging: { + campaignId: 'camp-123', + score: 42, + eligible: true, + allocationKey: 'should-be-skipped', + nestedObj: { foo: 'bar' } + } + }, + 'test-flag-empty-extra-logging': { + key: 'test-flag-empty-extra-logging', + value: 'green', + allocationKey: '', + variationKey: 'green', + reason: 'STATIC', + doLog: true, + variationType: '', + variationValue: '', + extraLogging: {} } }); @@ -206,6 +234,44 @@ describe('FlagsClient', () => { }); }); + it('should include extraLogging primitives (excluding allocationKey key) in details', async () => { + const flagsClient = DdFlags.getClient(); + await flagsClient.setEvaluationContext({ + targetingKey: 'test-user-1', + attributes: { country: 'US' } + }); + + const details = flagsClient.getBooleanDetails( + 'test-flag-with-extra-logging', + false + ); + + expect(details.extraLogging).toEqual({ + campaignId: 'camp-123', + score: 42, + eligible: true + // 'allocationKey' key is excluded (typed field wins) + // nestedObj is excluded (non-primitive) + }); + // allocationKey field itself is still populated + expect(details.allocationKey).toBe('alloc-abc'); + }); + + it('should return undefined extraLogging when extraLogging cache entry is empty', async () => { + const flagsClient = DdFlags.getClient(); + await flagsClient.setEvaluationContext({ + targetingKey: 'test-user-1', + attributes: { country: 'US' } + }); + + const details = flagsClient.getStringDetails( + 'test-flag-empty-extra-logging', + 'default' + ); + + expect(details.extraLogging).toBeUndefined(); + }); + it('should return TYPE_MISMATCH when using wrong typed accessor method', async () => { // Flag values are mocked in the __mocks__/react-native.ts file. const flagsClient = DdFlags.getClient(); diff --git a/packages/core/src/flags/types.ts b/packages/core/src/flags/types.ts index 12bfb18f6..6310cbc51 100644 --- a/packages/core/src/flags/types.ts +++ b/packages/core/src/flags/types.ts @@ -204,6 +204,12 @@ export interface FlagDetails { * Useful for debugging targeting rules. */ allocationKey?: string; + /** + * Extra logging metadata from the flag assignment. + * Contains only primitive values (boolean, string, number, null). + * Useful for debugging and analytics. + */ + extraLogging?: Record; /** * Code of the error that occurred during evaluation, if any. */ diff --git a/packages/react-native-openfeature/__mocks__/react-native.ts b/packages/react-native-openfeature/__mocks__/react-native.ts new file mode 100644 index 000000000..855d5a39c --- /dev/null +++ b/packages/react-native-openfeature/__mocks__/react-native.ts @@ -0,0 +1,16 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +// eslint-disable-next-line @typescript-eslint/no-var-requires +const actualRN = require('react-native'); + +actualRN.NativeModules.DdFlags = { + enable: jest.fn(() => Promise.resolve()), + setEvaluationContext: jest.fn(() => Promise.resolve({})), + trackEvaluation: jest.fn(() => Promise.resolve()) +}; + +module.exports = actualRN; diff --git a/packages/react-native-openfeature/src/__tests__/provider.test.ts b/packages/react-native-openfeature/src/__tests__/provider.test.ts new file mode 100644 index 000000000..e4242fd52 --- /dev/null +++ b/packages/react-native-openfeature/src/__tests__/provider.test.ts @@ -0,0 +1,146 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +import { NativeModules } from 'react-native'; + +import { DdFlags } from '@datadog/mobile-react-native'; + +import { DatadogOpenFeatureProvider } from '../provider'; + +jest.spyOn(NativeModules.DdFlags, 'setEvaluationContext').mockResolvedValue({ + 'bool-flag': { + key: 'bool-flag', + value: true, + allocationKey: 'alloc-xyz', + variationKey: 'true', + reason: 'TARGETED', + doLog: true, + variationType: '', + variationValue: '', + extraLogging: { + campaignId: 'camp-999', + score: 7, + eligible: false, + ignored: { nested: true } + } + }, + 'flag-no-alloc': { + key: 'flag-no-alloc', + value: 'hello', + allocationKey: '', + variationKey: 'hello', + reason: 'STATIC', + doLog: true, + variationType: '', + variationValue: '', + extraLogging: { + source: 'experiment-a' + } + }, + 'flag-alloc-key-collision': { + key: 'flag-alloc-key-collision', + value: true, + allocationKey: 'real-alloc', + variationKey: 'true', + reason: 'TARGETED', + doLog: true, + variationType: '', + variationValue: '', + extraLogging: { + allocationKey: 'impostor-alloc', + label: 'test' + } + }, + 'flag-empty-extra-logging': { + key: 'flag-empty-extra-logging', + value: 42, + allocationKey: '', + variationKey: '42', + reason: 'STATIC', + doLog: true, + variationType: '', + variationValue: '', + extraLogging: {} + } +}); + +describe('DatadogOpenFeatureProvider', () => { + let provider: DatadogOpenFeatureProvider; + + beforeEach(async () => { + jest.clearAllMocks(); + + Object.assign(DdFlags, { + isFeatureEnabled: false, + clients: {} + }); + + await DdFlags.enable(); + + provider = new DatadogOpenFeatureProvider(); + await provider.initialize({ targetingKey: 'user-1' }); + }); + + describe('toFlagResolution / flagMetadata', () => { + it('should include extraLogging primitives in flagMetadata', () => { + const result = provider.resolveBooleanEvaluation( + 'bool-flag', + false, + {}, + {} as any + ); + + expect(result.flagMetadata).toEqual({ + campaignId: 'camp-999', + score: 7, + eligible: false, + allocationKey: 'alloc-xyz' + }); + // Non-primitive 'ignored' key should NOT appear + expect(result.flagMetadata).not.toHaveProperty('ignored'); + }); + + it('should include extraLogging in flagMetadata when there is no allocationKey', () => { + const result = provider.resolveStringEvaluation( + 'flag-no-alloc', + 'default', + {}, + {} as any + ); + + expect(result.flagMetadata).toEqual({ + source: 'experiment-a' + }); + }); + + it('should use the typed allocationKey field, not the allocationKey key from extraLogging', () => { + const result = provider.resolveBooleanEvaluation( + 'flag-alloc-key-collision', + false, + {}, + {} as any + ); + + // The typed allocationKey wins; extraLogging's 'allocationKey' is excluded + expect(result.flagMetadata?.allocationKey).toBe('real-alloc'); + expect(result.flagMetadata).toEqual({ + label: 'test', + allocationKey: 'real-alloc' + }); + }); + + it('should return undefined flagMetadata when extraLogging is empty and allocationKey is absent', () => { + const result = provider.resolveNumberEvaluation( + 'flag-empty-extra-logging', + 0, + {}, + {} as any + ); + + expect(result.flagMetadata).toBeUndefined(); + }); + }); +}); diff --git a/packages/react-native-openfeature/src/provider.ts b/packages/react-native-openfeature/src/provider.ts index 6cbae29ca..44cf2eefc 100644 --- a/packages/react-native-openfeature/src/provider.ts +++ b/packages/react-native-openfeature/src/provider.ts @@ -160,6 +160,7 @@ const toFlagResolution = (details: FlagDetails): ResolutionDetails => { reason, variant, allocationKey, + extraLogging, errorCode, errorMessage } = details; @@ -167,11 +168,22 @@ const toFlagResolution = (details: FlagDetails): ResolutionDetails => { const parsedErrorCode = errorCode && (ErrorCode[errorCode as ErrorCode] || ErrorCode.GENERAL); + // Build flagMetadata: extraLogging primitives first, allocationKey last (wins on collision). + let flagMetadata: Record | undefined; + const hasExtraLogging = + extraLogging && Object.keys(extraLogging).length > 0; + if (allocationKey || hasExtraLogging) { + flagMetadata = { ...extraLogging }; + if (allocationKey) { + flagMetadata.allocationKey = allocationKey; + } + } + const result: ResolutionDetails = { value, reason, variant, - flagMetadata: allocationKey ? { allocationKey } : undefined, + flagMetadata: flagMetadata || undefined, errorCode: parsedErrorCode, errorMessage }; From 2a723ada2bdc3b177e79a1eecedf76775f935072 Mon Sep 17 00:00:00 2001 From: typotter Date: Thu, 11 Jun 2026 16:28:26 -0600 Subject: [PATCH 2/4] fix(FFL-2510): resolve lint and type errors in extraLogging implementation - Fix prettier formatting on multi-line import in FlagsClient.ts - Add curly braces to if/continue statements in buildExtraLogging (curly rule) - Fix import ordering and remove blank line between same-section imports in provider.test.ts (arca rules) - Narrow flagMetadata type to Record in provider.ts to match OpenFeature FlagMetadata; filter null values when building flagMetadata --- packages/core/src/flags/FlagsClient.ts | 15 ++++++++++++--- .../src/__tests__/provider.test.ts | 3 +-- packages/react-native-openfeature/src/provider.ts | 12 ++++++++++-- 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/packages/core/src/flags/FlagsClient.ts b/packages/core/src/flags/FlagsClient.ts index c82a00f40..003c02189 100644 --- a/packages/core/src/flags/FlagsClient.ts +++ b/packages/core/src/flags/FlagsClient.ts @@ -10,7 +10,12 @@ import type { DdNativeFlagsType } from '../nativeModulesTypes'; import { processEvaluationContext } from './internal'; import type { FlagCacheEntry } from './internal'; -import type { JsonValue, EvaluationContext, FlagDetails, PrimitiveValue } from './types'; +import type { + JsonValue, + EvaluationContext, + FlagDetails, + PrimitiveValue +} from './types'; export class FlagsClient { // eslint-disable-next-line global-require, @typescript-eslint/no-var-requires @@ -269,11 +274,15 @@ export class FlagsClient { const buildExtraLogging = ( raw: Record ): Record | undefined => { - if (!raw || Object.keys(raw).length === 0) return undefined; + if (!raw || Object.keys(raw).length === 0) { + return undefined; + } const result: Record = {}; for (const [key, value] of Object.entries(raw)) { - if (key === 'allocationKey') continue; // typed field wins + if (key === 'allocationKey') { + continue; // typed field wins + } if ( typeof value === 'boolean' || typeof value === 'string' || diff --git a/packages/react-native-openfeature/src/__tests__/provider.test.ts b/packages/react-native-openfeature/src/__tests__/provider.test.ts index e4242fd52..5c8382aba 100644 --- a/packages/react-native-openfeature/src/__tests__/provider.test.ts +++ b/packages/react-native-openfeature/src/__tests__/provider.test.ts @@ -4,9 +4,8 @@ * Copyright 2016-Present Datadog, Inc. */ -import { NativeModules } from 'react-native'; - import { DdFlags } from '@datadog/mobile-react-native'; +import { NativeModules } from 'react-native'; import { DatadogOpenFeatureProvider } from '../provider'; diff --git a/packages/react-native-openfeature/src/provider.ts b/packages/react-native-openfeature/src/provider.ts index 44cf2eefc..05b014215 100644 --- a/packages/react-native-openfeature/src/provider.ts +++ b/packages/react-native-openfeature/src/provider.ts @@ -169,11 +169,19 @@ const toFlagResolution = (details: FlagDetails): ResolutionDetails => { errorCode && (ErrorCode[errorCode as ErrorCode] || ErrorCode.GENERAL); // Build flagMetadata: extraLogging primitives first, allocationKey last (wins on collision). - let flagMetadata: Record | undefined; + // OpenFeature FlagMetadata does not support null values, so null entries are omitted. + let flagMetadata: Record | undefined; const hasExtraLogging = extraLogging && Object.keys(extraLogging).length > 0; if (allocationKey || hasExtraLogging) { - flagMetadata = { ...extraLogging }; + flagMetadata = {}; + if (extraLogging) { + for (const [k, v] of Object.entries(extraLogging)) { + if (v !== null) { + flagMetadata[k] = v; + } + } + } if (allocationKey) { flagMetadata.allocationKey = allocationKey; } From 51c866382d22ee4138b98d431269e250d299494a Mon Sep 17 00:00:00 2001 From: typotter Date: Mon, 15 Jun 2026 08:51:47 -0600 Subject: [PATCH 3/4] fix(FFL-2510): treat null-only extraLogging as absent for flagMetadata hasExtraLogging now checks for at least one non-null value rather than any key at all, so extraLogging with only null entries no longer produces an empty {} flagMetadata when allocationKey is absent. --- .../src/__tests__/provider.test.ts | 24 +++++++++++++++++++ .../react-native-openfeature/src/provider.ts | 2 +- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/packages/react-native-openfeature/src/__tests__/provider.test.ts b/packages/react-native-openfeature/src/__tests__/provider.test.ts index 5c8382aba..7a4d9f4ea 100644 --- a/packages/react-native-openfeature/src/__tests__/provider.test.ts +++ b/packages/react-native-openfeature/src/__tests__/provider.test.ts @@ -63,6 +63,19 @@ jest.spyOn(NativeModules.DdFlags, 'setEvaluationContext').mockResolvedValue({ variationType: '', variationValue: '', extraLogging: {} + }, + 'flag-null-only-extra-logging': { + key: 'flag-null-only-extra-logging', + value: 42, + allocationKey: '', + variationKey: '42', + reason: 'STATIC', + doLog: true, + variationType: '', + variationValue: '', + extraLogging: { + nullField: null + } } }); @@ -141,5 +154,16 @@ describe('DatadogOpenFeatureProvider', () => { expect(result.flagMetadata).toBeUndefined(); }); + + it('should return undefined flagMetadata when extraLogging has only null values and allocationKey is absent', () => { + const result = provider.resolveNumberEvaluation( + 'flag-null-only-extra-logging', + 0, + {}, + {} as any + ); + + expect(result.flagMetadata).toBeUndefined(); + }); }); }); diff --git a/packages/react-native-openfeature/src/provider.ts b/packages/react-native-openfeature/src/provider.ts index 05b014215..6c0e065f0 100644 --- a/packages/react-native-openfeature/src/provider.ts +++ b/packages/react-native-openfeature/src/provider.ts @@ -172,7 +172,7 @@ const toFlagResolution = (details: FlagDetails): ResolutionDetails => { // OpenFeature FlagMetadata does not support null values, so null entries are omitted. let flagMetadata: Record | undefined; const hasExtraLogging = - extraLogging && Object.keys(extraLogging).length > 0; + extraLogging && Object.values(extraLogging).some(v => v !== null); if (allocationKey || hasExtraLogging) { flagMetadata = {}; if (extraLogging) { From 3a0536de1639684ad3e96778832f1888798a111f Mon Sep 17 00:00:00 2001 From: typotter Date: Thu, 18 Jun 2026 14:14:18 -0600 Subject: [PATCH 4/4] Remove extraLogging threading from OpenFeature flagMetadata Only allocationKey is surfaced in flagMetadata; extraLogging is no longer built or stored on FlagDetails. Remove buildExtraLogging(), FlagDetails.extraLogging, and related provider logic and tests. --- packages/core/src/flags/FlagsClient.ts | 34 +------ .../src/flags/__tests__/FlagsClient.test.ts | 48 +--------- packages/core/src/flags/types.ts | 6 -- .../src/__tests__/provider.test.ts | 96 +------------------ .../react-native-openfeature/src/provider.ts | 26 +---- 5 files changed, 12 insertions(+), 198 deletions(-) diff --git a/packages/core/src/flags/FlagsClient.ts b/packages/core/src/flags/FlagsClient.ts index 003c02189..62284cc40 100644 --- a/packages/core/src/flags/FlagsClient.ts +++ b/packages/core/src/flags/FlagsClient.ts @@ -10,12 +10,7 @@ import type { DdNativeFlagsType } from '../nativeModulesTypes'; import { processEvaluationContext } from './internal'; import type { FlagCacheEntry } from './internal'; -import type { - JsonValue, - EvaluationContext, - FlagDetails, - PrimitiveValue -} from './types'; +import type { JsonValue, EvaluationContext, FlagDetails } from './types'; export class FlagsClient { // eslint-disable-next-line global-require, @typescript-eslint/no-var-requires @@ -148,8 +143,7 @@ export class FlagsClient { value: flag.value as T, variant: flag.variationKey, allocationKey: flag.allocationKey, - reason: flag.reason, - extraLogging: buildExtraLogging(flag.extraLogging) + reason: flag.reason }; return details; @@ -270,27 +264,3 @@ export class FlagsClient { return this.getObjectDetails(key, defaultValue).value; }; } - -const buildExtraLogging = ( - raw: Record -): Record | undefined => { - if (!raw || Object.keys(raw).length === 0) { - return undefined; - } - - const result: Record = {}; - for (const [key, value] of Object.entries(raw)) { - if (key === 'allocationKey') { - continue; // typed field wins - } - if ( - typeof value === 'boolean' || - typeof value === 'string' || - typeof value === 'number' || - value === null - ) { - result[key] = value as PrimitiveValue; - } - } - return Object.keys(result).length > 0 ? result : undefined; -}; diff --git a/packages/core/src/flags/__tests__/FlagsClient.test.ts b/packages/core/src/flags/__tests__/FlagsClient.test.ts index ed5882898..4504f3cd1 100644 --- a/packages/core/src/flags/__tests__/FlagsClient.test.ts +++ b/packages/core/src/flags/__tests__/FlagsClient.test.ts @@ -59,8 +59,8 @@ jest.spyOn(NativeModules.DdFlags, 'setEvaluationContext').mockResolvedValue({ variationValue: '', extraLogging: {} }, - 'test-flag-with-extra-logging': { - key: 'test-flag-with-extra-logging', + 'test-flag-with-alloc-key': { + key: 'test-flag-with-alloc-key', value: true, allocationKey: 'alloc-abc', variationKey: 'true', @@ -68,23 +68,6 @@ jest.spyOn(NativeModules.DdFlags, 'setEvaluationContext').mockResolvedValue({ doLog: true, variationType: '', variationValue: '', - extraLogging: { - campaignId: 'camp-123', - score: 42, - eligible: true, - allocationKey: 'should-be-skipped', - nestedObj: { foo: 'bar' } - } - }, - 'test-flag-empty-extra-logging': { - key: 'test-flag-empty-extra-logging', - value: 'green', - allocationKey: '', - variationKey: 'green', - reason: 'STATIC', - doLog: true, - variationType: '', - variationValue: '', extraLogging: {} } }); @@ -234,7 +217,7 @@ describe('FlagsClient', () => { }); }); - it('should include extraLogging primitives (excluding allocationKey key) in details', async () => { + it('should populate allocationKey in details', async () => { const flagsClient = DdFlags.getClient(); await flagsClient.setEvaluationContext({ targetingKey: 'test-user-1', @@ -242,36 +225,13 @@ describe('FlagsClient', () => { }); const details = flagsClient.getBooleanDetails( - 'test-flag-with-extra-logging', + 'test-flag-with-alloc-key', false ); - expect(details.extraLogging).toEqual({ - campaignId: 'camp-123', - score: 42, - eligible: true - // 'allocationKey' key is excluded (typed field wins) - // nestedObj is excluded (non-primitive) - }); - // allocationKey field itself is still populated expect(details.allocationKey).toBe('alloc-abc'); }); - it('should return undefined extraLogging when extraLogging cache entry is empty', async () => { - const flagsClient = DdFlags.getClient(); - await flagsClient.setEvaluationContext({ - targetingKey: 'test-user-1', - attributes: { country: 'US' } - }); - - const details = flagsClient.getStringDetails( - 'test-flag-empty-extra-logging', - 'default' - ); - - expect(details.extraLogging).toBeUndefined(); - }); - it('should return TYPE_MISMATCH when using wrong typed accessor method', async () => { // Flag values are mocked in the __mocks__/react-native.ts file. const flagsClient = DdFlags.getClient(); diff --git a/packages/core/src/flags/types.ts b/packages/core/src/flags/types.ts index 6310cbc51..12bfb18f6 100644 --- a/packages/core/src/flags/types.ts +++ b/packages/core/src/flags/types.ts @@ -204,12 +204,6 @@ export interface FlagDetails { * Useful for debugging targeting rules. */ allocationKey?: string; - /** - * Extra logging metadata from the flag assignment. - * Contains only primitive values (boolean, string, number, null). - * Useful for debugging and analytics. - */ - extraLogging?: Record; /** * Code of the error that occurred during evaluation, if any. */ diff --git a/packages/react-native-openfeature/src/__tests__/provider.test.ts b/packages/react-native-openfeature/src/__tests__/provider.test.ts index 7a4d9f4ea..f20ac73e2 100644 --- a/packages/react-native-openfeature/src/__tests__/provider.test.ts +++ b/packages/react-native-openfeature/src/__tests__/provider.test.ts @@ -19,12 +19,7 @@ jest.spyOn(NativeModules.DdFlags, 'setEvaluationContext').mockResolvedValue({ doLog: true, variationType: '', variationValue: '', - extraLogging: { - campaignId: 'camp-999', - score: 7, - eligible: false, - ignored: { nested: true } - } + extraLogging: {} }, 'flag-no-alloc': { key: 'flag-no-alloc', @@ -35,47 +30,7 @@ jest.spyOn(NativeModules.DdFlags, 'setEvaluationContext').mockResolvedValue({ doLog: true, variationType: '', variationValue: '', - extraLogging: { - source: 'experiment-a' - } - }, - 'flag-alloc-key-collision': { - key: 'flag-alloc-key-collision', - value: true, - allocationKey: 'real-alloc', - variationKey: 'true', - reason: 'TARGETED', - doLog: true, - variationType: '', - variationValue: '', - extraLogging: { - allocationKey: 'impostor-alloc', - label: 'test' - } - }, - 'flag-empty-extra-logging': { - key: 'flag-empty-extra-logging', - value: 42, - allocationKey: '', - variationKey: '42', - reason: 'STATIC', - doLog: true, - variationType: '', - variationValue: '', extraLogging: {} - }, - 'flag-null-only-extra-logging': { - key: 'flag-null-only-extra-logging', - value: 42, - allocationKey: '', - variationKey: '42', - reason: 'STATIC', - doLog: true, - variationType: '', - variationValue: '', - extraLogging: { - nullField: null - } } }); @@ -97,7 +52,7 @@ describe('DatadogOpenFeatureProvider', () => { }); describe('toFlagResolution / flagMetadata', () => { - it('should include extraLogging primitives in flagMetadata', () => { + it('should include allocationKey in flagMetadata when present', () => { const result = provider.resolveBooleanEvaluation( 'bool-flag', false, @@ -106,16 +61,11 @@ describe('DatadogOpenFeatureProvider', () => { ); expect(result.flagMetadata).toEqual({ - campaignId: 'camp-999', - score: 7, - eligible: false, allocationKey: 'alloc-xyz' }); - // Non-primitive 'ignored' key should NOT appear - expect(result.flagMetadata).not.toHaveProperty('ignored'); }); - it('should include extraLogging in flagMetadata when there is no allocationKey', () => { + it('should return undefined flagMetadata when allocationKey is absent', () => { const result = provider.resolveStringEvaluation( 'flag-no-alloc', 'default', @@ -123,46 +73,6 @@ describe('DatadogOpenFeatureProvider', () => { {} as any ); - expect(result.flagMetadata).toEqual({ - source: 'experiment-a' - }); - }); - - it('should use the typed allocationKey field, not the allocationKey key from extraLogging', () => { - const result = provider.resolveBooleanEvaluation( - 'flag-alloc-key-collision', - false, - {}, - {} as any - ); - - // The typed allocationKey wins; extraLogging's 'allocationKey' is excluded - expect(result.flagMetadata?.allocationKey).toBe('real-alloc'); - expect(result.flagMetadata).toEqual({ - label: 'test', - allocationKey: 'real-alloc' - }); - }); - - it('should return undefined flagMetadata when extraLogging is empty and allocationKey is absent', () => { - const result = provider.resolveNumberEvaluation( - 'flag-empty-extra-logging', - 0, - {}, - {} as any - ); - - expect(result.flagMetadata).toBeUndefined(); - }); - - it('should return undefined flagMetadata when extraLogging has only null values and allocationKey is absent', () => { - const result = provider.resolveNumberEvaluation( - 'flag-null-only-extra-logging', - 0, - {}, - {} as any - ); - expect(result.flagMetadata).toBeUndefined(); }); }); diff --git a/packages/react-native-openfeature/src/provider.ts b/packages/react-native-openfeature/src/provider.ts index 6c0e065f0..4c4be7b6c 100644 --- a/packages/react-native-openfeature/src/provider.ts +++ b/packages/react-native-openfeature/src/provider.ts @@ -8,7 +8,8 @@ import { DdFlags } from '@datadog/mobile-react-native'; import type { FlagDetails, FlagsClient, - EvaluationContext as DdEvaluationContext + EvaluationContext as DdEvaluationContext, + PrimitiveValue } from '@datadog/mobile-react-native'; import { OpenFeatureEventEmitter, ErrorCode } from '@openfeature/web-sdk'; import type { @@ -19,7 +20,6 @@ import type { Provider, ProviderMetadata, ResolutionDetails, - PrimitiveValue, ProviderEventEmitter, ProviderEvents } from '@openfeature/web-sdk'; @@ -160,7 +160,6 @@ const toFlagResolution = (details: FlagDetails): ResolutionDetails => { reason, variant, allocationKey, - extraLogging, errorCode, errorMessage } = details; @@ -168,30 +167,11 @@ const toFlagResolution = (details: FlagDetails): ResolutionDetails => { const parsedErrorCode = errorCode && (ErrorCode[errorCode as ErrorCode] || ErrorCode.GENERAL); - // Build flagMetadata: extraLogging primitives first, allocationKey last (wins on collision). - // OpenFeature FlagMetadata does not support null values, so null entries are omitted. - let flagMetadata: Record | undefined; - const hasExtraLogging = - extraLogging && Object.values(extraLogging).some(v => v !== null); - if (allocationKey || hasExtraLogging) { - flagMetadata = {}; - if (extraLogging) { - for (const [k, v] of Object.entries(extraLogging)) { - if (v !== null) { - flagMetadata[k] = v; - } - } - } - if (allocationKey) { - flagMetadata.allocationKey = allocationKey; - } - } - const result: ResolutionDetails = { value, reason, variant, - flagMetadata: flagMetadata || undefined, + flagMetadata: allocationKey ? { allocationKey } : undefined, errorCode: parsedErrorCode, errorMessage };