Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions packages/browser/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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('...flags configuration string...')
const provider = new CoreProvider({ configuration })

await OpenFeature.setProviderAndWait(provider)

await OpenFeature.setContext({ targetingKey: 'user-123', plan: 'enterprise' })

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

## End-user license agreement

https://www.datadoghq.com/legal/eula
79 changes: 0 additions & 79 deletions packages/browser/src/evaluation.ts

This file was deleted.

9 changes: 7 additions & 2 deletions packages/browser/src/index.ts
Original file line number Diff line number Diff line change
@@ -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,
})
132 changes: 132 additions & 0 deletions packages/browser/src/openfeature/core-provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import type { FlagsConfiguration, FlagTypeToValue } from '@datadog/flagging-core'
import { configMatchesContext, evaluate } from '@datadog/flagging-core'
import type {
EvaluationContext,
FlagValueType,
JsonValue,
Logger,
Paradigm,
Provider,
ProviderMetadata,
ResolutionDetails,
} from '@openfeature/web-sdk'
import { OpenFeatureEventEmitter, type ProviderEventEmitter, ProviderEvents } from '@openfeature/web-sdk'

export interface CoreProviderOptions {
configuration: FlagsConfiguration
}

export class CoreProvider implements Provider {
readonly metadata: ProviderMetadata = {
name: 'datadog-core',
}
readonly runsOn: Paradigm = 'client'
readonly events: ProviderEventEmitter<ProviderEvents>

private flagsConfiguration: FlagsConfiguration
private context: EvaluationContext = {}

constructor(options: CoreProviderOptions) {
this.events = new OpenFeatureEventEmitter()
this.flagsConfiguration = options.configuration
}

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.events.emit(ProviderEvents.Error, { error })
return
}

this.events.emit(hadEvaluatableConfiguration ? ProviderEvents.ConfigurationChanged : ProviderEvents.Ready)
}

async initialize(context: EvaluationContext = {}): Promise<void> {
this.context = context
const error = getConfigurationError(this.flagsConfiguration, this.context)
if (error) {
throw error
}
}

onContextChange(_oldContext: EvaluationContext, newContext: EvaluationContext): void {
this.context = newContext
const error = getConfigurationError(this.flagsConfiguration, this.context)
if (error) {
throw error
}
}

resolveBooleanEvaluation(
flagKey: string,
defaultValue: boolean,
context: EvaluationContext,
logger: Logger
): ResolutionDetails<boolean> {
return this.resolve('boolean', flagKey, defaultValue, context, logger)
}

resolveStringEvaluation(
flagKey: string,
defaultValue: string,
context: EvaluationContext,
logger: Logger
): ResolutionDetails<string> {
return this.resolve('string', flagKey, defaultValue, context, logger)
}

resolveNumberEvaluation(
flagKey: string,
defaultValue: number,
context: EvaluationContext,
logger: Logger
): ResolutionDetails<number> {
return this.resolve('number', flagKey, defaultValue, context, logger)
}

resolveObjectEvaluation<T extends JsonValue>(
flagKey: string,
defaultValue: T,
context: EvaluationContext,
logger: Logger
): ResolutionDetails<T> {
return this.resolve('object', flagKey, defaultValue, context, logger) as ResolutionDetails<T>
}

private resolve<T extends FlagValueType>(
type: T,
flagKey: string,
defaultValue: FlagTypeToValue<T>,
context: EvaluationContext,
logger: Logger
): ResolutionDetails<FlagTypeToValue<T>> {
return evaluate(this.flagsConfiguration, type, flagKey, defaultValue, context, logger)
}

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
}
44 changes: 38 additions & 6 deletions packages/browser/src/openfeature/provider.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import { type AssignmentCache, configMatchesContext, 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,
Expand All @@ -23,7 +30,6 @@ import {
type FlaggingInitConfiguration,
validateAndBuildFlaggingConfiguration,
} from '../domain/configuration'
import { evaluate } from '../evaluation'
import { createExposureLoggingHook } from './exposures'
import { createFlagEvalEVPHook } from './flagEvaluations'
import { createRumTrackingHook } from './rumIntegration'
Expand Down Expand Up @@ -67,6 +73,7 @@ export class DatadogProvider implements Provider {
status: ProviderStatus

private flagsConfiguration: FlagsConfiguration = {}
private context: EvaluationContext = {}
private flagsCache: IndexedDBFlagsCache | undefined

private exposureCache: AssignmentCache | undefined
Expand Down Expand Up @@ -184,6 +191,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)

Expand Down Expand Up @@ -271,7 +279,7 @@ export class DatadogProvider implements Provider {
context: EvaluationContext,
_logger: Logger
): ResolutionDetails<boolean> {
return evaluate(this.flagsConfiguration, 'boolean', flagKey, defaultValue, context)
return this.resolve('boolean', flagKey, defaultValue, context, _logger)
}

resolveStringEvaluation(
Expand All @@ -280,7 +288,7 @@ export class DatadogProvider implements Provider {
context: EvaluationContext,
_logger: Logger
): ResolutionDetails<string> {
return evaluate(this.flagsConfiguration, 'string', flagKey, defaultValue, context)
return this.resolve('string', flagKey, defaultValue, context, _logger)
}

resolveNumberEvaluation(
Expand All @@ -289,7 +297,7 @@ export class DatadogProvider implements Provider {
context: EvaluationContext,
_logger: Logger
): ResolutionDetails<number> {
return evaluate(this.flagsConfiguration, 'number', flagKey, defaultValue, context)
return this.resolve('number', flagKey, defaultValue, context, _logger)
}

resolveObjectEvaluation<T extends JsonValue>(
Expand All @@ -304,6 +312,30 @@ 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<T>
return this.resolve('object', flagKey, defaultValue, context, _logger) as ResolutionDetails<T>
}

private resolve<T extends FlagValueType>(
type: T,
flagKey: string,
defaultValue: FlagTypeToValue<T>,
context: EvaluationContext,
logger: Logger
): ResolutionDetails<FlagTypeToValue<T>> {
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 {
if (Object.keys(context).length > 0 || Object.keys(this.context).length === 0) {
return context
}
return this.context
}
}
2 changes: 1 addition & 1 deletion packages/browser/test/data/precomputed-v1-wire.json
Original file line number Diff line number Diff line change
Expand Up @@ -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\"}}}}}"
}
}
Loading