Skip to content
Draft
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
4 changes: 3 additions & 1 deletion LICENSE-3rdparty.csv
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ prod,react,MIT,Copyright (c) Facebook, Inc. and its affiliates.
prod,react-dom,MIT,Copyright (c) Facebook, Inc. and its affiliates.
dev,@eslint/js,MIT,Copyright OpenJS Foundation and other contributors, <www.openjsf.org>
dev,@jsdevtools/coverage-istanbul-loader,MIT,Copyright (c) 2015 James Messinger
dev,@openfeature/core,Apache-2.0,Copyright Linux Foundation
dev,@openfeature/web-sdk,Apache-2.0,Copyright Linux Foundation
dev,@playwright/test,Apache-2.0,Copyright Microsoft Corporation
dev,@types/chrome,MIT,Copyright Microsoft Corporation
dev,@types/connect-busboy,MIT,Copyright Microsoft Corporation
Expand Down Expand Up @@ -72,4 +74,4 @@ dev,webpack,MIT,Copyright JS Foundation and other contributors
dev,webpack-cli,MIT,Copyright JS Foundation and other contributors
dev,webpack-dev-middleware,MIT,Copyright JS Foundation and other contributors
dev,@swc/core,Apache-2.0,Copyright (c) SWC Contributors
dev,swc-loader,MIT,Copyright (c) SWC Contributors
dev,swc-loader,MIT,Copyright (c) SWC Contributors
4 changes: 4 additions & 0 deletions eslint-local-rules/disallowSideEffects.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ const pathsWithSideEffect = new Set([
const packagesWithoutSideEffect = new Set([
'@datadog/browser-core',
'@datadog/browser-rum-core',
// @openfeature/core is mostly type definitions and enums. I have
// reviewed the whole library source code as of 2025-05-26 and it
// has no side effects.
'@openfeature/core',
'react',
'react-router-dom',
])
Expand Down
2 changes: 2 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,8 @@ export default tseslint.config(
'no-restricted-syntax': [
'error',
{
// Using classes seems to increase bundle size. See
// https://github.com/DataDog/browser-sdk/pull/2885
selector: 'ClassDeclaration',
message: 'Classes are not allowed. Use functions instead.',
},
Expand Down
47 changes: 47 additions & 0 deletions packages/flagging/README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,50 @@
# Flagging SDK (Prerelease)

This package supports flagging and experimentation by performing evaluation in the browser.

## Initialize

```typescript
import { DatadogProvider } from '@datadog/openfeature-provider'

const datadogFlaggingProvider = new DatadogProvider()

// provide the subject
const subject = {
targetingKey: 'subject-key-1',
}
await OpenFeature.setContext(subject)

// initialize
await OpenFeature.setProviderAndWait(datadogFlaggingProvider)
```

## Evaluation

```typescript
const client = OpenFeature.getClient()

// provide the flag key and a default value which is returned for exceptional conditions.
const flagEval = client.getBooleanValue('<FLAG_KEY>', false)
```

## Integration with RUM feature flag tracking

```typescript
// Initialize RUM with experimental feature flags tracking
import { datadogRum } from '@datadog/browser-rum';

// Initialize Datadog Browser SDK
datadogRum.init({
...
enableExperimentalFeatures: ["feature_flags"],
...
});

// Add OpenFeature hook
OpenFeature.addHooks({
after(_hookContext: HookContext, details: EvaluationDetails<FlagValue>) {
datadogRum.addFeatureFlagEvaluation(details.flagKey, details.value)
}
})
```
Comment on lines +33 to +50
Copy link
Author

Choose a reason for hiding this comment

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

@leoromanovsky here's an example of sending of sending flag evaluation to RUM using existing feature flags implementation

9 changes: 7 additions & 2 deletions packages/flagging/package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "@datadog/browser-flagging",
"name": "@datadog/openfeature-provider",
"version": "6.8.0",
"license": "Apache-2.0",
"private": true,
Expand All @@ -18,13 +18,18 @@
"@datadog/browser-core": "6.8.0"
},
"peerDependencies": {
"@datadog/browser-rum": "6.8.0"
"@datadog/browser-rum": "6.8.0",
"@openfeature/web-sdk": "^1.5.0"
},
"peerDependenciesMeta": {
"@datadog/browser-rum": {
"optional": true
}
},
"devDependencies": {
"@openfeature/core": "1.8.0",
"@openfeature/web-sdk": "1.5.0"
},
"repository": {
"type": "git",
"url": "https://github.com/DataDog/browser-sdk.git",
Expand Down
46 changes: 46 additions & 0 deletions packages/flagging/src/configuration/configuration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import type { EvaluationContext, FlagValueType, JsonValue, ResolutionDetails } from '@openfeature/web-sdk'
/**
* Internal configuration for DatadogProvider.
*/
export type Configuration = {
/** @internal */
precomputed?: PrecomputedConfiguration
}

/** @internal */
export type PrecomputedConfiguration = {
response: PrecomputedConfigurationResponse
context?: EvaluationContext
fetchedAt?: UnixTimestamp
}

// Fancy way to map FlagValueType to expected FlagValue.
/** @internal */
export type FlagTypeToValue<T extends FlagValueType> = {
['boolean']: boolean
['string']: string
['number']: number
['object']: JsonValue
}[T]

/** @internal
* Timestamp in milliseconds since Unix Epoch.
*/
export type UnixTimestamp = number

/** @internal */
export type PrecomputedConfigurationResponse = {
data: {
attributes: {
/** When configuration was generated. */
createdAt: number
flags: Record<string, PrecomputedFlag>
}
}
}

/** @internal */
export type PrecomputedFlag<T extends FlagValueType = FlagValueType> = {
type: T
resolution: ResolutionDetails<FlagTypeToValue<T>>
}
2 changes: 2 additions & 0 deletions packages/flagging/src/configuration/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export type * from './configuration'
export * from './wire'
58 changes: 58 additions & 0 deletions packages/flagging/src/configuration/wire.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import type { EvaluationContext } from '@openfeature/web-sdk'

import type { Configuration, UnixTimestamp } from './configuration'

type ConfigurationWire = {
version: 2
precomputed?: {
context?: EvaluationContext
response: string
fetchedAt?: UnixTimestamp
}
}

/**
* Create configuration from a string created with `configurationToString`.
*/
export function configurationFromString(s: string): Configuration {
Copy link
Contributor

Choose a reason for hiding this comment

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

Who needs classes 😇

Copy link
Author

Choose a reason for hiding this comment

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

embracing classless life and tree-shaking 😅

try {
const wire: ConfigurationWire = JSON.parse(s)

if (wire.version !== 2) {
// Unknown version
return {}
}

const configuration: Configuration = {}
if (wire.precomputed) {
configuration.precomputed = {
...wire.precomputed,
response: JSON.parse(wire.precomputed.response),
}
}

return configuration
} catch {
return {}
}
}

/**
* Serialize configuration to string that can be deserialized with
* `configurationFromString`. The serialized string format is
* unspecified.
*/
export function configurationToString(configuration: Configuration): string {
const wire: ConfigurationWire = {
version: 2,
}

if (configuration.precomputed) {
wire.precomputed = {
...configuration.precomputed,
response: JSON.stringify(configuration.precomputed),
}
}

return JSON.stringify(wire)
}
13 changes: 9 additions & 4 deletions packages/flagging/src/entries/main.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import { defineGlobal, getGlobalObject } from '@datadog/browser-core'
import { flagging as importedFlagging } from '../hello'

export const datadogFlagging = importedFlagging
import { DatadogProvider } from '../openfeature/provider'

export { DatadogProvider }
export { configurationFromString, configurationToString } from '../configuration'

interface BrowserWindow extends Window {
DD_FLAGGING?: typeof datadogFlagging
DD_FLAGGING?: {
Provider: typeof DatadogProvider
}
}
defineGlobal(getGlobalObject<BrowserWindow>(), 'DD_FLAGGING', datadogFlagging)

defineGlobal(getGlobalObject<BrowserWindow>(), 'DD_FLAGGING', { Provider: DatadogProvider })
52 changes: 52 additions & 0 deletions packages/flagging/src/evaluation.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import configurationWire from '../test/data/precomputed-v2-wire.json'

import { configurationFromString } from './configuration'
import { evaluate } from './evaluation'

const configuration = configurationFromString(
// Adding stringify because import has parsed JSON
JSON.stringify(configurationWire)
)

describe('evaluate', () => {
it('returns default for missing configuration', () => {
const result = evaluate({}, 'boolean', 'boolean-flag', true, {})
expect(result).toEqual({
value: true,
reason: 'DEFAULT',
})
})

it('returns default for unknown flag', () => {
const result = evaluate(configuration, 'string', 'unknown-flag', 'default', {})
expect(result).toEqual({
value: 'default',
reason: 'ERROR',
errorCode: 'FLAG_NOT_FOUND' as any,
})
})

it('resolves string flag', () => {
const result = evaluate(configuration, 'string', 'string-flag', 'default', {})
expect(result).toEqual({
value: 'red',
variant: 'variation-123',
flagMetadata: {
allocationKey: 'allocation-123',
experiment: true,
},
})
})

it('resolves object flag', () => {
const result = evaluate<any>(configuration, 'object', 'json-flag', { hello: 'world' }, {})
expect(result).toEqual({
value: { key: 'value', prop: 123 },
variant: 'variation-127',
flagMetadata: {
allocationKey: 'allocation-127',
experiment: true,
},
})
})
})
47 changes: 47 additions & 0 deletions packages/flagging/src/evaluation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import type { ErrorCode, EvaluationContext, FlagValueType, ResolutionDetails } from '@openfeature/web-sdk'

import type { Configuration, PrecomputedConfiguration, FlagTypeToValue } from './configuration'

export function evaluate<T extends FlagValueType>(
configuration: Configuration,
type: T,
flagKey: string,
defaultValue: FlagTypeToValue<T>,
context: EvaluationContext
): ResolutionDetails<FlagTypeToValue<T>> {
if (configuration.precomputed) {
return evaluatePrecomputed(configuration.precomputed, type, flagKey, defaultValue, context)
}

return {
value: defaultValue,
reason: 'DEFAULT',
}
}

function evaluatePrecomputed<T extends FlagValueType>(
precomputed: PrecomputedConfiguration,
type: T,
flagKey: string,
defaultValue: FlagTypeToValue<T>,
_context: EvaluationContext
): ResolutionDetails<FlagTypeToValue<T>> {
const flag = precomputed.response.data.attributes.flags[flagKey]
if (!flag) {
return {
value: defaultValue,
reason: 'ERROR',
errorCode: 'FLAG_NOT_FOUND' as ErrorCode,
}
}

if (flag.type !== type) {
return {
value: defaultValue,
reason: 'ERROR',
errorCode: 'TYPE_MISMATCH' as ErrorCode,
}
}

return flag.resolution as ResolutionDetails<FlagTypeToValue<T>>
}
7 changes: 0 additions & 7 deletions packages/flagging/src/hello.ts

This file was deleted.

Loading