Skip to content

Commit 7e6c1c6

Browse files
beeme1mrtoddbaert
andauthored
feat: set context during provider init on web (#919)
## This PR - overloads the set provider methods to support defining context in the web SDK - updates the web sdk readme ## Related Issues Fixes #748 ## Notes I decided to only support setting context in the web SDK because it is less valuable on the server and the expected behavior was less clear due to `domains`. The behavior may need to be spec'd out. An issue in the spec repo has been created. open-feature/spec#219 --------- Signed-off-by: Michael Beemer <[email protected]> Co-authored-by: Todd Baert <[email protected]>
1 parent c6d0b5d commit 7e6c1c6

File tree

9 files changed

+338
-64
lines changed

9 files changed

+338
-64
lines changed

.vscode/settings.json

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"cSpell.words": [
3+
"domainless"
4+
]
5+
}

packages/client/README.md

+27-2
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ To register a provider and ensure it is ready before further actions are taken,
121121

122122
```ts
123123
await OpenFeature.setProviderAndWait(new MyProvider());
124-
```
124+
```
125125

126126
#### Synchronous
127127

@@ -158,9 +158,16 @@ Sometimes, the value of a flag must consider some dynamic criteria about the app
158158
In OpenFeature, we refer to this as [targeting](https://openfeature.dev/specification/glossary#targeting).
159159
If the flag management system you're using supports targeting, you can provide the input data using the [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context).
160160

161+
```ts
162+
// Sets global context during provider registration
163+
await OpenFeature.setProvider(new MyProvider(), { origin: document.location.host });
164+
```
165+
166+
Change context after the provider has been registered using `setContext`.
167+
161168
```ts
162169
// Set a value to the global context
163-
await OpenFeature.setContext({ origin: document.location.host });
170+
await OpenFeature.setContext({ targetingKey: localStorage.getItem("targetingKey") });
164171
```
165172

166173
Context is global and setting it is `async`.
@@ -233,6 +240,24 @@ const domainScopedClient = OpenFeature.getClient("my-domain");
233240
Domains can be defined on a provider during registration.
234241
For more details, please refer to the [providers](#providers) section.
235242

243+
#### Manage evaluation context for domains
244+
245+
By default, domain-scoped clients use the global context.
246+
This can be overridden by explicitly setting context when registering the provider or by references the domain when updating context:
247+
248+
```ts
249+
OpenFeature.setProvider("my-domain", new NewCachedProvider(), { targetingKey: localStorage.getItem("targetingKey") });
250+
```
251+
252+
To change context after the provider has been registered, use `setContext` with a name:
253+
254+
```ts
255+
await OpenFeature.setContext("my-domain", { targetingKey: localStorage.getItem("targetingKey") })
256+
```
257+
258+
Once context has been defined for a named client, it will override the global context for all clients using the associated provider.
259+
Context can be cleared using for a named provider using `OpenFeature.clearContext("my-domain")` or call `OpenFeature.clearContexts()` to reset all context.
260+
236261
### Eventing
237262

238263
Events allow you to react to state changes in the provider or underlying flag management system, such as flag definition changes, provider readiness, or error conditions.

packages/client/src/open-feature.ts

+135-1
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,140 @@ export class OpenFeatureAPI
6969
return this._domainScopedProviders.get(domain)?.status ?? this._defaultProvider.status;
7070
}
7171

72+
/**
73+
* Sets the default provider for flag evaluations and returns a promise that resolves when the provider is ready.
74+
* This provider will be used by domainless clients and clients associated with domains to which no provider is bound.
75+
* Setting a provider supersedes the current provider used in new and existing unbound clients.
76+
* @param {Provider} provider The provider responsible for flag evaluations.
77+
* @returns {Promise<void>}
78+
* @throws Uncaught exceptions thrown by the provider during initialization.
79+
*/
80+
setProviderAndWait(provider: Provider): Promise<void>;
81+
/**
82+
* Sets the default provider for flag evaluations and returns a promise that resolves when the provider is ready.
83+
* This provider will be used by domainless clients and clients associated with domains to which no provider is bound.
84+
* Setting a provider supersedes the current provider used in new and existing unbound clients.
85+
* @param {Provider} provider The provider responsible for flag evaluations.
86+
* @param {EvaluationContext} context The evaluation context to use for flag evaluations.
87+
* @returns {Promise<void>}
88+
* @throws Uncaught exceptions thrown by the provider during initialization.
89+
*/
90+
setProviderAndWait(provider: Provider, context: EvaluationContext): Promise<void>;
91+
/**
92+
* Sets the provider that OpenFeature will use for flag evaluations on clients bound to the same domain.
93+
* A promise is returned that resolves when the provider is ready.
94+
* Setting a provider supersedes the current provider used in new and existing clients bound to the same domain.
95+
* @param {string} domain The name to identify the client
96+
* @param {Provider} provider The provider responsible for flag evaluations.
97+
* @returns {Promise<void>}
98+
* @throws Uncaught exceptions thrown by the provider during initialization.
99+
*/
100+
setProviderAndWait(domain: string, provider: Provider): Promise<void>;
101+
/**
102+
* Sets the provider that OpenFeature will use for flag evaluations on clients bound to the same domain.
103+
* A promise is returned that resolves when the provider is ready.
104+
* Setting a provider supersedes the current provider used in new and existing clients bound to the same domain.
105+
* @param {string} domain The name to identify the client
106+
* @param {Provider} provider The provider responsible for flag evaluations.
107+
* @param {EvaluationContext} context The evaluation context to use for flag evaluations.
108+
* @returns {Promise<void>}
109+
* @throws Uncaught exceptions thrown by the provider during initialization.
110+
*/
111+
setProviderAndWait(domain: string, provider: Provider, context: EvaluationContext): Promise<void>;
112+
async setProviderAndWait(
113+
clientOrProvider?: string | Provider,
114+
providerContextOrUndefined?: Provider | EvaluationContext,
115+
contextOrUndefined?: EvaluationContext,
116+
): Promise<void> {
117+
const domain = stringOrUndefined(clientOrProvider);
118+
const provider = domain
119+
? objectOrUndefined<Provider>(providerContextOrUndefined)
120+
: objectOrUndefined<Provider>(clientOrProvider);
121+
const context = domain
122+
? objectOrUndefined<EvaluationContext>(contextOrUndefined)
123+
: objectOrUndefined<EvaluationContext>(providerContextOrUndefined);
124+
125+
if (context) {
126+
// synonymously setting context prior to provider initialization.
127+
// No context change event will be emitted.
128+
if (domain) {
129+
this._domainScopedContext.set(domain, context);
130+
} else {
131+
this._context = context;
132+
}
133+
}
134+
135+
await this.setAwaitableProvider(domain, provider);
136+
}
137+
138+
/**
139+
* Sets the default provider for flag evaluations.
140+
* This provider will be used by domainless clients and clients associated with domains to which no provider is bound.
141+
* Setting a provider supersedes the current provider used in new and existing unbound clients.
142+
* @param {Provider} provider The provider responsible for flag evaluations.
143+
* @returns {this} OpenFeature API
144+
*/
145+
setProvider(provider: Provider): this;
146+
/**
147+
* Sets the default provider and evaluation context for flag evaluations.
148+
* This provider will be used by domainless clients and clients associated with domains to which no provider is bound.
149+
* Setting a provider supersedes the current provider used in new and existing unbound clients.
150+
* @param {Provider} provider The provider responsible for flag evaluations.
151+
* @param context {EvaluationContext} The evaluation context to use for flag evaluations.
152+
* @returns {this} OpenFeature API
153+
*/
154+
setProvider(provider: Provider, context: EvaluationContext): this;
155+
/**
156+
* Sets the provider for flag evaluations of providers with the given name.
157+
* Setting a provider supersedes the current provider used in new and existing clients bound to the same domain.
158+
* @param {string} domain The name to identify the client
159+
* @param {Provider} provider The provider responsible for flag evaluations.
160+
* @returns {this} OpenFeature API
161+
*/
162+
setProvider(domain: string, provider: Provider): this;
163+
/**
164+
* Sets the provider and evaluation context flag evaluations of providers with the given name.
165+
* Setting a provider supersedes the current provider used in new and existing clients bound to the same domain.
166+
* @param {string} domain The name to identify the client
167+
* @param {Provider} provider The provider responsible for flag evaluations.
168+
* @param context {EvaluationContext} The evaluation context to use for flag evaluations.
169+
* @returns {this} OpenFeature API
170+
*/
171+
setProvider(domain: string, provider: Provider, context: EvaluationContext): this;
172+
setProvider(
173+
domainOrProvider?: string | Provider,
174+
providerContextOrUndefined?: Provider | EvaluationContext,
175+
contextOrUndefined?: EvaluationContext,
176+
): this {
177+
const domain = stringOrUndefined(domainOrProvider);
178+
const provider = domain
179+
? objectOrUndefined<Provider>(providerContextOrUndefined)
180+
: objectOrUndefined<Provider>(domainOrProvider);
181+
const context = domain
182+
? objectOrUndefined<EvaluationContext>(contextOrUndefined)
183+
: objectOrUndefined<EvaluationContext>(providerContextOrUndefined);
184+
185+
if (context) {
186+
// synonymously setting context prior to provider initialization.
187+
// No context change event will be emitted.
188+
if (domain) {
189+
this._domainScopedContext.set(domain, context);
190+
} else {
191+
this._context = context;
192+
}
193+
}
194+
195+
const maybePromise = this.setAwaitableProvider(domain, provider);
196+
197+
// The setProvider method doesn't return a promise so we need to catch and
198+
// log any errors that occur during provider initialization to avoid having
199+
// an unhandled promise rejection.
200+
Promise.resolve(maybePromise).catch((err) => {
201+
this._logger.error('Error during provider initialization:', err);
202+
});
203+
return this;
204+
}
205+
72206
/**
73207
* Sets the evaluation context globally.
74208
* This will be used by all providers that have not bound to a domain.
@@ -135,7 +269,7 @@ export class OpenFeatureAPI
135269
* @param {string} domain An identifier which logically binds clients with providers
136270
* @returns {EvaluationContext} Evaluation context
137271
*/
138-
getContext(domain?: string): EvaluationContext;
272+
getContext(domain?: string | undefined): EvaluationContext;
139273
getContext(domainOrUndefined?: string): EvaluationContext {
140274
const domain = stringOrUndefined(domainOrUndefined);
141275
if (domain) {

packages/client/test/evaluation-context.spec.ts

+42-1
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
import { EvaluationContext, JsonValue, OpenFeature, Provider, ProviderMetadata, ResolutionDetails } from '../src';
22

3+
const initializeMock = jest.fn();
4+
35
class MockProvider implements Provider {
46
readonly metadata: ProviderMetadata;
57

68
constructor(options?: { name?: string }) {
79
this.metadata = { name: options?.name ?? 'mock-provider' };
810
}
911

12+
initialize = initializeMock;
13+
1014
// eslint-disable-next-line @typescript-eslint/no-unused-vars
1115
onContextChange(oldContext: EvaluationContext, newContext: EvaluationContext): Promise<void> {
1216
return Promise.resolve();
@@ -15,7 +19,7 @@ class MockProvider implements Provider {
1519
// eslint-disable-next-line @typescript-eslint/no-unused-vars
1620
resolveBooleanEvaluation = jest.fn((flagKey: string, defaultValue: boolean, context: EvaluationContext) => {
1721
return {
18-
value: true
22+
value: true,
1923
};
2024
});
2125

@@ -35,6 +39,7 @@ class MockProvider implements Provider {
3539
describe('Evaluation Context', () => {
3640
afterEach(async () => {
3741
await OpenFeature.clearContexts();
42+
jest.clearAllMocks();
3843
});
3944

4045
describe('Requirement 3.2.2', () => {
@@ -59,6 +64,42 @@ describe('Evaluation Context', () => {
5964
expect(OpenFeature.getContext('invalid')).toEqual(defaultContext);
6065
});
6166

67+
describe('Set context during provider registration', () => {
68+
it('should set the context for the default provider', () => {
69+
const context: EvaluationContext = { property1: false };
70+
const provider = new MockProvider();
71+
OpenFeature.setProvider(provider, context);
72+
expect(OpenFeature.getContext()).toEqual(context);
73+
});
74+
75+
it('should set the context for a domain', async () => {
76+
const context: EvaluationContext = { property1: false };
77+
const domain = 'test';
78+
const provider = new MockProvider({ name: domain });
79+
OpenFeature.setProvider(domain, provider, context);
80+
expect(OpenFeature.getContext()).toEqual({});
81+
expect(OpenFeature.getContext(domain)).toEqual(context);
82+
});
83+
84+
it('should set the context for the default provider prior to initialization', async () => {
85+
const context: EvaluationContext = { property1: false };
86+
const provider = new MockProvider();
87+
await OpenFeature.setProviderAndWait(provider, context);
88+
expect(initializeMock).toHaveBeenCalledWith(context);
89+
expect(OpenFeature.getContext()).toEqual(context);
90+
});
91+
92+
it('should set the context for a domain prior to initialization', async () => {
93+
const context: EvaluationContext = { property1: false };
94+
const domain = 'test';
95+
const provider = new MockProvider({ name: domain });
96+
await OpenFeature.setProviderAndWait(domain, provider, context);
97+
expect(OpenFeature.getContext()).toEqual({});
98+
expect(OpenFeature.getContext(domain)).toEqual(context);
99+
expect(initializeMock).toHaveBeenCalledWith(context);
100+
});
101+
});
102+
62103
describe('Context Management', () => {
63104
it('should reset global context', async () => {
64105
const globalContext: EvaluationContext = { scope: 'global' };

packages/react/test/options.spec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ describe('normalizeOptions', () => {
2727
});
2828
});
2929

30-
// we fallback the more specific suspense props (`ssuspendUntilReady` and `suspendWhileReconciling`) to `suspend`
30+
// we fallback the more specific suspense props (`suspendUntilReady` and `suspendWhileReconciling`) to `suspend`
3131
describe('suspend fallback', () => {
3232
it('should fallback to true suspend value', () => {
3333
const normalized = normalizeOptions({

packages/server/README.md

+3
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,9 @@ const requestContext = {
161161
const boolValue = await client.getBooleanValue('some-flag', false, requestContext);
162162
```
163163

164+
Context is merged by the SDK before a flag evaluation occurs.
165+
The merge order is defined [here](https://openfeature.dev/specification/sections/evaluation-context#requirement-323) in the OpenFeature specification.
166+
164167
### Hooks
165168

166169
[Hooks](https://openfeature.dev/docs/reference/concepts/hooks) allow for custom logic to be added at well-defined points of the flag evaluation life-cycle.

packages/server/src/open-feature.ts

+62
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,68 @@ export class OpenFeatureAPI
7373
return this._domainScopedProviders.get(domain)?.status ?? this._defaultProvider.status;
7474
}
7575

76+
/**
77+
* Sets the default provider for flag evaluations and returns a promise that resolves when the provider is ready.
78+
* This provider will be used by domainless clients and clients associated with domains to which no provider is bound.
79+
* Setting a provider supersedes the current provider used in new and existing unbound clients.
80+
* @param {Provider} provider The provider responsible for flag evaluations.
81+
* @returns {Promise<void>}
82+
* @throws Uncaught exceptions thrown by the provider during initialization.
83+
*/
84+
setProviderAndWait(provider: Provider): Promise<void>;
85+
/**
86+
* Sets the provider that OpenFeature will use for flag evaluations on clients bound to the same domain.
87+
* A promise is returned that resolves when the provider is ready.
88+
* Setting a provider supersedes the current provider used in new and existing clients bound to the same domain.
89+
* @param {string} domain The name to identify the client
90+
* @param {Provider} provider The provider responsible for flag evaluations.
91+
* @returns {Promise<void>}
92+
* @throws Uncaught exceptions thrown by the provider during initialization.
93+
*/
94+
setProviderAndWait(domain: string, provider: Provider): Promise<void>;
95+
async setProviderAndWait(domainOrProvider?: string | Provider, providerOrUndefined?: Provider): Promise<void> {
96+
const domain = stringOrUndefined(domainOrProvider);
97+
const provider = domain
98+
? objectOrUndefined<Provider>(providerOrUndefined)
99+
: objectOrUndefined<Provider>(domainOrProvider);
100+
101+
await this.setAwaitableProvider(domain, provider);
102+
}
103+
104+
/**
105+
* Sets the default provider for flag evaluations.
106+
* This provider will be used by domainless clients and clients associated with domains to which no provider is bound.
107+
* Setting a provider supersedes the current provider used in new and existing unbound clients.
108+
* @param {Provider} provider The provider responsible for flag evaluations.
109+
* @returns {this} OpenFeature API
110+
*/
111+
setProvider(provider: Provider): this;
112+
/**
113+
* Sets the provider for flag evaluations of providers with the given name.
114+
* Setting a provider supersedes the current provider used in new and existing clients bound to the same domain.
115+
* @param {string} domain The name to identify the client
116+
* @param {Provider} provider The provider responsible for flag evaluations.
117+
* @returns {this} OpenFeature API
118+
*/
119+
setProvider(domain: string, provider: Provider): this;
120+
setProvider(clientOrProvider?: string | Provider, providerOrUndefined?: Provider): this {
121+
const domain = stringOrUndefined(clientOrProvider);
122+
const provider = domain
123+
? objectOrUndefined<Provider>(providerOrUndefined)
124+
: objectOrUndefined<Provider>(clientOrProvider);
125+
126+
const maybePromise = this.setAwaitableProvider(domain, provider);
127+
128+
// The setProvider method doesn't return a promise so we need to catch and
129+
// log any errors that occur during provider initialization to avoid having
130+
// an unhandled promise rejection.
131+
Promise.resolve(maybePromise).catch((err) => {
132+
this._logger.error('Error during provider initialization:', err);
133+
});
134+
135+
return this;
136+
}
137+
76138
setContext(context: EvaluationContext): this {
77139
this._context = context;
78140
return this;

0 commit comments

Comments
 (0)