From cdada423acb65208bacb6ff3d85c7ea480a7ad2e Mon Sep 17 00:00:00 2001 From: Sameeran Kunche Date: Fri, 10 Jul 2026 07:54:59 -0700 Subject: [PATCH 01/13] FFL-2753 Add browser CoreProvider --- packages/browser/README.md | 21 ++ packages/browser/src/evaluation.ts | 35 ++- packages/browser/src/index.ts | 9 +- .../browser/src/openfeature/core-provider.ts | 164 +++++++++++++ packages/browser/src/openfeature/provider.ts | 45 +++- packages/browser/test/evaluation.spec.ts | 114 ++++++++- .../test/openfeature/core-provider.spec.ts | 231 ++++++++++++++++++ .../browser/test/openfeature/provider.spec.ts | 1 + .../core/src/configuration/configuration.ts | 11 + packages/core/src/configuration/wire.test.ts | 64 +++++ packages/core/src/configuration/wire.ts | 18 ++ .../core/src/evaluation/evaluateForSubject.ts | 1 + .../evaluation/evaluateForSubject.spec.ts | 2 + 13 files changed, 701 insertions(+), 15 deletions(-) create mode 100644 packages/browser/src/openfeature/core-provider.ts create mode 100644 packages/browser/test/openfeature/core-provider.spec.ts diff --git a/packages/browser/README.md b/packages/browser/README.md index 7071911d..1ac42149 100644 --- a/packages/browser/README.md +++ b/packages/browser/README.md @@ -90,6 +90,27 @@ console.log(result.value) // Flag value console.log(result.reason) // Evaluation reason ``` +### Using CoreProvider With Portable Configuration + +`CoreProvider` is an opt-in evaluation-only provider for applications that supply their own flags configuration, such as an SSR bootstrap or offline init payload. It does not fetch or poll configuration. + +For dynamic context, the generic configuration wire should contain rules-based flag configuration. Precomputed configuration can also be evaluated, but only for the matching context it was generated for. + +```javascript +import { configurationFromString, CoreProvider } from '@datadog/openfeature-browser' +import { OpenFeature } from '@openfeature/web-sdk' + +const configuration = configurationFromString(window.__DD_FLAGS_CONFIGURATION__) +const provider = new CoreProvider({ configuration }) + +await OpenFeature.setProviderAndWait(provider) + +await OpenFeature.setContext({ targetingKey: 'user-123', plan: 'enterprise' }) + +const client = OpenFeature.getClient() +const enabled = await client.getBooleanValue('new-checkout', false) +``` + ## End-user license agreement https://www.datadoghq.com/legal/eula diff --git a/packages/browser/src/evaluation.ts b/packages/browser/src/evaluation.ts index 48ae3d98..2bdf0025 100644 --- a/packages/browser/src/evaluation.ts +++ b/packages/browser/src/evaluation.ts @@ -4,19 +4,47 @@ import type { PrecomputedConfiguration, PrecomputedFlagMetadata, } from '@datadog/flagging-core' -import type { ErrorCode, EvaluationContext, FlagValueType, ResolutionDetails } from '@openfeature/web-sdk' +import { configMatchesContext, evaluateRulesBasedConfiguration } from '@datadog/flagging-core' +import type { ErrorCode, EvaluationContext, FlagValueType, Logger, ResolutionDetails } from '@openfeature/web-sdk' + +const NOOP_LOGGER: Logger = { + debug: () => {}, + info: () => {}, + warn: () => {}, + error: () => {}, +} export function evaluate( flagsConfiguration: FlagsConfiguration, type: T, flagKey: string, defaultValue: FlagTypeToValue, - context: EvaluationContext + context: EvaluationContext, + logger: Logger = NOOP_LOGGER ): ResolutionDetails> { - if (flagsConfiguration.precomputed) { + if (flagsConfiguration.precomputed && configMatchesContext(flagsConfiguration, context)) { return evaluatePrecomputed(flagsConfiguration.precomputed, type, flagKey, defaultValue, context) } + if (flagsConfiguration.rulesBased) { + return evaluateRulesBasedConfiguration( + flagsConfiguration.rulesBased.response, + type, + flagKey, + defaultValue, + context, + logger + ) + } + + if (flagsConfiguration.precomputed) { + return { + value: defaultValue, + reason: 'ERROR', + errorCode: 'INVALID_CONTEXT' as ErrorCode, + } + } + return { value: defaultValue, reason: 'DEFAULT', @@ -54,6 +82,7 @@ function evaluatePrecomputed( allocationKey: flag.allocationKey, variationType: flag.variationType, doLog: flag.doLog, + extraLogging: flag.extraLogging, } as PrecomputedFlagMetadata, reason: flag.reason, } as ResolutionDetails> diff --git a/packages/browser/src/index.ts b/packages/browser/src/index.ts index 9916ffab..9e4fec64 100644 --- a/packages/browser/src/index.ts +++ b/packages/browser/src/index.ts @@ -1,12 +1,17 @@ import { defineGlobal, getGlobalObject } from '@datadog/browser-core' +import { CoreProvider } from './openfeature/core-provider' import { DatadogProvider } from './openfeature/provider' export { configurationFromString, configurationToString } from '@datadog/flagging-core' export type { FlaggingInitConfiguration } from './domain/configuration' +export type { CoreProviderOptions } from './openfeature/core-provider' export { DatadogDevtools } from './openfeature/devtools-provider' -export { DatadogProvider } +export { CoreProvider, DatadogProvider } // Build environment placeholder for testing const _SDK_VERSION = __BUILD_ENV__SDK_VERSION__ -defineGlobal(getGlobalObject(), 'DD_FLAGGING' as keyof typeof globalThis, { Provider: DatadogProvider }) +defineGlobal(getGlobalObject(), 'DD_FLAGGING' as keyof typeof globalThis, { + Provider: DatadogProvider, + CoreProvider, +}) diff --git a/packages/browser/src/openfeature/core-provider.ts b/packages/browser/src/openfeature/core-provider.ts new file mode 100644 index 00000000..d5ca061e --- /dev/null +++ b/packages/browser/src/openfeature/core-provider.ts @@ -0,0 +1,164 @@ +import type { FlagsConfiguration, FlagTypeToValue } from '@datadog/flagging-core' +import { configMatchesContext } from '@datadog/flagging-core' +import type { + ErrorCode, + EvaluationContext, + FlagValueType, + JsonValue, + Logger, + Paradigm, + Provider, + ProviderMetadata, + ResolutionDetails, +} from '@openfeature/web-sdk' +import { + OpenFeatureEventEmitter, + type ProviderEventEmitter, + ProviderEvents, + ProviderStatus, +} from '@openfeature/web-sdk' +import { evaluate } from '../evaluation' + +export interface CoreProviderOptions { + configuration: FlagsConfiguration +} + +export class CoreProvider implements Provider { + readonly metadata: ProviderMetadata = { + name: 'datadog-core', + } + readonly runsOn: Paradigm = 'client' + readonly events: ProviderEventEmitter + + status: ProviderStatus + + private flagsConfiguration: FlagsConfiguration + private context: EvaluationContext = {} + + constructor(options: CoreProviderOptions) { + this.events = new OpenFeatureEventEmitter() + this.flagsConfiguration = options.configuration + this.status = hasEvaluatableConfiguration(options.configuration) ? ProviderStatus.READY : ProviderStatus.ERROR + } + + getConfiguration(): FlagsConfiguration { + return this.flagsConfiguration + } + + setConfiguration(configuration: FlagsConfiguration): void { + const hadEvaluatableConfiguration = this.canEvaluateCurrentContext() + this.flagsConfiguration = configuration + + const error = getConfigurationError(configuration, this.context) + if (error) { + this.status = ProviderStatus.ERROR + this.events.emit(ProviderEvents.Error, { error }) + return + } + + this.status = ProviderStatus.READY + this.events.emit(hadEvaluatableConfiguration ? ProviderEvents.ConfigurationChanged : ProviderEvents.Ready) + } + + async initialize(context: EvaluationContext = {}): Promise { + this.context = context + const error = getConfigurationError(this.flagsConfiguration, this.context) + if (error) { + this.status = ProviderStatus.ERROR + throw error + } + this.status = ProviderStatus.READY + } + + async onContextChange(_oldContext: EvaluationContext, newContext: EvaluationContext): Promise { + this.context = newContext + const error = getConfigurationError(this.flagsConfiguration, this.context) + if (error) { + this.status = ProviderStatus.ERROR + this.events.emit(ProviderEvents.Error, { error }) + throw error + } + this.status = ProviderStatus.READY + } + + resolveBooleanEvaluation( + flagKey: string, + defaultValue: boolean, + context: EvaluationContext, + logger: Logger + ): ResolutionDetails { + return this.resolve('boolean', flagKey, defaultValue, context, logger) + } + + resolveStringEvaluation( + flagKey: string, + defaultValue: string, + context: EvaluationContext, + logger: Logger + ): ResolutionDetails { + return this.resolve('string', flagKey, defaultValue, context, logger) + } + + resolveNumberEvaluation( + flagKey: string, + defaultValue: number, + context: EvaluationContext, + logger: Logger + ): ResolutionDetails { + return this.resolve('number', flagKey, defaultValue, context, logger) + } + + resolveObjectEvaluation( + flagKey: string, + defaultValue: T, + context: EvaluationContext, + logger: Logger + ): ResolutionDetails { + return this.resolve('object', flagKey, defaultValue, context, logger) as ResolutionDetails + } + + private resolve( + type: T, + flagKey: string, + defaultValue: FlagTypeToValue, + context: EvaluationContext, + logger: Logger + ): ResolutionDetails> { + if (!hasEvaluatableConfiguration(this.flagsConfiguration)) { + return { + value: defaultValue, + reason: 'ERROR', + errorCode: 'PROVIDER_NOT_READY' as ErrorCode, + } + } + + return evaluate(this.flagsConfiguration, type, flagKey, defaultValue, this.getEvaluationContext(context), logger) + } + + private getEvaluationContext(context: EvaluationContext): EvaluationContext { + if (Object.keys(context).length > 0 || Object.keys(this.context).length === 0) { + return context + } + return this.context + } + + private canEvaluateCurrentContext(): boolean { + return !getConfigurationError(this.flagsConfiguration, this.context) + } +} + +function hasEvaluatableConfiguration(configuration: FlagsConfiguration): boolean { + return !!(configuration.precomputed || configuration.rulesBased) +} + +function getConfigurationError(configuration: FlagsConfiguration, context: EvaluationContext): Error | undefined { + if (!hasEvaluatableConfiguration(configuration)) { + return new Error('No flags configuration has been set') + } + + if (!configuration.rulesBased && configuration.precomputed && !configMatchesContext(configuration, context)) { + return new Error('Precomputed flags configuration does not match the current context') + } + + return undefined +} diff --git a/packages/browser/src/openfeature/provider.ts b/packages/browser/src/openfeature/provider.ts index 3483c464..0dacf5e5 100644 --- a/packages/browser/src/openfeature/provider.ts +++ b/packages/browser/src/openfeature/provider.ts @@ -67,6 +67,7 @@ export class DatadogProvider implements Provider { status: ProviderStatus private flagsConfiguration: FlagsConfiguration = {} + private context: EvaluationContext = {} private flagsCache: IndexedDBFlagsCache | undefined private exposureCache: AssignmentCache | undefined @@ -184,6 +185,7 @@ export class DatadogProvider implements Provider { // scheduling). this.flagsConfiguration = config + this.context = context this.status = fromCache ? ProviderStatus.STALE : ProviderStatus.READY this.events.emit(ProviderEvents.ConfigurationChanged) @@ -271,7 +273,14 @@ export class DatadogProvider implements Provider { context: EvaluationContext, _logger: Logger ): ResolutionDetails { - return evaluate(this.flagsConfiguration, 'boolean', flagKey, defaultValue, context) + return evaluate( + this.flagsConfiguration, + 'boolean', + flagKey, + defaultValue, + this.getEvaluationContext(context), + _logger + ) } resolveStringEvaluation( @@ -280,7 +289,14 @@ export class DatadogProvider implements Provider { context: EvaluationContext, _logger: Logger ): ResolutionDetails { - return evaluate(this.flagsConfiguration, 'string', flagKey, defaultValue, context) + return evaluate( + this.flagsConfiguration, + 'string', + flagKey, + defaultValue, + this.getEvaluationContext(context), + _logger + ) } resolveNumberEvaluation( @@ -289,7 +305,14 @@ export class DatadogProvider implements Provider { context: EvaluationContext, _logger: Logger ): ResolutionDetails { - return evaluate(this.flagsConfiguration, 'number', flagKey, defaultValue, context) + return evaluate( + this.flagsConfiguration, + 'number', + flagKey, + defaultValue, + this.getEvaluationContext(context), + _logger + ) } resolveObjectEvaluation( @@ -304,6 +327,20 @@ export class DatadogProvider implements Provider { // type-sound way because there's no runtime information passed to // learn what type the user expects. So it's up to the user to // make sure they pass the appropriate type. - return evaluate(this.flagsConfiguration, 'object', flagKey, defaultValue, context) as ResolutionDetails + return evaluate( + this.flagsConfiguration, + 'object', + flagKey, + defaultValue, + this.getEvaluationContext(context), + _logger + ) as ResolutionDetails + } + + private getEvaluationContext(context: EvaluationContext): EvaluationContext { + if (Object.keys(context).length > 0 || Object.keys(this.context).length === 0) { + return context + } + return this.context } } diff --git a/packages/browser/test/evaluation.spec.ts b/packages/browser/test/evaluation.spec.ts index 4f640249..c787afa2 100644 --- a/packages/browser/test/evaluation.spec.ts +++ b/packages/browser/test/evaluation.spec.ts @@ -1,4 +1,4 @@ -import { configurationFromString } from '@datadog/flagging-core' +import { configurationFromString, type FlagsConfiguration, OperatorType } from '@datadog/flagging-core' import type { ErrorCode } from '@openfeature/web-sdk' import { evaluate } from '../src/evaluation' import configurationWire from './data/precomputed-v1-wire.json' @@ -7,6 +7,58 @@ const configuration = configurationFromString( // Adding stringify because import has parsed JSON JSON.stringify(configurationWire) ) +const matchingContext = configuration.precomputed?.context ?? {} + +const rulesBasedConfiguration: FlagsConfiguration = { + rulesBased: { + response: { + createdAt: '2026-07-06T23:01:56.822Z', + format: 'SERVER', + environment: { + name: 'prod', + }, + flags: { + 'dynamic-flag': { + key: 'dynamic-flag', + enabled: true, + variationType: 'STRING', + variations: { + enterprise: { key: 'enterprise', value: 'enabled' }, + fallback: { key: 'fallback', value: 'disabled' }, + }, + allocations: [ + { + key: 'enterprise-allocation', + rules: [ + { + conditions: [{ operator: OperatorType.ONE_OF, attribute: 'plan', value: ['enterprise'] }], + }, + ], + doLog: true, + splits: [ + { + variationKey: 'enterprise', + extraLogging: { experiment: 'dynamic-context' }, + shards: [{ salt: 'salt', ranges: [{ start: 0, end: 10000 }], totalShards: 10000 }], + }, + ], + }, + { + key: 'fallback-allocation', + doLog: false, + splits: [ + { + variationKey: 'fallback', + shards: [{ salt: 'salt', ranges: [{ start: 0, end: 10000 }], totalShards: 10000 }], + }, + ], + }, + ], + }, + }, + }, + }, +} describe('evaluate', () => { it('returns default for missing configuration', () => { @@ -18,7 +70,7 @@ describe('evaluate', () => { }) it('returns default for unknown flag', () => { - const result = evaluate(configuration, 'string', 'unknown-flag', 'default', {}) + const result = evaluate(configuration, 'string', 'unknown-flag', 'default', matchingContext) expect(result).toEqual({ value: 'default', reason: 'ERROR', @@ -27,7 +79,7 @@ describe('evaluate', () => { }) it('returns default without variant metadata for type mismatch', () => { - const result = evaluate(configuration, 'string', 'boolean-flag', 'default', {}) + const result = evaluate(configuration, 'string', 'boolean-flag', 'default', matchingContext) expect(result).toEqual({ value: 'default', reason: 'ERROR', @@ -38,7 +90,7 @@ describe('evaluate', () => { }) it('resolves boolean flag', () => { - const result = evaluate(configuration, 'boolean', 'boolean-flag', true, {}) + const result = evaluate(configuration, 'boolean', 'boolean-flag', true, matchingContext) expect(result).toEqual({ value: true, variant: 'variation-124', @@ -46,13 +98,14 @@ describe('evaluate', () => { flagMetadata: { allocationKey: 'allocation-124', doLog: true, + extraLogging: { experiment: true }, variationType: 'BOOLEAN', }, }) }) it('resolves string flag', () => { - const result = evaluate(configuration, 'string', 'string-flag', 'default', {}) + const result = evaluate(configuration, 'string', 'string-flag', 'default', matchingContext) expect(result).toEqual({ value: 'red', variant: 'variation-123', @@ -60,13 +113,14 @@ describe('evaluate', () => { flagMetadata: { allocationKey: 'allocation-123', doLog: true, + extraLogging: { experiment: true }, variationType: 'STRING', }, }) }) it('resolves object flag', () => { - const result = evaluate<'object'>(configuration, 'object', 'json-flag', { hello: 'world' }, {}) + const result = evaluate<'object'>(configuration, 'object', 'json-flag', { hello: 'world' }, matchingContext) expect(result).toEqual({ value: { key: 'value', prop: 123 }, variant: 'variation-127', @@ -74,8 +128,56 @@ describe('evaluate', () => { flagMetadata: { allocationKey: 'allocation-127', doLog: true, + extraLogging: { experiment: true }, variationType: 'OBJECT', }, }) }) + + it('does not use precomputed configuration when context does not match', () => { + const result = evaluate(configuration, 'string', 'string-flag', 'default', { targetingKey: 'other-user' }) + + expect(result).toEqual({ + value: 'default', + reason: 'ERROR', + errorCode: 'INVALID_CONTEXT' as ErrorCode, + }) + }) + + it('evaluates rules-based configuration when present', () => { + const result = evaluate(rulesBasedConfiguration, 'string', 'dynamic-flag', 'default', { + targetingKey: 'user-1', + plan: 'enterprise', + }) + + expect(result).toMatchObject({ + value: 'enabled', + variant: 'enterprise', + reason: 'TARGETING_MATCH', + flagMetadata: { + allocationKey: 'enterprise-allocation', + doLog: true, + extraLogging: { experiment: 'dynamic-context' }, + }, + }) + }) + + it('falls back to rules-based configuration when precomputed context does not match', () => { + const result = evaluate( + { + ...rulesBasedConfiguration, + precomputed: configuration.precomputed, + }, + 'string', + 'dynamic-flag', + 'default', + { targetingKey: 'other-user', plan: 'enterprise' } + ) + + expect(result).toMatchObject({ + value: 'enabled', + variant: 'enterprise', + reason: 'TARGETING_MATCH', + }) + }) }) diff --git a/packages/browser/test/openfeature/core-provider.spec.ts b/packages/browser/test/openfeature/core-provider.spec.ts new file mode 100644 index 00000000..351e02fd --- /dev/null +++ b/packages/browser/test/openfeature/core-provider.spec.ts @@ -0,0 +1,231 @@ +import { type FlagsConfiguration, OperatorType } from '@datadog/flagging-core' +import type { Logger } from '@openfeature/core' +import { ProviderEvents, ProviderStatus } from '@openfeature/web-sdk' +import { CoreProvider } from '../../src/openfeature/core-provider' + +const logger: Logger = { + debug: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), +} + +const rulesBasedConfiguration: FlagsConfiguration = { + rulesBased: { + response: { + createdAt: '2026-07-06T23:01:56.822Z', + format: 'SERVER', + environment: { + name: 'prod', + }, + flags: { + 'dynamic-flag': { + key: 'dynamic-flag', + enabled: true, + variationType: 'STRING', + variations: { + enterprise: { key: 'enterprise', value: 'enabled' }, + fallback: { key: 'fallback', value: 'disabled' }, + }, + allocations: [ + { + key: 'enterprise-allocation', + rules: [ + { + conditions: [{ operator: OperatorType.ONE_OF, attribute: 'plan', value: ['enterprise'] }], + }, + ], + doLog: true, + splits: [ + { + variationKey: 'enterprise', + extraLogging: { experiment: 'dynamic-context' }, + shards: [{ salt: 'salt', ranges: [{ start: 0, end: 10000 }], totalShards: 10000 }], + }, + ], + }, + { + key: 'fallback-allocation', + doLog: false, + splits: [ + { + variationKey: 'fallback', + shards: [{ salt: 'salt', ranges: [{ start: 0, end: 10000 }], totalShards: 10000 }], + }, + ], + }, + ], + }, + }, + }, + etag: 'rules-etag', + }, +} + +const precomputedConfiguration: FlagsConfiguration = { + precomputed: { + context: { targetingKey: 'static-user', plan: 'free' }, + response: { + data: { + attributes: { + createdAt: '2026-07-06T23:01:56.822Z', + flags: { + 'static-flag': { + allocationKey: 'static-allocation', + variationKey: 'static-variation', + variationType: 'string', + variationValue: 'static-value', + reason: 'TARGETING_MATCH', + doLog: true, + extraLogging: { experiment: 'static-context' }, + }, + }, + }, + }, + }, + }, +} + +describe('CoreProvider', () => { + it('has browser provider metadata', () => { + const provider = new CoreProvider({ configuration: rulesBasedConfiguration }) + + expect(provider.metadata).toEqual({ name: 'datadog-core' }) + expect(provider.runsOn).toBe('client') + expect(provider.status).toBe(ProviderStatus.READY) + }) + + it('stores context changes and evaluates rules locally without changing status', async () => { + const provider = new CoreProvider({ configuration: rulesBasedConfiguration }) + + await provider.initialize({ targetingKey: 'user-1', plan: 'free' }) + + expect(provider.resolveStringEvaluation('dynamic-flag', 'default', {}, logger)).toMatchObject({ + value: 'disabled', + variant: 'fallback', + reason: 'TARGETING_MATCH', + }) + + await provider.onContextChange( + { targetingKey: 'user-1', plan: 'free' }, + { targetingKey: 'user-1', plan: 'enterprise' } + ) + + expect(provider.status).toBe(ProviderStatus.READY) + expect(provider.resolveStringEvaluation('dynamic-flag', 'default', {}, logger)).toMatchObject({ + value: 'enabled', + variant: 'enterprise', + reason: 'TARGETING_MATCH', + flagMetadata: { + allocationKey: 'enterprise-allocation', + doLog: true, + extraLogging: { experiment: 'dynamic-context' }, + }, + }) + }) + + it('uses precomputed configuration only when the context matches', async () => { + const provider = new CoreProvider({ configuration: precomputedConfiguration }) + + await provider.initialize({ targetingKey: 'static-user', plan: 'free' }) + + expect( + provider.resolveStringEvaluation('static-flag', 'default', { targetingKey: 'static-user', plan: 'free' }, logger) + ).toMatchObject({ + value: 'static-value', + variant: 'static-variation', + reason: 'TARGETING_MATCH', + }) + }) + + it('rejects precomputed-only context changes that do not match the configuration', async () => { + const provider = new CoreProvider({ configuration: precomputedConfiguration }) + const errorHandler = jest.fn() + provider.events.addHandler(ProviderEvents.Error, errorHandler) + + await provider.initialize({ targetingKey: 'static-user', plan: 'free' }) + + await expect( + provider.onContextChange( + { targetingKey: 'static-user', plan: 'free' }, + { targetingKey: 'other-user', plan: 'free' } + ) + ).rejects.toThrow('Precomputed flags configuration does not match the current context') + + expect(provider.status).toBe(ProviderStatus.ERROR) + expect(errorHandler).toHaveBeenCalledTimes(1) + expect( + provider.resolveStringEvaluation('static-flag', 'default', { targetingKey: 'other-user', plan: 'free' }, logger) + ).toEqual({ + value: 'default', + reason: 'ERROR', + errorCode: 'INVALID_CONTEXT', + }) + }) + + it('uses rules-based configuration when precomputed context does not match', async () => { + const provider = new CoreProvider({ + configuration: { + ...rulesBasedConfiguration, + precomputed: precomputedConfiguration.precomputed, + }, + }) + + await provider.initialize({ targetingKey: 'other-user', plan: 'enterprise' }) + + expect(provider.status).toBe(ProviderStatus.READY) + expect(provider.resolveStringEvaluation('dynamic-flag', 'default', {}, logger)).toMatchObject({ + value: 'enabled', + variant: 'enterprise', + reason: 'TARGETING_MATCH', + }) + }) + + it('emits Ready when setConfiguration recovers from an invalid configuration', () => { + const provider = new CoreProvider({ configuration: {} }) + const readyHandler = jest.fn() + provider.events.addHandler(ProviderEvents.Ready, readyHandler) + + provider.setConfiguration(rulesBasedConfiguration) + + expect(provider.status).toBe(ProviderStatus.READY) + expect(readyHandler).toHaveBeenCalledTimes(1) + }) + + it('emits ConfigurationChanged for replacement configuration', () => { + const provider = new CoreProvider({ configuration: rulesBasedConfiguration }) + const changedHandler = jest.fn() + provider.events.addHandler(ProviderEvents.ConfigurationChanged, changedHandler) + + provider.setConfiguration({ + rulesBased: { + ...rulesBasedConfiguration.rulesBased!, + etag: 'new-rules-etag', + }, + }) + + expect(provider.status).toBe(ProviderStatus.READY) + expect(changedHandler).toHaveBeenCalledTimes(1) + }) + + it('emits Error when setConfiguration receives an invalid configuration', () => { + const provider = new CoreProvider({ configuration: rulesBasedConfiguration }) + const errorHandler = jest.fn() + provider.events.addHandler(ProviderEvents.Error, errorHandler) + + provider.setConfiguration({}) + + expect(provider.status).toBe(ProviderStatus.ERROR) + expect(errorHandler).toHaveBeenCalledTimes(1) + }) + + it('returns provider not ready when no evaluatable configuration is available', () => { + const provider = new CoreProvider({ configuration: {} }) + + expect(provider.resolveBooleanEvaluation('missing-flag', true, {}, logger)).toEqual({ + value: true, + reason: 'ERROR', + errorCode: 'PROVIDER_NOT_READY', + }) + }) +}) diff --git a/packages/browser/test/openfeature/provider.spec.ts b/packages/browser/test/openfeature/provider.spec.ts index 2c881dcb..5df4a34b 100644 --- a/packages/browser/test/openfeature/provider.spec.ts +++ b/packages/browser/test/openfeature/provider.spec.ts @@ -298,6 +298,7 @@ describe('DatadogProvider', () => { flagMetadata: { allocationKey: 'allocation-123', doLog: true, + extraLogging: { experiment: true }, variationType: 'STRING', }, }) diff --git a/packages/core/src/configuration/configuration.ts b/packages/core/src/configuration/configuration.ts index 47fb6a81..a9d03133 100644 --- a/packages/core/src/configuration/configuration.ts +++ b/packages/core/src/configuration/configuration.ts @@ -1,5 +1,6 @@ import type { TimeStamp } from '@datadog/js-core/time' import type { EvaluationContext, FlagValueType, JsonValue, ResolutionReason } from '@openfeature/core' +import type { UniversalFlagConfigurationV1 } from '../evaluation' /** * Internal flags configuration for DatadogProvider. @@ -7,6 +8,8 @@ import type { EvaluationContext, FlagValueType, JsonValue, ResolutionReason } fr export type FlagsConfiguration = { /** @internal */ precomputed?: PrecomputedConfiguration + /** @internal */ + rulesBased?: RulesBasedConfiguration } /** @internal */ @@ -14,6 +17,14 @@ export type PrecomputedConfiguration = { response: PrecomputedConfigurationResponse context?: EvaluationContext fetchedAt?: TimeStamp + etag?: string +} + +/** @internal */ +export type RulesBasedConfiguration = { + response: UniversalFlagConfigurationV1 + fetchedAt?: TimeStamp + etag?: string } // Fancy way to map FlagValueType to expected FlagValue. diff --git a/packages/core/src/configuration/wire.test.ts b/packages/core/src/configuration/wire.test.ts index 6e92e062..57b577de 100644 --- a/packages/core/src/configuration/wire.test.ts +++ b/packages/core/src/configuration/wire.test.ts @@ -1,3 +1,4 @@ +import type { TimeStamp } from '@datadog/js-core/time' import type { FlagsConfiguration } from './configuration' import { configurationFromString, configurationToString } from './wire' @@ -25,6 +26,51 @@ const configuration: FlagsConfiguration = { }, } +const rulesBasedConfiguration: FlagsConfiguration = { + rulesBased: { + response: { + createdAt: '2026-07-06T23:01:56.822Z', + format: 'SERVER', + environment: { + name: 'prod', + }, + flags: { + 'my-flag': { + key: 'my-flag', + enabled: true, + variationType: 'BOOLEAN', + variations: { + true: { + key: 'true', + value: true, + }, + }, + allocations: [ + { + key: 'alloc-1', + splits: [ + { + variationKey: 'true', + shards: [ + { + salt: 'salt', + ranges: [{ start: 0, end: 10000 }], + totalShards: 10000, + }, + ], + }, + ], + doLog: true, + }, + ], + }, + }, + }, + fetchedAt: 1731939819456 as TimeStamp, + etag: 'rules-etag', + }, +} + describe('configuration wire', () => { it('round-trips a precomputed configuration', () => { const restored = configurationFromString(configurationToString(configuration)) @@ -32,12 +78,24 @@ describe('configuration wire', () => { expect(restored).toEqual(configuration) }) + it('round-trips a rules-based configuration', () => { + const restored = configurationFromString(configurationToString(rulesBasedConfiguration)) + + expect(restored).toEqual(rulesBasedConfiguration) + }) + it('keeps flags readable after a round-trip', () => { const restored = configurationFromString(configurationToString(configuration)) expect(restored.precomputed?.response.data.attributes.flags['my-flag'].variationValue).toBe(true) }) + it('keeps rules-based flags readable after a round-trip', () => { + const restored = configurationFromString(configurationToString(rulesBasedConfiguration)) + + expect(restored.rulesBased?.response.flags['my-flag'].variations.true.value).toBe(true) + }) + it('serializes precomputed.response as a stringified response object, not the whole precomputed', () => { const wire = JSON.parse(configurationToString(configuration)) @@ -47,6 +105,12 @@ describe('configuration wire', () => { expect(JSON.parse(wire.precomputed.response)).toEqual(configuration.precomputed?.response) }) + it('serializes rulesBased.response as a stringified response object, not the whole rulesBased config', () => { + const wire = JSON.parse(configurationToString(rulesBasedConfiguration)) + + expect(JSON.parse(wire.rulesBased.response)).toEqual(rulesBasedConfiguration.rulesBased?.response) + }) + it('returns an empty configuration for an unknown version', () => { expect(configurationFromString(JSON.stringify({ version: 2 }))).toEqual({}) }) diff --git a/packages/core/src/configuration/wire.ts b/packages/core/src/configuration/wire.ts index 615f8922..ff8b3d66 100644 --- a/packages/core/src/configuration/wire.ts +++ b/packages/core/src/configuration/wire.ts @@ -8,6 +8,12 @@ type ConfigurationWire = { context?: EvaluationContext response: string fetchedAt?: TimeStamp + etag?: string + } + rulesBased?: { + response: string + fetchedAt?: TimeStamp + etag?: string } } @@ -30,6 +36,12 @@ export function configurationFromString(s: string): FlagsConfiguration { response: JSON.parse(wire.precomputed.response), } } + if (wire.rulesBased) { + configuration.rulesBased = { + ...wire.rulesBased, + response: JSON.parse(wire.rulesBased.response), + } + } return configuration } catch { @@ -53,6 +65,12 @@ export function configurationToString(configuration: FlagsConfiguration): string response: JSON.stringify(configuration.precomputed.response), } } + if (configuration.rulesBased) { + wire.rulesBased = { + ...configuration.rulesBased, + response: JSON.stringify(configuration.rulesBased.response), + } + } return JSON.stringify(wire) } diff --git a/packages/core/src/evaluation/evaluateForSubject.ts b/packages/core/src/evaluation/evaluateForSubject.ts index 9ab7fbb8..74bfd29a 100644 --- a/packages/core/src/evaluation/evaluateForSubject.ts +++ b/packages/core/src/evaluation/evaluateForSubject.ts @@ -96,6 +96,7 @@ export function evaluateForSubject( allocationKey: allocation.key, variationType: variantTypeToFlagValueType(flag.variationType), doLog: !!allocation.doLog, + extraLogging: selectedSplit.extraLogging, } as PrecomputedFlagMetadata, } } diff --git a/packages/core/test/evaluation/evaluateForSubject.spec.ts b/packages/core/test/evaluation/evaluateForSubject.spec.ts index bc3d1484..467f1f44 100644 --- a/packages/core/test/evaluation/evaluateForSubject.spec.ts +++ b/packages/core/test/evaluation/evaluateForSubject.spec.ts @@ -151,6 +151,7 @@ describe('evaluateForSubject', () => { { variationKey: 'treatment', serialId, + extraLogging: { campaign: 'summer' }, shards: [ { salt: 'experiment-salt', @@ -176,6 +177,7 @@ describe('evaluateForSubject', () => { // Also verify deprecated keys are still set for backwards compatibility allocationKey: 'experiment-allocation', doLog: true, + extraLogging: { campaign: 'summer' }, }) }) }) From efb2034faf809fe6a22a1f28845138f3849a3a28 Mon Sep 17 00:00:00 2001 From: Sameeran Kunche Date: Tue, 14 Jul 2026 08:01:45 -0700 Subject: [PATCH 02/13] Remove CoreProvider status field --- packages/browser/src/openfeature/core-provider.ts | 10 ---------- .../browser/test/openfeature/core-provider.spec.ts | 11 ++--------- 2 files changed, 2 insertions(+), 19 deletions(-) diff --git a/packages/browser/src/openfeature/core-provider.ts b/packages/browser/src/openfeature/core-provider.ts index d5ca061e..da7960ee 100644 --- a/packages/browser/src/openfeature/core-provider.ts +++ b/packages/browser/src/openfeature/core-provider.ts @@ -15,7 +15,6 @@ import { OpenFeatureEventEmitter, type ProviderEventEmitter, ProviderEvents, - ProviderStatus, } from '@openfeature/web-sdk' import { evaluate } from '../evaluation' @@ -30,15 +29,12 @@ export class CoreProvider implements Provider { readonly runsOn: Paradigm = 'client' readonly events: ProviderEventEmitter - status: ProviderStatus - private flagsConfiguration: FlagsConfiguration private context: EvaluationContext = {} constructor(options: CoreProviderOptions) { this.events = new OpenFeatureEventEmitter() this.flagsConfiguration = options.configuration - this.status = hasEvaluatableConfiguration(options.configuration) ? ProviderStatus.READY : ProviderStatus.ERROR } getConfiguration(): FlagsConfiguration { @@ -51,12 +47,10 @@ export class CoreProvider implements Provider { const error = getConfigurationError(configuration, this.context) if (error) { - this.status = ProviderStatus.ERROR this.events.emit(ProviderEvents.Error, { error }) return } - this.status = ProviderStatus.READY this.events.emit(hadEvaluatableConfiguration ? ProviderEvents.ConfigurationChanged : ProviderEvents.Ready) } @@ -64,21 +58,17 @@ export class CoreProvider implements Provider { this.context = context const error = getConfigurationError(this.flagsConfiguration, this.context) if (error) { - this.status = ProviderStatus.ERROR throw error } - this.status = ProviderStatus.READY } async onContextChange(_oldContext: EvaluationContext, newContext: EvaluationContext): Promise { this.context = newContext const error = getConfigurationError(this.flagsConfiguration, this.context) if (error) { - this.status = ProviderStatus.ERROR this.events.emit(ProviderEvents.Error, { error }) throw error } - this.status = ProviderStatus.READY } resolveBooleanEvaluation( diff --git a/packages/browser/test/openfeature/core-provider.spec.ts b/packages/browser/test/openfeature/core-provider.spec.ts index 351e02fd..69148fb1 100644 --- a/packages/browser/test/openfeature/core-provider.spec.ts +++ b/packages/browser/test/openfeature/core-provider.spec.ts @@ -1,6 +1,6 @@ import { type FlagsConfiguration, OperatorType } from '@datadog/flagging-core' import type { Logger } from '@openfeature/core' -import { ProviderEvents, ProviderStatus } from '@openfeature/web-sdk' +import { ProviderEvents } from '@openfeature/web-sdk' import { CoreProvider } from '../../src/openfeature/core-provider' const logger: Logger = { @@ -92,10 +92,9 @@ describe('CoreProvider', () => { expect(provider.metadata).toEqual({ name: 'datadog-core' }) expect(provider.runsOn).toBe('client') - expect(provider.status).toBe(ProviderStatus.READY) }) - it('stores context changes and evaluates rules locally without changing status', async () => { + it('stores context changes and evaluates rules locally', async () => { const provider = new CoreProvider({ configuration: rulesBasedConfiguration }) await provider.initialize({ targetingKey: 'user-1', plan: 'free' }) @@ -111,7 +110,6 @@ describe('CoreProvider', () => { { targetingKey: 'user-1', plan: 'enterprise' } ) - expect(provider.status).toBe(ProviderStatus.READY) expect(provider.resolveStringEvaluation('dynamic-flag', 'default', {}, logger)).toMatchObject({ value: 'enabled', variant: 'enterprise', @@ -152,7 +150,6 @@ describe('CoreProvider', () => { ) ).rejects.toThrow('Precomputed flags configuration does not match the current context') - expect(provider.status).toBe(ProviderStatus.ERROR) expect(errorHandler).toHaveBeenCalledTimes(1) expect( provider.resolveStringEvaluation('static-flag', 'default', { targetingKey: 'other-user', plan: 'free' }, logger) @@ -173,7 +170,6 @@ describe('CoreProvider', () => { await provider.initialize({ targetingKey: 'other-user', plan: 'enterprise' }) - expect(provider.status).toBe(ProviderStatus.READY) expect(provider.resolveStringEvaluation('dynamic-flag', 'default', {}, logger)).toMatchObject({ value: 'enabled', variant: 'enterprise', @@ -188,7 +184,6 @@ describe('CoreProvider', () => { provider.setConfiguration(rulesBasedConfiguration) - expect(provider.status).toBe(ProviderStatus.READY) expect(readyHandler).toHaveBeenCalledTimes(1) }) @@ -204,7 +199,6 @@ describe('CoreProvider', () => { }, }) - expect(provider.status).toBe(ProviderStatus.READY) expect(changedHandler).toHaveBeenCalledTimes(1) }) @@ -215,7 +209,6 @@ describe('CoreProvider', () => { provider.setConfiguration({}) - expect(provider.status).toBe(ProviderStatus.ERROR) expect(errorHandler).toHaveBeenCalledTimes(1) }) From 9cf1cccc419bb2e189c335b1dad3303c75108cfa Mon Sep 17 00:00:00 2001 From: Sameeran Kunche Date: Tue, 14 Jul 2026 08:02:40 -0700 Subject: [PATCH 03/13] Make CoreProvider context changes synchronous --- packages/browser/src/openfeature/core-provider.ts | 2 +- packages/browser/test/openfeature/core-provider.spec.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/browser/src/openfeature/core-provider.ts b/packages/browser/src/openfeature/core-provider.ts index da7960ee..1574940a 100644 --- a/packages/browser/src/openfeature/core-provider.ts +++ b/packages/browser/src/openfeature/core-provider.ts @@ -62,7 +62,7 @@ export class CoreProvider implements Provider { } } - async onContextChange(_oldContext: EvaluationContext, newContext: EvaluationContext): Promise { + onContextChange(_oldContext: EvaluationContext, newContext: EvaluationContext): void { this.context = newContext const error = getConfigurationError(this.flagsConfiguration, this.context) if (error) { diff --git a/packages/browser/test/openfeature/core-provider.spec.ts b/packages/browser/test/openfeature/core-provider.spec.ts index 69148fb1..7c32785b 100644 --- a/packages/browser/test/openfeature/core-provider.spec.ts +++ b/packages/browser/test/openfeature/core-provider.spec.ts @@ -136,19 +136,19 @@ describe('CoreProvider', () => { }) }) - it('rejects precomputed-only context changes that do not match the configuration', async () => { + it('throws for precomputed-only context changes that do not match the configuration', async () => { const provider = new CoreProvider({ configuration: precomputedConfiguration }) const errorHandler = jest.fn() provider.events.addHandler(ProviderEvents.Error, errorHandler) await provider.initialize({ targetingKey: 'static-user', plan: 'free' }) - await expect( + expect(() => provider.onContextChange( { targetingKey: 'static-user', plan: 'free' }, { targetingKey: 'other-user', plan: 'free' } ) - ).rejects.toThrow('Precomputed flags configuration does not match the current context') + ).toThrow('Precomputed flags configuration does not match the current context') expect(errorHandler).toHaveBeenCalledTimes(1) expect( From f151909fdbb7f81e11815520066e5392c7a98878 Mon Sep 17 00:00:00 2001 From: Sameeran Kunche Date: Tue, 14 Jul 2026 08:03:18 -0700 Subject: [PATCH 04/13] Let OpenFeature handle context change errors --- packages/browser/src/openfeature/core-provider.ts | 1 - packages/browser/test/openfeature/core-provider.spec.ts | 3 --- 2 files changed, 4 deletions(-) diff --git a/packages/browser/src/openfeature/core-provider.ts b/packages/browser/src/openfeature/core-provider.ts index 1574940a..f97dfa34 100644 --- a/packages/browser/src/openfeature/core-provider.ts +++ b/packages/browser/src/openfeature/core-provider.ts @@ -66,7 +66,6 @@ export class CoreProvider implements Provider { this.context = newContext const error = getConfigurationError(this.flagsConfiguration, this.context) if (error) { - this.events.emit(ProviderEvents.Error, { error }) throw error } } diff --git a/packages/browser/test/openfeature/core-provider.spec.ts b/packages/browser/test/openfeature/core-provider.spec.ts index 7c32785b..8ea36e75 100644 --- a/packages/browser/test/openfeature/core-provider.spec.ts +++ b/packages/browser/test/openfeature/core-provider.spec.ts @@ -138,8 +138,6 @@ describe('CoreProvider', () => { it('throws for precomputed-only context changes that do not match the configuration', async () => { const provider = new CoreProvider({ configuration: precomputedConfiguration }) - const errorHandler = jest.fn() - provider.events.addHandler(ProviderEvents.Error, errorHandler) await provider.initialize({ targetingKey: 'static-user', plan: 'free' }) @@ -150,7 +148,6 @@ describe('CoreProvider', () => { ) ).toThrow('Precomputed flags configuration does not match the current context') - expect(errorHandler).toHaveBeenCalledTimes(1) expect( provider.resolveStringEvaluation('static-flag', 'default', { targetingKey: 'other-user', plan: 'free' }, logger) ).toEqual({ From f011597aaf12ba59b42ae565290d46592b5b1e66 Mon Sep 17 00:00:00 2001 From: Sameeran Kunche Date: Tue, 14 Jul 2026 08:04:05 -0700 Subject: [PATCH 05/13] Move missing configuration fallback into evaluate --- packages/browser/src/evaluation.ts | 3 ++- packages/browser/src/openfeature/core-provider.ts | 9 --------- packages/browser/test/evaluation.spec.ts | 3 ++- 3 files changed, 4 insertions(+), 11 deletions(-) diff --git a/packages/browser/src/evaluation.ts b/packages/browser/src/evaluation.ts index 2bdf0025..43592227 100644 --- a/packages/browser/src/evaluation.ts +++ b/packages/browser/src/evaluation.ts @@ -47,7 +47,8 @@ export function evaluate( return { value: defaultValue, - reason: 'DEFAULT', + reason: 'ERROR', + errorCode: 'PROVIDER_NOT_READY' as ErrorCode, } } diff --git a/packages/browser/src/openfeature/core-provider.ts b/packages/browser/src/openfeature/core-provider.ts index f97dfa34..6289ebf3 100644 --- a/packages/browser/src/openfeature/core-provider.ts +++ b/packages/browser/src/openfeature/core-provider.ts @@ -1,7 +1,6 @@ import type { FlagsConfiguration, FlagTypeToValue } from '@datadog/flagging-core' import { configMatchesContext } from '@datadog/flagging-core' import type { - ErrorCode, EvaluationContext, FlagValueType, JsonValue, @@ -113,14 +112,6 @@ export class CoreProvider implements Provider { context: EvaluationContext, logger: Logger ): ResolutionDetails> { - if (!hasEvaluatableConfiguration(this.flagsConfiguration)) { - return { - value: defaultValue, - reason: 'ERROR', - errorCode: 'PROVIDER_NOT_READY' as ErrorCode, - } - } - return evaluate(this.flagsConfiguration, type, flagKey, defaultValue, this.getEvaluationContext(context), logger) } diff --git a/packages/browser/test/evaluation.spec.ts b/packages/browser/test/evaluation.spec.ts index c787afa2..c0407785 100644 --- a/packages/browser/test/evaluation.spec.ts +++ b/packages/browser/test/evaluation.spec.ts @@ -65,7 +65,8 @@ describe('evaluate', () => { const result = evaluate({}, 'boolean', 'boolean-flag', true, {}) expect(result).toEqual({ value: true, - reason: 'DEFAULT', + reason: 'ERROR', + errorCode: 'PROVIDER_NOT_READY' as ErrorCode, }) }) From 21195fb847e662e92572c71a616ab621ba4e7fe9 Mon Sep 17 00:00:00 2001 From: Sameeran Kunche Date: Tue, 14 Jul 2026 08:04:55 -0700 Subject: [PATCH 06/13] Use OpenFeature evaluation context directly --- .../browser/src/openfeature/core-provider.ts | 9 +------ .../test/openfeature/core-provider.spec.ts | 24 +++++++++++++++---- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/packages/browser/src/openfeature/core-provider.ts b/packages/browser/src/openfeature/core-provider.ts index 6289ebf3..9a9a6453 100644 --- a/packages/browser/src/openfeature/core-provider.ts +++ b/packages/browser/src/openfeature/core-provider.ts @@ -112,14 +112,7 @@ export class CoreProvider implements Provider { context: EvaluationContext, logger: Logger ): ResolutionDetails> { - return evaluate(this.flagsConfiguration, type, flagKey, defaultValue, this.getEvaluationContext(context), logger) - } - - private getEvaluationContext(context: EvaluationContext): EvaluationContext { - if (Object.keys(context).length > 0 || Object.keys(this.context).length === 0) { - return context - } - return this.context + return evaluate(this.flagsConfiguration, type, flagKey, defaultValue, context, logger) } private canEvaluateCurrentContext(): boolean { diff --git a/packages/browser/test/openfeature/core-provider.spec.ts b/packages/browser/test/openfeature/core-provider.spec.ts index 8ea36e75..2479fcd2 100644 --- a/packages/browser/test/openfeature/core-provider.spec.ts +++ b/packages/browser/test/openfeature/core-provider.spec.ts @@ -94,12 +94,14 @@ describe('CoreProvider', () => { expect(provider.runsOn).toBe('client') }) - it('stores context changes and evaluates rules locally', async () => { + it('evaluates rules locally with the supplied context', async () => { const provider = new CoreProvider({ configuration: rulesBasedConfiguration }) await provider.initialize({ targetingKey: 'user-1', plan: 'free' }) - expect(provider.resolveStringEvaluation('dynamic-flag', 'default', {}, logger)).toMatchObject({ + expect( + provider.resolveStringEvaluation('dynamic-flag', 'default', { targetingKey: 'user-1', plan: 'free' }, logger) + ).toMatchObject({ value: 'disabled', variant: 'fallback', reason: 'TARGETING_MATCH', @@ -110,7 +112,14 @@ describe('CoreProvider', () => { { targetingKey: 'user-1', plan: 'enterprise' } ) - expect(provider.resolveStringEvaluation('dynamic-flag', 'default', {}, logger)).toMatchObject({ + expect( + provider.resolveStringEvaluation( + 'dynamic-flag', + 'default', + { targetingKey: 'user-1', plan: 'enterprise' }, + logger + ) + ).toMatchObject({ value: 'enabled', variant: 'enterprise', reason: 'TARGETING_MATCH', @@ -167,7 +176,14 @@ describe('CoreProvider', () => { await provider.initialize({ targetingKey: 'other-user', plan: 'enterprise' }) - expect(provider.resolveStringEvaluation('dynamic-flag', 'default', {}, logger)).toMatchObject({ + expect( + provider.resolveStringEvaluation( + 'dynamic-flag', + 'default', + { targetingKey: 'other-user', plan: 'enterprise' }, + logger + ) + ).toMatchObject({ value: 'enabled', variant: 'enterprise', reason: 'TARGETING_MATCH', From 1ad76ed5f1e86c46e68930be20fcc656feda23a7 Mon Sep 17 00:00:00 2001 From: Sameeran Kunche Date: Tue, 14 Jul 2026 08:08:49 -0700 Subject: [PATCH 07/13] Remove extraLogging from flag metadata --- packages/browser/src/evaluation.ts | 1 - packages/browser/test/data/precomputed-v1-wire.json | 2 +- packages/browser/test/data/precomputed-v1.json | 10 +++++----- packages/browser/test/evaluation.spec.ts | 4 ---- .../browser/test/openfeature/core-provider.spec.ts | 1 - packages/browser/test/openfeature/provider.spec.ts | 3 +-- packages/core/src/configuration/configuration.ts | 2 +- packages/core/src/evaluation/evaluateForSubject.ts | 1 - .../core/test/evaluation/evaluateForSubject.spec.ts | 2 +- 9 files changed, 9 insertions(+), 17 deletions(-) diff --git a/packages/browser/src/evaluation.ts b/packages/browser/src/evaluation.ts index 43592227..07bd9f48 100644 --- a/packages/browser/src/evaluation.ts +++ b/packages/browser/src/evaluation.ts @@ -83,7 +83,6 @@ function evaluatePrecomputed( allocationKey: flag.allocationKey, variationType: flag.variationType, doLog: flag.doLog, - extraLogging: flag.extraLogging, } as PrecomputedFlagMetadata, reason: flag.reason, } as ResolutionDetails> diff --git a/packages/browser/test/data/precomputed-v1-wire.json b/packages/browser/test/data/precomputed-v1-wire.json index 39e82a21..a4c10f49 100644 --- a/packages/browser/test/data/precomputed-v1-wire.json +++ b/packages/browser/test/data/precomputed-v1-wire.json @@ -11,6 +11,6 @@ "lifetimeValue": 543.21 }, "fetchedAt": 1731939819456, - "response": "{\"data\":{\"id\":\"test_subject\",\"type\":\"precomputed-assignments\",\"attributes\":{\"createdAt\":1731939805123,\"environment\":{\"name\":\"prod\"},\"flags\":{\"string-flag\":{\"allocationKey\":\"allocation-123\",\"variationKey\":\"variation-123\",\"variationType\":\"STRING\",\"variationValue\":\"red\",\"extraLogging\":{\"experiment\":true},\"doLog\":true,\"reason\":\"TARGETING_MATCH\"},\"boolean-flag\":{\"allocationKey\":\"allocation-124\",\"variationKey\":\"variation-124\",\"variationType\":\"BOOLEAN\",\"variationValue\":true,\"extraLogging\":{\"experiment\":true},\"doLog\":true,\"reason\":\"TARGETING_MATCH\"},\"integer-flag\":{\"allocationKey\":\"allocation-125\",\"variationKey\":\"variation-125\",\"variationType\":\"NUMBER\",\"variationValue\":42,\"extraLogging\":{\"experiment\":true},\"doLog\":true,\"reason\":\"TARGETING_MATCH\"},\"numeric-flag\":{\"allocationKey\":\"allocation-126\",\"variationKey\":\"variation-126\",\"variationType\":\"NUMBER\",\"variationValue\":3.14,\"extraLogging\":{\"experiment\":true},\"doLog\":true,\"reason\":\"TARGETING_MATCH\"},\"json-flag\":{\"allocationKey\":\"allocation-127\",\"variationKey\":\"variation-127\",\"variationType\":\"OBJECT\",\"variationValue\":{\"key\":\"value\",\"prop\":123},\"extraLogging\":{\"experiment\":true},\"doLog\":true,\"reason\":\"TARGETING_MATCH\"}}}}}" + "response": "{\"data\":{\"id\":\"test_subject\",\"type\":\"precomputed-assignments\",\"attributes\":{\"createdAt\":1731939805123,\"environment\":{\"name\":\"prod\"},\"flags\":{\"string-flag\":{\"allocationKey\":\"allocation-123\",\"variationKey\":\"variation-123\",\"variationType\":\"STRING\",\"variationValue\":\"red\",\"extraLogging\":{\"experiment\":\"true\"},\"doLog\":true,\"reason\":\"TARGETING_MATCH\"},\"boolean-flag\":{\"allocationKey\":\"allocation-124\",\"variationKey\":\"variation-124\",\"variationType\":\"BOOLEAN\",\"variationValue\":true,\"extraLogging\":{\"experiment\":\"true\"},\"doLog\":true,\"reason\":\"TARGETING_MATCH\"},\"integer-flag\":{\"allocationKey\":\"allocation-125\",\"variationKey\":\"variation-125\",\"variationType\":\"NUMBER\",\"variationValue\":42,\"extraLogging\":{\"experiment\":\"true\"},\"doLog\":true,\"reason\":\"TARGETING_MATCH\"},\"numeric-flag\":{\"allocationKey\":\"allocation-126\",\"variationKey\":\"variation-126\",\"variationType\":\"NUMBER\",\"variationValue\":3.14,\"extraLogging\":{\"experiment\":\"true\"},\"doLog\":true,\"reason\":\"TARGETING_MATCH\"},\"json-flag\":{\"allocationKey\":\"allocation-127\",\"variationKey\":\"variation-127\",\"variationType\":\"OBJECT\",\"variationValue\":{\"key\":\"value\",\"prop\":123},\"extraLogging\":{\"experiment\":\"true\"},\"doLog\":true,\"reason\":\"TARGETING_MATCH\"}}}}}" } } diff --git a/packages/browser/test/data/precomputed-v1.json b/packages/browser/test/data/precomputed-v1.json index 8d09af81..0969928f 100644 --- a/packages/browser/test/data/precomputed-v1.json +++ b/packages/browser/test/data/precomputed-v1.json @@ -14,7 +14,7 @@ "variationType": "STRING", "variationValue": "red", "extraLogging": { - "experiment": true + "experiment": "true" }, "doLog": true, "reason": "TARGETING_MATCH" @@ -25,7 +25,7 @@ "variationType": "BOOLEAN", "variationValue": true, "extraLogging": { - "experiment": true + "experiment": "true" }, "doLog": true, "reason": "TARGETING_MATCH" @@ -36,7 +36,7 @@ "variationType": "NUMBER", "variationValue": 42, "extraLogging": { - "experiment": true + "experiment": "true" }, "doLog": true, "reason": "TARGETING_MATCH" @@ -47,7 +47,7 @@ "variationType": "NUMBER", "variationValue": 3.14, "extraLogging": { - "experiment": true + "experiment": "true" }, "doLog": true, "reason": "TARGETING_MATCH" @@ -58,7 +58,7 @@ "variationType": "OBJECT", "variationValue": { "key": "value", "prop": 123 }, "extraLogging": { - "experiment": true + "experiment": "true" }, "doLog": true, "reason": "TARGETING_MATCH" diff --git a/packages/browser/test/evaluation.spec.ts b/packages/browser/test/evaluation.spec.ts index c0407785..6b07c2a3 100644 --- a/packages/browser/test/evaluation.spec.ts +++ b/packages/browser/test/evaluation.spec.ts @@ -99,7 +99,6 @@ describe('evaluate', () => { flagMetadata: { allocationKey: 'allocation-124', doLog: true, - extraLogging: { experiment: true }, variationType: 'BOOLEAN', }, }) @@ -114,7 +113,6 @@ describe('evaluate', () => { flagMetadata: { allocationKey: 'allocation-123', doLog: true, - extraLogging: { experiment: true }, variationType: 'STRING', }, }) @@ -129,7 +127,6 @@ describe('evaluate', () => { flagMetadata: { allocationKey: 'allocation-127', doLog: true, - extraLogging: { experiment: true }, variationType: 'OBJECT', }, }) @@ -158,7 +155,6 @@ describe('evaluate', () => { flagMetadata: { allocationKey: 'enterprise-allocation', doLog: true, - extraLogging: { experiment: 'dynamic-context' }, }, }) }) diff --git a/packages/browser/test/openfeature/core-provider.spec.ts b/packages/browser/test/openfeature/core-provider.spec.ts index 2479fcd2..a22a1b4c 100644 --- a/packages/browser/test/openfeature/core-provider.spec.ts +++ b/packages/browser/test/openfeature/core-provider.spec.ts @@ -126,7 +126,6 @@ describe('CoreProvider', () => { flagMetadata: { allocationKey: 'enterprise-allocation', doLog: true, - extraLogging: { experiment: 'dynamic-context' }, }, }) }) diff --git a/packages/browser/test/openfeature/provider.spec.ts b/packages/browser/test/openfeature/provider.spec.ts index 5df4a34b..e6746cc8 100644 --- a/packages/browser/test/openfeature/provider.spec.ts +++ b/packages/browser/test/openfeature/provider.spec.ts @@ -298,7 +298,6 @@ describe('DatadogProvider', () => { flagMetadata: { allocationKey: 'allocation-123', doLog: true, - extraLogging: { experiment: true }, variationType: 'STRING', }, }) @@ -329,7 +328,7 @@ describe('DatadogProvider', () => { variationKey: 'variation-123', variationType: 'STRING', variationValue: stringFlagValue, - extraLogging: { experiment: true }, + extraLogging: { experiment: 'true' }, doLog: true, reason: 'TARGETING_MATCH', }, diff --git a/packages/core/src/configuration/configuration.ts b/packages/core/src/configuration/configuration.ts index a9d03133..68a3a1e1 100644 --- a/packages/core/src/configuration/configuration.ts +++ b/packages/core/src/configuration/configuration.ts @@ -55,7 +55,7 @@ export type PrecomputedFlag = { variationValue: FlagTypeToValue reason: ResolutionReason doLog: boolean - extraLogging: Record + extraLogging: Record } /** @internal */ diff --git a/packages/core/src/evaluation/evaluateForSubject.ts b/packages/core/src/evaluation/evaluateForSubject.ts index 74bfd29a..9ab7fbb8 100644 --- a/packages/core/src/evaluation/evaluateForSubject.ts +++ b/packages/core/src/evaluation/evaluateForSubject.ts @@ -96,7 +96,6 @@ export function evaluateForSubject( allocationKey: allocation.key, variationType: variantTypeToFlagValueType(flag.variationType), doLog: !!allocation.doLog, - extraLogging: selectedSplit.extraLogging, } as PrecomputedFlagMetadata, } } diff --git a/packages/core/test/evaluation/evaluateForSubject.spec.ts b/packages/core/test/evaluation/evaluateForSubject.spec.ts index 467f1f44..872671f3 100644 --- a/packages/core/test/evaluation/evaluateForSubject.spec.ts +++ b/packages/core/test/evaluation/evaluateForSubject.spec.ts @@ -177,8 +177,8 @@ describe('evaluateForSubject', () => { // Also verify deprecated keys are still set for backwards compatibility allocationKey: 'experiment-allocation', doLog: true, - extraLogging: { campaign: 'summer' }, }) + expect(result.flagMetadata).not.toHaveProperty('extraLogging') }) }) From 34b3e808975fb3b8de471d88be43b4257bfe3ae0 Mon Sep 17 00:00:00 2001 From: Sameeran Kunche Date: Tue, 14 Jul 2026 08:09:23 -0700 Subject: [PATCH 08/13] Clarify portable configuration string example --- packages/browser/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/browser/README.md b/packages/browser/README.md index 1ac42149..187acf33 100644 --- a/packages/browser/README.md +++ b/packages/browser/README.md @@ -100,7 +100,7 @@ For dynamic context, the generic configuration wire should contain rules-based f import { configurationFromString, CoreProvider } from '@datadog/openfeature-browser' import { OpenFeature } from '@openfeature/web-sdk' -const configuration = configurationFromString(window.__DD_FLAGS_CONFIGURATION__) +const configuration = configurationFromString('...flags configuration string...') const provider = new CoreProvider({ configuration }) await OpenFeature.setProviderAndWait(provider) From 9371b90d2ab9a246292422639eb0d5be7e370317 Mon Sep 17 00:00:00 2001 From: Sameeran Kunche Date: Tue, 14 Jul 2026 08:09:50 -0700 Subject: [PATCH 09/13] Document browser evaluations as synchronous --- packages/browser/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/browser/README.md b/packages/browser/README.md index 187acf33..ab46f53e 100644 --- a/packages/browser/README.md +++ b/packages/browser/README.md @@ -108,7 +108,7 @@ await OpenFeature.setProviderAndWait(provider) await OpenFeature.setContext({ targetingKey: 'user-123', plan: 'enterprise' }) const client = OpenFeature.getClient() -const enabled = await client.getBooleanValue('new-checkout', false) +const enabled = client.getBooleanValue('new-checkout', false) ``` ## End-user license agreement From 86081daa5d47f433bbbb990e3352cfc86a38858e Mon Sep 17 00:00:00 2001 From: Sameeran Kunche Date: Tue, 14 Jul 2026 08:11:30 -0700 Subject: [PATCH 10/13] Move browser evaluation helper to core --- packages/browser/src/evaluation.ts | 108 ------------------ .../browser/src/openfeature/core-provider.ts | 3 +- packages/browser/src/openfeature/provider.ts | 3 +- packages/browser/test/evaluation.spec.ts | 3 +- packages/core/src/evaluation/evaluation.ts | 107 ++++++++++++++++- 5 files changed, 109 insertions(+), 115 deletions(-) delete mode 100644 packages/browser/src/evaluation.ts diff --git a/packages/browser/src/evaluation.ts b/packages/browser/src/evaluation.ts deleted file mode 100644 index 07bd9f48..00000000 --- a/packages/browser/src/evaluation.ts +++ /dev/null @@ -1,108 +0,0 @@ -import type { - FlagsConfiguration, - FlagTypeToValue, - PrecomputedConfiguration, - PrecomputedFlagMetadata, -} from '@datadog/flagging-core' -import { configMatchesContext, evaluateRulesBasedConfiguration } from '@datadog/flagging-core' -import type { ErrorCode, EvaluationContext, FlagValueType, Logger, ResolutionDetails } from '@openfeature/web-sdk' - -const NOOP_LOGGER: Logger = { - debug: () => {}, - info: () => {}, - warn: () => {}, - error: () => {}, -} - -export function evaluate( - flagsConfiguration: FlagsConfiguration, - type: T, - flagKey: string, - defaultValue: FlagTypeToValue, - context: EvaluationContext, - logger: Logger = NOOP_LOGGER -): ResolutionDetails> { - if (flagsConfiguration.precomputed && configMatchesContext(flagsConfiguration, context)) { - return evaluatePrecomputed(flagsConfiguration.precomputed, type, flagKey, defaultValue, context) - } - - if (flagsConfiguration.rulesBased) { - return evaluateRulesBasedConfiguration( - flagsConfiguration.rulesBased.response, - type, - flagKey, - defaultValue, - context, - logger - ) - } - - if (flagsConfiguration.precomputed) { - return { - value: defaultValue, - reason: 'ERROR', - errorCode: 'INVALID_CONTEXT' as ErrorCode, - } - } - - return { - value: defaultValue, - reason: 'ERROR', - errorCode: 'PROVIDER_NOT_READY' as ErrorCode, - } -} - -function evaluatePrecomputed( - precomputed: PrecomputedConfiguration, - type: T, - flagKey: string, - defaultValue: FlagTypeToValue, - _context: EvaluationContext -): ResolutionDetails> { - const flag = precomputed.response.data.attributes.flags[flagKey] - if (!flag) { - return { - value: defaultValue, - reason: 'ERROR', - errorCode: 'FLAG_NOT_FOUND' as ErrorCode, - } - } - - if (flag.variationType && variationTypeToOpenFeature(flag.variationType) !== type) { - return { - value: defaultValue, - reason: 'ERROR', - errorCode: 'TYPE_MISMATCH' as ErrorCode, - } - } - - return { - value: flag.variationValue as FlagTypeToValue, - variant: flag.variationKey, - flagMetadata: { - allocationKey: flag.allocationKey, - variationType: flag.variationType, - doLog: flag.doLog, - } as PrecomputedFlagMetadata, - reason: flag.reason, - } as ResolutionDetails> -} - -function variationTypeToOpenFeature(s: string): FlagValueType { - const typeMap: Record = { - string: 'string', - boolean: 'boolean', - number: 'number', - integer: 'number', - float: 'number', - object: 'object', - - BOOLEAN: 'boolean', - STRING: 'string', - NUMERIC: 'number', - INTEGER: 'number', - JSON: 'object', - } - - return typeMap[s] || s.toLowerCase() -} diff --git a/packages/browser/src/openfeature/core-provider.ts b/packages/browser/src/openfeature/core-provider.ts index 9a9a6453..c14c6721 100644 --- a/packages/browser/src/openfeature/core-provider.ts +++ b/packages/browser/src/openfeature/core-provider.ts @@ -1,5 +1,5 @@ import type { FlagsConfiguration, FlagTypeToValue } from '@datadog/flagging-core' -import { configMatchesContext } from '@datadog/flagging-core' +import { configMatchesContext, evaluate } from '@datadog/flagging-core' import type { EvaluationContext, FlagValueType, @@ -15,7 +15,6 @@ import { type ProviderEventEmitter, ProviderEvents, } from '@openfeature/web-sdk' -import { evaluate } from '../evaluation' export interface CoreProviderOptions { configuration: FlagsConfiguration diff --git a/packages/browser/src/openfeature/provider.ts b/packages/browser/src/openfeature/provider.ts index 0dacf5e5..af0c9c8e 100644 --- a/packages/browser/src/openfeature/provider.ts +++ b/packages/browser/src/openfeature/provider.ts @@ -1,4 +1,4 @@ -import { type AssignmentCache, configMatchesContext, type FlagsConfiguration } from '@datadog/flagging-core' +import { type AssignmentCache, configMatchesContext, evaluate, type FlagsConfiguration } from '@datadog/flagging-core' import type { EvaluationContext, Hook, @@ -23,7 +23,6 @@ import { type FlaggingInitConfiguration, validateAndBuildFlaggingConfiguration, } from '../domain/configuration' -import { evaluate } from '../evaluation' import { createExposureLoggingHook } from './exposures' import { createFlagEvalEVPHook } from './flagEvaluations' import { createRumTrackingHook } from './rumIntegration' diff --git a/packages/browser/test/evaluation.spec.ts b/packages/browser/test/evaluation.spec.ts index 6b07c2a3..3fd895d0 100644 --- a/packages/browser/test/evaluation.spec.ts +++ b/packages/browser/test/evaluation.spec.ts @@ -1,6 +1,5 @@ -import { configurationFromString, type FlagsConfiguration, OperatorType } from '@datadog/flagging-core' +import { configurationFromString, evaluate, type FlagsConfiguration, OperatorType } from '@datadog/flagging-core' import type { ErrorCode } from '@openfeature/web-sdk' -import { evaluate } from '../src/evaluation' import configurationWire from './data/precomputed-v1-wire.json' const configuration = configurationFromString( diff --git a/packages/core/src/evaluation/evaluation.ts b/packages/core/src/evaluation/evaluation.ts index ff8e8be9..49e45c21 100644 --- a/packages/core/src/evaluation/evaluation.ts +++ b/packages/core/src/evaluation/evaluation.ts @@ -1,11 +1,62 @@ import { timeStampNow } from '@datadog/js-core/time' import type { ErrorCode, EvaluationContext, FlagValueType, Logger, ResolutionDetails } from '@openfeature/core' -import type { FlagTypeToValue } from '../configuration' +import { + configMatchesContext, + type FlagsConfiguration, + type FlagTypeToValue, + type PrecomputedConfiguration, + type PrecomputedFlagMetadata, +} from '../configuration' import { TargetingKeyMissingError } from './errors' import { evaluateForSubject } from './evaluateForSubject' import { createEvaluationTimestampMetadata } from './evaluationMetadata' import type { UniversalFlagConfigurationV1 } from './ufc-v1' +const NOOP_LOGGER: Logger = { + debug: () => {}, + info: () => {}, + warn: () => {}, + error: () => {}, +} + +export function evaluate( + flagsConfiguration: FlagsConfiguration, + type: T, + flagKey: string, + defaultValue: FlagTypeToValue, + context: EvaluationContext, + logger: Logger = NOOP_LOGGER +): ResolutionDetails> { + if (flagsConfiguration.precomputed && configMatchesContext(flagsConfiguration, context)) { + return evaluatePrecomputed(flagsConfiguration.precomputed, type, flagKey, defaultValue) + } + + if (flagsConfiguration.rulesBased) { + return evaluateRulesBasedConfiguration( + flagsConfiguration.rulesBased.response, + type, + flagKey, + defaultValue, + context, + logger + ) + } + + if (flagsConfiguration.precomputed) { + return { + value: defaultValue, + reason: 'ERROR', + errorCode: 'INVALID_CONTEXT' as ErrorCode, + } + } + + return { + value: defaultValue, + reason: 'ERROR', + errorCode: 'PROVIDER_NOT_READY' as ErrorCode, + } +} + export function evaluateRulesBasedConfiguration( config: UniversalFlagConfigurationV1 | undefined, type: T, @@ -63,3 +114,57 @@ export function evaluateRulesBasedConfiguration( } } } + +function evaluatePrecomputed( + precomputed: PrecomputedConfiguration, + type: T, + flagKey: string, + defaultValue: FlagTypeToValue +): ResolutionDetails> { + const flag = precomputed.response.data.attributes.flags[flagKey] + if (!flag) { + return { + value: defaultValue, + reason: 'ERROR', + errorCode: 'FLAG_NOT_FOUND' as ErrorCode, + } + } + + if (flag.variationType && variationTypeToOpenFeature(flag.variationType) !== type) { + return { + value: defaultValue, + reason: 'ERROR', + errorCode: 'TYPE_MISMATCH' as ErrorCode, + } + } + + return { + value: flag.variationValue as FlagTypeToValue, + variant: flag.variationKey, + flagMetadata: { + allocationKey: flag.allocationKey, + variationType: flag.variationType, + doLog: flag.doLog, + } as PrecomputedFlagMetadata, + reason: flag.reason, + } as ResolutionDetails> +} + +function variationTypeToOpenFeature(s: string): FlagValueType { + const typeMap: Record = { + string: 'string', + boolean: 'boolean', + number: 'number', + integer: 'number', + float: 'number', + object: 'object', + + BOOLEAN: 'boolean', + STRING: 'string', + NUMERIC: 'number', + INTEGER: 'number', + JSON: 'object', + } + + return typeMap[s] || s.toLowerCase() +} From 9c4e4154f1869bb38dde7cd6849cc5374f3f35ba Mon Sep 17 00:00:00 2001 From: Sameeran Kunche Date: Tue, 14 Jul 2026 08:13:56 -0700 Subject: [PATCH 11/13] Preserve DatadogProvider default fallback --- packages/browser/src/openfeature/provider.ts | 62 +++++++++----------- 1 file changed, 29 insertions(+), 33 deletions(-) diff --git a/packages/browser/src/openfeature/provider.ts b/packages/browser/src/openfeature/provider.ts index af0c9c8e..e647860f 100644 --- a/packages/browser/src/openfeature/provider.ts +++ b/packages/browser/src/openfeature/provider.ts @@ -1,6 +1,13 @@ -import { type AssignmentCache, configMatchesContext, evaluate, type FlagsConfiguration } from '@datadog/flagging-core' +import { + type AssignmentCache, + configMatchesContext, + evaluate, + type FlagsConfiguration, + type FlagTypeToValue, +} from '@datadog/flagging-core' import type { EvaluationContext, + FlagValueType, Hook, JsonValue, Logger, @@ -272,14 +279,7 @@ export class DatadogProvider implements Provider { context: EvaluationContext, _logger: Logger ): ResolutionDetails { - return evaluate( - this.flagsConfiguration, - 'boolean', - flagKey, - defaultValue, - this.getEvaluationContext(context), - _logger - ) + return this.resolve('boolean', flagKey, defaultValue, context, _logger) } resolveStringEvaluation( @@ -288,14 +288,7 @@ export class DatadogProvider implements Provider { context: EvaluationContext, _logger: Logger ): ResolutionDetails { - return evaluate( - this.flagsConfiguration, - 'string', - flagKey, - defaultValue, - this.getEvaluationContext(context), - _logger - ) + return this.resolve('string', flagKey, defaultValue, context, _logger) } resolveNumberEvaluation( @@ -304,14 +297,7 @@ export class DatadogProvider implements Provider { context: EvaluationContext, _logger: Logger ): ResolutionDetails { - return evaluate( - this.flagsConfiguration, - 'number', - flagKey, - defaultValue, - this.getEvaluationContext(context), - _logger - ) + return this.resolve('number', flagKey, defaultValue, context, _logger) } resolveObjectEvaluation( @@ -326,14 +312,24 @@ export class DatadogProvider implements Provider { // type-sound way because there's no runtime information passed to // learn what type the user expects. So it's up to the user to // make sure they pass the appropriate type. - return evaluate( - this.flagsConfiguration, - 'object', - flagKey, - defaultValue, - this.getEvaluationContext(context), - _logger - ) as ResolutionDetails + return this.resolve('object', flagKey, defaultValue, context, _logger) as ResolutionDetails + } + + private resolve( + type: T, + flagKey: string, + defaultValue: FlagTypeToValue, + context: EvaluationContext, + logger: Logger + ): ResolutionDetails> { + if (!this.flagsConfiguration.precomputed && !this.flagsConfiguration.rulesBased) { + return { + value: defaultValue, + reason: 'DEFAULT', + } + } + + return evaluate(this.flagsConfiguration, type, flagKey, defaultValue, this.getEvaluationContext(context), logger) } private getEvaluationContext(context: EvaluationContext): EvaluationContext { From 47e49b04e27dba8d18e6427b3c5ac804f1c4a4d1 Mon Sep 17 00:00:00 2001 From: Sameeran Kunche Date: Tue, 14 Jul 2026 08:15:05 -0700 Subject: [PATCH 12/13] Avoid networked provider setup in local tests --- packages/browser/test/openfeature/provider.spec.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/browser/test/openfeature/provider.spec.ts b/packages/browser/test/openfeature/provider.spec.ts index e6746cc8..f353bd06 100644 --- a/packages/browser/test/openfeature/provider.spec.ts +++ b/packages/browser/test/openfeature/provider.spec.ts @@ -32,7 +32,6 @@ describe('DatadogProvider', () => { describe('configuration validation', () => { beforeEach(() => { setupProvider() - OpenFeature.setProvider(provider) }) it('should throw error when ddog-gov.com site is provided', () => { @@ -138,7 +137,6 @@ describe('DatadogProvider', () => { describe('metadata', () => { beforeEach(() => { setupProvider() - OpenFeature.setProvider(provider) }) it('should have correct metadata', () => { From 6a3d86c62535419322a2645ce68d4aa5a2c92e93 Mon Sep 17 00:00:00 2001 From: Sameeran Kunche Date: Tue, 14 Jul 2026 13:45:19 -0700 Subject: [PATCH 13/13] Fix CoreProvider formatting --- packages/browser/src/openfeature/core-provider.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/browser/src/openfeature/core-provider.ts b/packages/browser/src/openfeature/core-provider.ts index c14c6721..38063bd9 100644 --- a/packages/browser/src/openfeature/core-provider.ts +++ b/packages/browser/src/openfeature/core-provider.ts @@ -10,11 +10,7 @@ import type { ProviderMetadata, ResolutionDetails, } from '@openfeature/web-sdk' -import { - OpenFeatureEventEmitter, - type ProviderEventEmitter, - ProviderEvents, -} from '@openfeature/web-sdk' +import { OpenFeatureEventEmitter, type ProviderEventEmitter, ProviderEvents } from '@openfeature/web-sdk' export interface CoreProviderOptions { configuration: FlagsConfiguration