Skip to content

Commit 7beba56

Browse files
committed
[FFL-2871] treat empty contexts literally
1 parent 6da4c27 commit 7beba56

3 files changed

Lines changed: 40 additions & 66 deletions

File tree

packages/browser/README.md

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -135,17 +135,26 @@ import { configurationFromString, DatadogProvider } from '@datadog/openfeature-b
135135

136136
`DatadogOfflineProvider` 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.
137137

138-
For static offline initialization, a precomputed-only configuration adopts its embedded context when the OpenFeature context is empty. You do not need to call `OpenFeature.setContext()`. An explicit non-empty context must match the embedded context.
138+
For static offline initialization, a context-specific precomputed configuration must use the OpenFeature context for which it was computed. Use `getPrecomputedContext()` to access a detached copy through the supported API. An empty context (`{}`) is treated literally and does not select the embedded context.
139139

140140
```javascript
141-
import { configurationFromString, DatadogOfflineProvider } from '@datadog/openfeature-browser/precomputed'
141+
import {
142+
configurationFromString,
143+
getPrecomputedContext,
144+
DatadogOfflineProvider,
145+
} from '@datadog/openfeature-browser/precomputed'
142146
import { OpenFeature } from '@openfeature/web-sdk'
143147

144148
const configuration = configurationFromString('...flags configuration string...')
145149
const provider = new DatadogOfflineProvider()
146150
provider.setConfiguration(configuration)
151+
const context = getPrecomputedContext(configuration)
147152

148-
await OpenFeature.setProviderAndWait(provider)
153+
if (context === undefined) {
154+
await OpenFeature.setProviderAndWait(provider)
155+
} else {
156+
await OpenFeature.setProviderAndWait(provider, context)
157+
}
149158

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

packages/browser/src/openfeature/offline-provider.ts

Lines changed: 5 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,7 @@ export class DatadogOfflineProvider extends DatadogCoreProvider {
3131
const hadEvaluatableConfiguration = this.canEvaluateCurrentContext()
3232
this.flagsConfiguration = configuration
3333

34-
const error = toOpenFeatureError(
35-
getFlagsConfigurationError(configuration, getEffectiveContext(configuration, this.context))
36-
)
34+
const error = toOpenFeatureError(getFlagsConfigurationError(configuration, this.context))
3735
if (error) {
3836
this.events.emit(ProviderEvents.Error, toProviderErrorEvent(error))
3937
return
@@ -47,25 +45,15 @@ export class DatadogOfflineProvider extends DatadogCoreProvider {
4745

4846
async initialize(context: EvaluationContext = {}): Promise<void> {
4947
this.context = context
50-
const error = toOpenFeatureError(
51-
getFlagsConfigurationError(
52-
this.flagsConfiguration,
53-
getEffectiveContext(this.flagsConfiguration, this.context)
54-
)
55-
)
48+
const error = toOpenFeatureError(getFlagsConfigurationError(this.flagsConfiguration, this.context))
5649
if (error) {
5750
throw error
5851
}
5952
}
6053

6154
onContextChange(_oldContext: EvaluationContext, newContext: EvaluationContext): void {
6255
this.context = newContext
63-
const error = toOpenFeatureError(
64-
getFlagsConfigurationError(
65-
this.flagsConfiguration,
66-
getEffectiveContext(this.flagsConfiguration, this.context)
67-
)
68-
)
56+
const error = toOpenFeatureError(getFlagsConfigurationError(this.flagsConfiguration, this.context))
6957
if (error) {
7058
throw error
7159
}
@@ -78,21 +66,11 @@ export class DatadogOfflineProvider extends DatadogCoreProvider {
7866
context: EvaluationContext,
7967
logger: Logger
8068
): ResolutionDetails<FlagTypeToValue<T>> {
81-
return evaluate(
82-
this.flagsConfiguration,
83-
type,
84-
flagKey,
85-
defaultValue,
86-
getEffectiveContext(this.flagsConfiguration, context),
87-
logger
88-
)
69+
return evaluate(this.flagsConfiguration, type, flagKey, defaultValue, context, logger)
8970
}
9071

9172
private canEvaluateCurrentContext(): boolean {
92-
return !getFlagsConfigurationError(
93-
this.flagsConfiguration,
94-
getEffectiveContext(this.flagsConfiguration, this.context)
95-
)
73+
return !getFlagsConfigurationError(this.flagsConfiguration, this.context)
9674
}
9775
}
9876

@@ -102,19 +80,3 @@ function toOpenFeatureError(error: FlagsConfigurationError | undefined): Error |
10280
if (error.errorCode === 'INVALID_CONTEXT') return new InvalidContextError(error.errorMessage)
10381
return new ProviderNotReadyError(error.errorMessage)
10482
}
105-
106-
function getEffectiveContext(
107-
configuration: FlagsConfiguration | undefined,
108-
context: EvaluationContext
109-
): EvaluationContext {
110-
// An empty OpenFeature context means there is no external override for an offline precomputed configuration.
111-
if (isEmptyContext(context) && configuration.precomputed?.context) {
112-
return configuration.precomputed.context
113-
}
114-
115-
return context
116-
}
117-
118-
function isEmptyContext(context: EvaluationContext): boolean {
119-
return Object.values(context).every((value) => value === undefined)
120-
}

packages/browser/test/openfeature/offline-provider.spec.ts

Lines changed: 23 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -144,28 +144,30 @@ describe('DatadogOfflineProvider', () => {
144144
})
145145
})
146146

147-
it('adopts the context embedded in decoded precomputed configuration when initialized without a context', async () => {
148-
const provider = new DatadogOfflineProvider({ configuration: decodedPrecomputedConfiguration })
147+
it('treats an empty initialization context literally', async () => {
148+
const provider = providerWithConfiguration(decodedPrecomputedConfiguration)
149149

150-
await expect(provider.initialize({})).resolves.toBeUndefined()
150+
await expect(provider.initialize({})).rejects.toBeInstanceOf(InvalidContextError)
151151

152-
expect(provider.resolveStringEvaluation('string-flag', 'default', {}, logger)).toMatchObject({
153-
value: 'red',
154-
variant: 'variation-123',
155-
reason: 'TARGETING_MATCH',
152+
expect(provider.resolveStringEvaluation('string-flag', 'default', {}, logger)).toEqual({
153+
value: 'default',
154+
reason: 'ERROR',
155+
errorCode: 'INVALID_CONTEXT',
156+
errorMessage: 'Precomputed flags configuration does not match the current context',
156157
})
157158
})
158159

159-
it('treats a context containing only undefined values as empty', async () => {
160-
const provider = new DatadogOfflineProvider({ configuration: precomputedConfiguration })
160+
it('does not replace a context containing only undefined values with the embedded context', async () => {
161+
const provider = providerWithConfiguration(precomputedConfiguration)
161162
const emptyContext = { targetingKey: undefined } as unknown as EvaluationContext
162163

163-
await expect(provider.initialize(emptyContext)).resolves.toBeUndefined()
164+
await expect(provider.initialize(emptyContext)).rejects.toBeInstanceOf(InvalidContextError)
164165

165-
expect(provider.resolveStringEvaluation('static-flag', 'default', emptyContext, logger)).toMatchObject({
166-
value: 'static-value',
167-
variant: 'static-variation',
168-
reason: 'TARGETING_MATCH',
166+
expect(provider.resolveStringEvaluation('static-flag', 'default', emptyContext, logger)).toEqual({
167+
value: 'default',
168+
reason: 'ERROR',
169+
errorCode: 'INVALID_CONTEXT',
170+
errorMessage: 'Precomputed flags configuration does not match the current context',
169171
})
170172
})
171173

@@ -284,7 +286,7 @@ describe('DatadogOfflineProvider', () => {
284286
})
285287

286288
it('throws ProviderNotReadyError when initialized without evaluatable configuration', async () => {
287-
const provider = new DatadogOfflineProvider({ configuration: {} })
289+
const provider = new DatadogOfflineProvider()
288290

289291
await expect(provider.initialize({})).rejects.toBeInstanceOf(ProviderNotReadyError)
290292
})
@@ -303,14 +305,15 @@ describe('DatadogOfflineProvider precomputed lifecycle', () => {
303305
OpenFeature.clearHandlers()
304306
})
305307

306-
it('recovers the embedded context after the OpenFeature context is cleared', async () => {
307-
const provider = new DatadogOfflineProvider({ configuration: precomputedConfiguration })
308-
await OpenFeature.setProviderAndWait(provider)
308+
it('treats a cleared OpenFeature context as an empty context', async () => {
309+
const provider = providerWithConfiguration(precomputedConfiguration)
310+
const matchingContext = { targetingKey: 'static-user', plan: 'free' }
311+
await OpenFeature.setProviderAndWait(provider, matchingContext)
309312
const client = OpenFeature.getClient()
310313

311314
expect(client.getStringValue('static-flag', 'default')).toBe('static-value')
312315

313-
await OpenFeature.setContext({ targetingKey: 'other-user', plan: 'free' })
316+
await OpenFeature.clearContext()
314317

315318
const errorHandler = jest.fn()
316319
OpenFeature.addHandler(ProviderEvents.Error, errorHandler)
@@ -321,7 +324,7 @@ describe('DatadogOfflineProvider precomputed lifecycle', () => {
321324
errorCode: 'INVALID_CONTEXT',
322325
})
323326

324-
await OpenFeature.clearContext()
327+
await OpenFeature.setContext(matchingContext)
325328

326329
const readyHandler = jest.fn()
327330
OpenFeature.addHandler(ProviderEvents.Ready, readyHandler)

0 commit comments

Comments
 (0)