Skip to content

FFL-2753 Add browser CoreProvider#336

Open
sameerank wants to merge 13 commits into
mainfrom
sameerank/FFL-2753-browser-core-provider
Open

FFL-2753 Add browser CoreProvider#336
sameerank wants to merge 13 commits into
mainfrom
sameerank/FFL-2753-browser-core-provider

Conversation

@sameerank

@sameerank sameerank commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Summary

Add an opt-in browser CoreProvider that evaluates a caller-supplied FlagsConfiguration without fetching or polling configuration.

This PR also extends the shared core configuration/evaluation layer so portable configuration can carry and evaluate rules-based config:

  • @datadog/flagging-core now supports a rulesBased slot in FlagsConfiguration and configurationFromString() / configurationToString().
  • @datadog/flagging-core exports the shared evaluate(...) helper used by browser OpenFeature providers.
  • @datadog/openfeature-browser exports CoreProvider and keeps the existing DatadogProvider API unchanged.
  • Browser evaluation now uses matching precomputed assignments first, then rules-based evaluation when available.
  • Precomputed-only configurations no longer resolve against a mismatched context.
  • extraLogging remains part of Datadog flag configuration data, but is not exposed through OpenFeature flagMetadata.

Why

FFL-2753 is the browser provider layer for the portable configuration / dynamic context work. Customers can bootstrap or otherwise provide a generic configuration wire payload and use CoreProvider to evaluate locally. When that payload includes rules-based configuration, OpenFeature context changes are handled locally without a network round trip.

Behavior

  • CoreProvider implements the browser OpenFeature Provider interface.
  • initialize() validates the initial configuration/context and follows OpenFeature's async lifecycle signature.
  • onContextChange() validates the new context synchronously and lets OpenFeature handle thrown lifecycle errors.
  • setConfiguration() replaces the active config and emits Ready, ConfigurationChanged, or Error because it is a provider-owned mutator rather than an OpenFeature lifecycle callback.
  • Precomputed configs are only used when their stored context matches the evaluation context. If rules-based config is also available, it is used as the fallback path.
  • The existing DatadogProvider no-config fallback behavior is preserved.

Release Plan

Because @datadog/openfeature-browser now imports the shared evaluate(...) helper from @datadog/flagging-core, publish a new @datadog/flagging-core version first. Then update the browser package to depend on that released core version before publishing @datadog/openfeature-browser.

The existing DatadogProvider behavior is preserved; the new CoreProvider remains opt-in.

Validation

  • node .yarn/releases/yarn-4.10.3.cjs workspace @datadog/flagging-core test --watchman=false
  • node .yarn/releases/yarn-4.10.3.cjs workspace @datadog/flagging-core typecheck
  • node .yarn/releases/yarn-4.10.3.cjs workspace @datadog/openfeature-browser test --watchman=false
  • node .yarn/releases/yarn-4.10.3.cjs workspace @datadog/openfeature-browser typecheck
  • node .yarn/releases/yarn-4.10.3.cjs workspace @datadog/openfeature-browser build
  • node .yarn/releases/yarn-4.10.3.cjs prettier --check packages/browser/src/openfeature/core-provider.ts
  • git diff --check

@sameerank
sameerank marked this pull request as ready for review July 10, 2026 21:29
@sameerank
sameerank requested a review from a team as a code owner July 10, 2026 21:29
@sameerank
sameerank requested review from dd-oleksii and pavlokhrebto and removed request for a team July 10, 2026 21:29
constructor(options: CoreProviderOptions) {
this.events = new OpenFeatureEventEmitter()
this.flagsConfiguration = options.configuration
this.status = hasEvaluatableConfiguration(options.configuration) ? ProviderStatus.READY : ProviderStatus.ERROR

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

status is deprecated / unused, so we can leave it out

Suggested change
this.status = hasEvaluatableConfiguration(options.configuration) ? ProviderStatus.READY : ProviderStatus.ERROR

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

efb2034 removes the CoreProvider status field

this.status = ProviderStatus.READY
}

async onContextChange(_oldContext: EvaluationContext, newContext: EvaluationContext): Promise<void> {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor: does it make sense to make initialize and onContextChange sync?

Suggested change
async onContextChange(_oldContext: EvaluationContext, newContext: EvaluationContext): Promise<void> {
onContextChange(_oldContext: EvaluationContext, newContext: EvaluationContext): Promise<void> {

@sameerank sameerank Jul 14, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call for onContextChange. The web provider signature allows sync context changes:

onContextChange?(oldContext: EvaluationContext, newContext: EvaluationContext): Promise<void> | void;

So I can remove async there and change the return type to void, which also avoids OpenFeature treating rules-based context changes as async reconciliation.

For initialize though, I believe we still need to return a promise to match the shared provider contract:

initialize?(context?: EvaluationContext): Promise<void>;

The SDK calls .then(...) on the returned value to mark the provider ready, so a truly sync initialize(): void would not be compatible with the current OpenFeature implementation.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

9cf1ccc makes CoreProvider context changes synchronous

const error = getConfigurationError(this.flagsConfiguration, this.context)
if (error) {
this.status = ProviderStatus.ERROR
this.events.emit(ProviderEvents.Error, { error })

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we emit an event here but not on ready path?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

f151909 removes the explicit error emit from onContextChange, so CoreProvider treats OpenFeature lifecycle methods consistently:

  • initialize(...) updates context and throws if invalid; OpenFeature handles provider readiness/error lifecycle.
  • onContextChange(...) updates context and throws if invalid; OpenFeature handles the context-change error lifecycle.
  • setConfiguration(...) still emits Ready, ConfigurationChanged, or Error because that method is our own provider API, not an OpenFeature lifecycle callback.

Comment on lines +127 to +133
if (!hasEvaluatableConfiguration(this.flagsConfiguration)) {
return {
value: defaultValue,
reason: 'ERROR',
errorCode: 'PROVIDER_NOT_READY' as ErrorCode,
}
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: this check looks like something that evaluate should handle

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

f011597 moves missing configuration fallback into evaluate

@sameerank sameerank Jul 14, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A follow up 9c4e415 fixes the consequence of this change: CoreProvider should return PROVIDER_NOT_READY when it has no configuration, but the pre-existing DatadogProvider tests expected the old behavior: return the caller’s default value with reason DEFAULT before fetched config is available. Or should I switch them both to PROVIDER_NOT_READY? I hesitate because changing the server side evaluation feels like creeping beyond the scope of what we're trying to do here

Comment on lines +138 to +143
private getEvaluationContext(context: EvaluationContext): EvaluationContext {
if (Object.keys(context).length > 0 || Object.keys(this.context).length === 0) {
return context
}
return this.context
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this needed? have you seen openfeature sdk passing the wrong context?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, unnecessary. 21195fb removes getEvaluationContext and relies on the OpenFeature to pass the right evaluation context into the provider resolve methods

flagMetadata: {
allocationKey: 'allocation-123',
doLog: true,
extraLogging: { experiment: true },

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

major:

  1. extraLogging is deprecated.
  2. it's Record<string, string> (boolean not allowed).
  3. flagMetadata only allows boolean, string, and number values (nested object is not allowed)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1ad76ed removes extraLogging from flag metadata

Comment thread packages/browser/README.md Outdated
import { configurationFromString, CoreProvider } from '@datadog/openfeature-browser'
import { OpenFeature } from '@openfeature/web-sdk'

const configuration = configurationFromString(window.__DD_FLAGS_CONFIGURATION__)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor: users might get confused about __DD_FLAGS_CONFIGURATION__ — what's that and how it is set. Maybe we should hint that it's just a string and they should transfer it however they want, or say that __DD_FLAGS_CONFIGURATION__ is set by SSR process?

Suggested change
const configuration = configurationFromString(window.__DD_FLAGS_CONFIGURATION__)
const configuration = configurationFromString("...flags configuration string...")

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

34b3e80 updates the portable configuration string example

Comment thread packages/browser/README.md Outdated
await OpenFeature.setContext({ targetingKey: 'user-123', plan: 'enterprise' })

const client = OpenFeature.getClient()
const enabled = await client.getBooleanValue('new-checkout', false)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor: web evalulation methods are sync:

Suggested change
const enabled = await client.getBooleanValue('new-checkout', false)
const enabled = client.getBooleanValue('new-checkout', false)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

9371b90 documents browser evaluations as synchronous

allocationKey: allocation.key,
variationType: variantTypeToFlagValueType(flag.variationType),
doLog: !!allocation.doLog,
extraLogging: selectedSplit.extraLogging,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

major: extraLogging is deprecated and violates flagMetadata spec requirements (only string, boolean, and number values are allowed — not nested object)

Comment thread packages/browser/src/evaluation.ts Outdated
error: () => {},
}

export function evaluate<T extends FlagValueType>(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor: are we able to move evaluation function to the core package?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, moved in 86081da

Will have to do a 2.0.1 release for @datadog/flagging-core and update the @datadog/openfeature-browser dependency before we can release

@sameerank
sameerank requested a review from dd-oleksii July 15, 2026 13:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants