FFL-2753 Add browser CoreProvider#336
Conversation
| constructor(options: CoreProviderOptions) { | ||
| this.events = new OpenFeatureEventEmitter() | ||
| this.flagsConfiguration = options.configuration | ||
| this.status = hasEvaluatableConfiguration(options.configuration) ? ProviderStatus.READY : ProviderStatus.ERROR |
There was a problem hiding this comment.
status is deprecated / unused, so we can leave it out
| this.status = hasEvaluatableConfiguration(options.configuration) ? ProviderStatus.READY : ProviderStatus.ERROR |
There was a problem hiding this comment.
efb2034 removes the CoreProvider status field
| this.status = ProviderStatus.READY | ||
| } | ||
|
|
||
| async onContextChange(_oldContext: EvaluationContext, newContext: EvaluationContext): Promise<void> { |
There was a problem hiding this comment.
minor: does it make sense to make initialize and onContextChange sync?
| async onContextChange(_oldContext: EvaluationContext, newContext: EvaluationContext): Promise<void> { | |
| onContextChange(_oldContext: EvaluationContext, newContext: EvaluationContext): Promise<void> { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 }) |
There was a problem hiding this comment.
Why do we emit an event here but not on ready path?
There was a problem hiding this comment.
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.
| if (!hasEvaluatableConfiguration(this.flagsConfiguration)) { | ||
| return { | ||
| value: defaultValue, | ||
| reason: 'ERROR', | ||
| errorCode: 'PROVIDER_NOT_READY' as ErrorCode, | ||
| } | ||
| } |
There was a problem hiding this comment.
nitpick: this check looks like something that evaluate should handle
There was a problem hiding this comment.
f011597 moves missing configuration fallback into evaluate
There was a problem hiding this comment.
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
| private getEvaluationContext(context: EvaluationContext): EvaluationContext { | ||
| if (Object.keys(context).length > 0 || Object.keys(this.context).length === 0) { | ||
| return context | ||
| } | ||
| return this.context | ||
| } |
There was a problem hiding this comment.
why is this needed? have you seen openfeature sdk passing the wrong context?
There was a problem hiding this comment.
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 }, |
There was a problem hiding this comment.
major:
extraLoggingis deprecated.- it's
Record<string, string>(boolean not allowed). flagMetadataonly allowsboolean,string, andnumbervalues (nested object is not allowed)
There was a problem hiding this comment.
1ad76ed removes extraLogging from flag metadata
| import { configurationFromString, CoreProvider } from '@datadog/openfeature-browser' | ||
| import { OpenFeature } from '@openfeature/web-sdk' | ||
|
|
||
| const configuration = configurationFromString(window.__DD_FLAGS_CONFIGURATION__) |
There was a problem hiding this comment.
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?
| const configuration = configurationFromString(window.__DD_FLAGS_CONFIGURATION__) | |
| const configuration = configurationFromString("...flags configuration string...") |
There was a problem hiding this comment.
34b3e80 updates the portable configuration string example
| await OpenFeature.setContext({ targetingKey: 'user-123', plan: 'enterprise' }) | ||
|
|
||
| const client = OpenFeature.getClient() | ||
| const enabled = await client.getBooleanValue('new-checkout', false) |
There was a problem hiding this comment.
minor: web evalulation methods are sync:
| const enabled = await client.getBooleanValue('new-checkout', false) | |
| const enabled = client.getBooleanValue('new-checkout', false) |
There was a problem hiding this comment.
9371b90 documents browser evaluations as synchronous
| allocationKey: allocation.key, | ||
| variationType: variantTypeToFlagValueType(flag.variationType), | ||
| doLog: !!allocation.doLog, | ||
| extraLogging: selectedSplit.extraLogging, |
There was a problem hiding this comment.
major: extraLogging is deprecated and violates flagMetadata spec requirements (only string, boolean, and number values are allowed — not nested object)
| error: () => {}, | ||
| } | ||
|
|
||
| export function evaluate<T extends FlagValueType>( |
There was a problem hiding this comment.
minor: are we able to move evaluation function to the core package?
There was a problem hiding this comment.
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
Summary
Add an opt-in browser
CoreProviderthat evaluates a caller-suppliedFlagsConfigurationwithout 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-corenow supports arulesBasedslot inFlagsConfigurationandconfigurationFromString()/configurationToString().@datadog/flagging-coreexports the sharedevaluate(...)helper used by browser OpenFeature providers.@datadog/openfeature-browserexportsCoreProviderand keeps the existingDatadogProviderAPI unchanged.extraLoggingremains part of Datadog flag configuration data, but is not exposed through OpenFeatureflagMetadata.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
CoreProviderto evaluate locally. When that payload includes rules-based configuration, OpenFeature context changes are handled locally without a network round trip.Behavior
CoreProviderimplements the browser OpenFeatureProviderinterface.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 emitsReady,ConfigurationChanged, orErrorbecause it is a provider-owned mutator rather than an OpenFeature lifecycle callback.DatadogProviderno-config fallback behavior is preserved.Release Plan
Because
@datadog/openfeature-browsernow imports the sharedevaluate(...)helper from@datadog/flagging-core, publish a new@datadog/flagging-coreversion first. Then update the browser package to depend on that released core version before publishing@datadog/openfeature-browser.The existing
DatadogProviderbehavior is preserved; the newCoreProviderremains opt-in.Validation
node .yarn/releases/yarn-4.10.3.cjs workspace @datadog/flagging-core test --watchman=falsenode .yarn/releases/yarn-4.10.3.cjs workspace @datadog/flagging-core typechecknode .yarn/releases/yarn-4.10.3.cjs workspace @datadog/openfeature-browser test --watchman=falsenode .yarn/releases/yarn-4.10.3.cjs workspace @datadog/openfeature-browser typechecknode .yarn/releases/yarn-4.10.3.cjs workspace @datadog/openfeature-browser buildnode .yarn/releases/yarn-4.10.3.cjs prettier --check packages/browser/src/openfeature/core-provider.tsgit diff --check