Skip to content

Commit 3078f1b

Browse files
committed
✨ [FFL-24] add openfeature dependency and datadog provider
1 parent 839f58a commit 3078f1b

22 files changed

+944
-35
lines changed

LICENSE-3rdparty.csv

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,16 @@ file,tracekit,MIT,Copyright 2013 Onur Can Cakmak and all TraceKit contributors
66
file,web-vitals,Apache-2.0,Copyright 2020 Google LLC
77
prod,@mantine/core,MIT,Copyright (c) 2021 Vitaly Rtishchev
88
prod,@mantine/hooks,MIT,Copyright (c) 2021 Vitaly Rtishchev
9+
prod,@openfeature/core,Apache-2.0,Copyright Linux Foundation
10+
prod,@openfeature/web-sdk,Apache-2.0,Copyright Linux Foundation
911
prod,@tabler/icons-react,MIT,Copyright (c) 2020-2023 Paweł Kuna
1012
prod,clsx,MIT,Copyright (c) Luke Edwards <[email protected]> (lukeed.com)
1113
prod,react,MIT,Copyright (c) Facebook, Inc. and its affiliates.
1214
prod,react-dom,MIT,Copyright (c) Facebook, Inc. and its affiliates.
1315
dev,@eslint/js,MIT,Copyright OpenJS Foundation and other contributors, <www.openjsf.org>
1416
dev,@jsdevtools/coverage-istanbul-loader,MIT,Copyright (c) 2015 James Messinger
17+
dev,@openfeature/core,Apache-2.0,Copyright Linux Foundation
18+
dev,@openfeature/web-sdk,Apache-2.0,Copyright Linux Foundation
1519
dev,@playwright/test,Apache-2.0,Copyright Microsoft Corporation
1620
dev,@types/chrome,MIT,Copyright Microsoft Corporation
1721
dev,@types/connect-busboy,MIT,Copyright Microsoft Corporation
@@ -72,4 +76,4 @@ dev,webpack,MIT,Copyright JS Foundation and other contributors
7276
dev,webpack-cli,MIT,Copyright JS Foundation and other contributors
7377
dev,webpack-dev-middleware,MIT,Copyright JS Foundation and other contributors
7478
dev,@swc/core,Apache-2.0,Copyright (c) SWC Contributors
75-
dev,swc-loader,MIT,Copyright (c) SWC Contributors
79+
dev,swc-loader,MIT,Copyright (c) SWC Contributors

packages/flagging/README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,29 @@
11
# Flagging SDK (Prerelease)
22

33
This package supports flagging and experimentation by performing evaluation in the browser.
4+
5+
## Initialize
6+
7+
```typescript
8+
import { DatadogProvider } from '@datadog/browser-flagging'
9+
10+
const datadogFlaggingProvider = new DatadogProvider()
11+
12+
// provide the subject
13+
const subject = {
14+
key: 'subject-key-1',
15+
}
16+
await OpenFeature.setContext(subject)
17+
18+
// initialize
19+
await OpenFeature.setProviderAndWait(datadogFlaggingProvider)
20+
```
21+
22+
## Evaluation
23+
24+
```typescript
25+
const client = OpenFeature.getClient()
26+
27+
// provide the flag key and a default value which is returned for exceptional conditions.
28+
const flagEval = client.getBooleanValue('<FLAG_KEY>', false)
29+
```

packages/flagging/package.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,14 @@
1515
"replace-build-env": "node ../../scripts/build/replace-build-env.js"
1616
},
1717
"dependencies": {
18-
"@datadog/browser-core": "6.8.0"
18+
"@datadog/browser-core": "6.8.0",
19+
"@openfeature/core": "1.8.0",
20+
"@openfeature/web-sdk": "1.5.0"
1921
},
2022
"peerDependencies": {
21-
"@datadog/browser-rum": "6.8.0"
23+
"@datadog/browser-rum": "6.8.0",
24+
"@openfeature/core": "1.8.0",
25+
"@openfeature/web-sdk": "1.5.0"
2226
},
2327
"peerDependenciesMeta": {
2428
"@datadog/browser-rum": {
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export interface ISyncStore<T> {
2+
get(key: string): T | null
3+
entries(): Record<string, T>
4+
getKeys(): string[]
5+
isInitialized(): boolean
6+
setEntries(entries: Record<string, T>): void
7+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { createMemoryStore } from './memoryStore'
2+
3+
describe('MemoryOnlyConfigurationStore', () => {
4+
let memoryStore: ReturnType<typeof createMemoryStore<string>>
5+
6+
beforeEach(() => {
7+
memoryStore = createMemoryStore()
8+
})
9+
10+
it('should initialize without any entries', () => {
11+
expect(memoryStore.isInitialized()).toBe(false)
12+
expect(memoryStore.getKeys()).toEqual([])
13+
})
14+
15+
it('should return null for non-existent keys', () => {
16+
expect(memoryStore.get('nonexistent')).toBeNull()
17+
})
18+
19+
it('should allow setting and retrieving entries', () => {
20+
memoryStore.setEntries({ key1: 'value1', key2: 'value2' })
21+
expect(memoryStore.get('key1')).toBe('value1')
22+
expect(memoryStore.get('key2')).toBe('value2')
23+
})
24+
25+
it('should report initialized after setting entries', () => {
26+
memoryStore.setEntries({ key1: 'value1' })
27+
expect(memoryStore.isInitialized()).toBe(true)
28+
})
29+
30+
it('should return all keys', () => {
31+
memoryStore.setEntries({ key1: 'value1', key2: 'value2', key3: 'value3' })
32+
expect(memoryStore.getKeys()).toEqual(['key1', 'key2', 'key3'])
33+
})
34+
35+
it('should return all entries', () => {
36+
const entries = { key1: 'value1', key2: 'value2', key3: 'value3' }
37+
memoryStore.setEntries(entries)
38+
expect(memoryStore.entries()).toEqual(entries)
39+
})
40+
41+
it('should overwrite existing entries', () => {
42+
memoryStore.setEntries({ toBeReplaced: 'old value', toBeRemoved: 'delete me' })
43+
expect(memoryStore.get('toBeReplaced')).toBe('old value')
44+
expect(memoryStore.get('toBeRemoved')).toBe('delete me')
45+
expect(memoryStore.get('toBeAdded')).toBeNull()
46+
47+
memoryStore.setEntries({ toBeReplaced: 'new value', toBeAdded: 'add me' })
48+
expect(memoryStore.get('toBeReplaced')).toBe('new value')
49+
expect(memoryStore.get('toBeRemoved')).toBeNull()
50+
expect(memoryStore.get('toBeAdded')).toBe('add me')
51+
})
52+
})
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import type { ISyncStore } from './configurationStore'
2+
3+
export function createMemoryStore<T>(): ISyncStore<T> {
4+
let store: Record<string, T> = {}
5+
let initialized = false
6+
7+
return {
8+
get(key: string): T | null {
9+
return store[key] ?? null
10+
},
11+
12+
entries(): Record<string, T> {
13+
return store
14+
},
15+
16+
getKeys(): string[] {
17+
return Object.keys(store)
18+
},
19+
20+
isInitialized(): boolean {
21+
return initialized
22+
},
23+
24+
setEntries(entries: Record<string, T>): void {
25+
store = { ...entries }
26+
initialized = true
27+
},
28+
}
29+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { MOCK_DEOBFUSCATED_PRECOMPUTED_RESPONSE_FILE, readMockConfigurationWireResponse } from '../../test/helpers'
2+
import { configurationWireV1 } from './configurationWireTypes'
3+
4+
describe('Response String Type Safety', () => {
5+
const mockFlagConfig = readMockConfigurationWireResponse(MOCK_DEOBFUSCATED_PRECOMPUTED_RESPONSE_FILE)
6+
7+
describe('ConfigurationWireV1', () => {
8+
it('should create empty configuration', () => {
9+
const config = configurationWireV1.empty()
10+
11+
expect(config.version).toBe(1)
12+
expect(config.precomputed).toBeUndefined()
13+
})
14+
15+
it('should include fetchedAt timestamps', () => {
16+
const wirePacket = configurationWireV1.fromString(mockFlagConfig)
17+
18+
expect(wirePacket.precomputed).toBeDefined()
19+
expect(wirePacket.precomputed?.response).toBeDefined()
20+
expect(wirePacket.precomputed?.subjectKey).toBeDefined()
21+
expect(wirePacket.precomputed?.subjectAttributes).toBeDefined()
22+
})
23+
})
24+
})
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import type { ContextAttributes, Environment, FlagKey, PrecomputedFlag } from '../interfaces'
2+
import { FormatEnum } from '../interfaces'
3+
4+
// Base interface for all configuration responses
5+
interface IBasePrecomputedConfigurationResponse {
6+
readonly format: FormatEnum.PRECOMPUTED
7+
readonly obfuscated: boolean
8+
readonly createdAt: string
9+
readonly environment?: Environment
10+
readonly subjectKey: string
11+
readonly subjectAttributes?: ContextAttributes
12+
}
13+
14+
export interface IPrecomputedConfigurationResponse extends IBasePrecomputedConfigurationResponse {
15+
readonly obfuscated: false // Always false
16+
readonly flags: Record<FlagKey, PrecomputedFlag>
17+
}
18+
19+
export interface IPrecomputedConfiguration {
20+
// JSON encoded configuration response (obfuscated or unobfuscated)
21+
readonly response: string
22+
readonly subjectKey: string
23+
readonly subjectAttributes?: ContextAttributes
24+
}
25+
26+
export function createPrecomputedConfiguration(
27+
response: string,
28+
subjectKey: string,
29+
subjectAttributes?: ContextAttributes
30+
): IPrecomputedConfiguration {
31+
return {
32+
response,
33+
subjectKey,
34+
subjectAttributes,
35+
}
36+
}
37+
38+
export function createUnobfuscatedPrecomputedConfiguration(
39+
subjectKey: string,
40+
flags: Record<FlagKey, PrecomputedFlag>,
41+
subjectAttributes?: ContextAttributes,
42+
environment?: Environment
43+
): IPrecomputedConfiguration {
44+
const response = createPrecomputedConfigurationResponse(subjectKey, flags, subjectAttributes, environment)
45+
return createPrecomputedConfiguration(JSON.stringify(response), subjectKey, subjectAttributes)
46+
}
47+
48+
export function createPrecomputedConfigurationResponse(
49+
subjectKey: string,
50+
flags: Record<FlagKey, PrecomputedFlag>,
51+
subjectAttributes?: ContextAttributes,
52+
environment?: Environment
53+
): IPrecomputedConfigurationResponse {
54+
return {
55+
format: FormatEnum.PRECOMPUTED,
56+
obfuscated: false,
57+
createdAt: new Date().toISOString(),
58+
subjectKey,
59+
subjectAttributes,
60+
environment,
61+
flags,
62+
}
63+
}
64+
65+
// "Wire" in the name means "in-transit"/"file" format.
66+
// In-memory representation may differ significantly and is up to SDKs.
67+
export interface IConfigurationWire {
68+
/**
69+
* Version field should be incremented for breaking format changes.
70+
* For example, removing required fields or changing field type/meaning.
71+
*/
72+
readonly version: number
73+
readonly precomputed?: IPrecomputedConfiguration
74+
}
75+
76+
export function createConfigurationWireV1(precomputed?: IPrecomputedConfiguration): IConfigurationWire {
77+
return {
78+
version: 1,
79+
precomputed,
80+
}
81+
}
82+
83+
export const configurationWireV1 = {
84+
fromString(str: string): IConfigurationWire {
85+
return JSON.parse(str) as IConfigurationWire
86+
},
87+
88+
precomputed(precomputedConfig: IPrecomputedConfiguration): IConfigurationWire {
89+
return createConfigurationWireV1(precomputedConfig)
90+
},
91+
92+
empty(): IConfigurationWire {
93+
return createConfigurationWireV1()
94+
},
95+
96+
toString(config: IConfigurationWire): string {
97+
return JSON.stringify(config)
98+
},
99+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { ISyncStore } from './configuration-store/configurationStore'
2+
import { createMemoryStore } from './configuration-store/memoryStore'
3+
import type { PrecomputedFlag } from './interfaces'
4+
5+
export function precomputedFlagsStorageFactory(): ISyncStore<PrecomputedFlag> {
6+
return createMemoryStore()
7+
}
Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
import { defineGlobal, getGlobalObject } from '@datadog/browser-core'
2-
import { flagging as importedFlagging } from '../hello'
2+
import { DatadogProvider } from '../openfeature/provider'
3+
import { offlinePrecomputedInit as offlineClientInit } from '../precomputeClient'
34

4-
export const datadogFlagging = importedFlagging
5+
export { DatadogProvider, offlineClientInit }
56

67
interface BrowserWindow extends Window {
7-
DD_FLAGGING?: typeof datadogFlagging
8+
DD_FLAGGING?: DatadogProvider
89
}
9-
defineGlobal(getGlobalObject<BrowserWindow>(), 'DD_FLAGGING', datadogFlagging)
10+
11+
defineGlobal(
12+
getGlobalObject<BrowserWindow>(),
13+
'DD_FLAGGING',
14+
new DatadogProvider(offlineClientInit({ precomputedConfiguration: '' }) ?? undefined)
15+
)

0 commit comments

Comments
 (0)