Skip to content

Commit 9a855bf

Browse files
committed
chore(flagsmith): implemented-context-evaluation-in-provider
Signed-off-by: wadii <wadii.zaim@flagsmith.com>
1 parent 29af6ad commit 9a855bf

File tree

5 files changed

+139
-59
lines changed

5 files changed

+139
-59
lines changed

libs/providers/flagsmith-client/package-lock.json

Lines changed: 5 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

libs/providers/flagsmith-client/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
},
1111
"peerDependencies": {
1212
"@openfeature/web-sdk": "^1.0.0",
13-
"flagsmith": "^4.1.4"
13+
"flagsmith": "^9.3.0"
1414
},
1515
"dependencies": {}
1616
}

libs/providers/flagsmith-client/src/lib/flagsmith-client-provider.spec.ts

Lines changed: 44 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { FlagsmithClientProvider } from './flagsmith-client-provider';
22
import {
3+
cacheConfig,
34
defaultConfig,
45
defaultState,
6+
defaultStateWithoutEnvironment,
57
exampleBooleanFlag,
68
exampleBooleanFlagName,
79
exampleFloatFlagName,
@@ -21,19 +23,22 @@ const logger = {
2123
warn: jest.fn(),
2224
};
2325

26+
const CACHE_KEY = 'FLAGSMITH_CACHE_DB';
27+
2428
describe('FlagsmithProvider', () => {
2529
beforeEach(async () => {
2630
// Clear all instances and calls to constructor and all methods of mock logger
2731
jest.clearAllMocks();
28-
await Promise.all([OpenFeature.clearProviders(), OpenFeature.clearContexts()]);
32+
await OpenFeature.clearContexts();
33+
await OpenFeature.clearProviders();
2934
});
3035

3136
describe('constructor', () => {
3237
it('should initialize the environment ID', async () => {
3338
const config = defaultConfig();
3439
const provider = new FlagsmithClientProvider(config);
3540
await OpenFeature.setProviderAndWait(provider);
36-
expect(provider.flagsmithClient.getState().environmentID).toEqual(config.environmentID);
41+
expect(provider.flagsmithClient.getContext().environment?.apiKey).toEqual(config.environmentID);
3742
});
3843

3944
it('calls onChange', async () => {
@@ -44,7 +49,7 @@ describe('FlagsmithProvider', () => {
4449
expect(onChange).toHaveBeenCalledTimes(1);
4550
await OpenFeature.setContext({ targetingKey: 'test' });
4651
expect(onChange).toHaveBeenCalledTimes(2);
47-
expect(provider.flagsmithClient.getState().environmentID).toEqual(config.environmentID);
52+
expect(provider.flagsmithClient.getContext().environment?.apiKey).toEqual(config.environmentID);
4853
});
4954

5055
it('should allow a custom instance of Flagsmith to be used', async () => {
@@ -56,12 +61,23 @@ describe('FlagsmithProvider', () => {
5661
it('should initialize with SSR state and evaluate synchronously if provided', async () => {
5762
const config = defaultConfig();
5863
const state = {
59-
...defaultState,
64+
...defaultStateWithoutEnvironment,
6065
identity: 'test',
61-
traits: { example: 1 },
66+
traits: undefined,
6267
evaluationEvent: null,
63-
ts: null,
68+
ts: undefined,
69+
evaluationContext: {
70+
identity: {
71+
identifier: 'test',
72+
traits: {
73+
test: {
74+
value: '1',
75+
},
76+
},
77+
},
78+
},
6479
};
80+
6581
const provider = new FlagsmithClientProvider({
6682
logger,
6783
...config,
@@ -80,12 +96,13 @@ describe('FlagsmithProvider', () => {
8096
const config = defaultConfig();
8197
const provider = new FlagsmithClientProvider({
8298
...config,
99+
...cacheConfig,
100+
defaultFlags: defaultState.flags,
83101
logger,
84-
cacheFlags: true,
85102
preventFetch: true,
86103
});
87104
await config.AsyncStorage.setItem(
88-
'BULLET_TRAIN_DB',
105+
CACHE_KEY,
89106
JSON.stringify({
90107
...defaultState,
91108
}),
@@ -100,11 +117,11 @@ describe('FlagsmithProvider', () => {
100117
const config = defaultConfig();
101118
const provider = new FlagsmithClientProvider({
102119
...config,
120+
...cacheConfig,
103121
logger,
104-
cacheFlags: true,
105122
});
106123
await config.AsyncStorage.setItem(
107-
'BULLET_TRAIN_DB',
124+
CACHE_KEY,
108125
JSON.stringify({
109126
...defaultState,
110127
}),
@@ -122,11 +139,11 @@ describe('FlagsmithProvider', () => {
122139
const config = defaultConfig();
123140
const provider = new FlagsmithClientProvider({
124141
...config,
142+
...cacheConfig,
125143
logger,
126-
cacheFlags: true,
127144
});
128145
await config.AsyncStorage.setItem(
129-
'BULLET_TRAIN_DB',
146+
CACHE_KEY,
130147
JSON.stringify({
131148
...defaultState,
132149
flags: {
@@ -169,11 +186,12 @@ describe('FlagsmithProvider', () => {
169186
const config = defaultConfig();
170187
const provider = new FlagsmithClientProvider({
171188
...config,
189+
...cacheConfig,
172190
logger,
173191
defaultFlags: defaultState.flags,
174192
});
175193
await config.AsyncStorage.setItem(
176-
'BULLET_TRAIN_DB',
194+
CACHE_KEY,
177195
JSON.stringify({
178196
...defaultState,
179197
}),
@@ -192,10 +210,10 @@ describe('FlagsmithProvider', () => {
192210
const provider = new FlagsmithClientProvider({
193211
...config,
194212
logger,
195-
cacheFlags: true,
213+
...cacheConfig,
196214
});
197215
await config.AsyncStorage.setItem(
198-
'BULLET_TRAIN_DB',
216+
CACHE_KEY,
199217
JSON.stringify({
200218
...defaultState,
201219
flags: {
@@ -303,7 +321,7 @@ describe('FlagsmithProvider', () => {
303321
});
304322
expect(errorHandler).toHaveBeenCalledTimes(1);
305323
expect(errorHandler).toHaveBeenCalledWith(
306-
expect.objectContaining({ message: 'Please specify a environment id' }),
324+
expect.objectContaining({ message: 'Please provide `evaluationContext.environment` with non-empty `apiKey`' }),
307325
);
308326
});
309327
it('should call the stale handler when context changed', async () => {
@@ -325,11 +343,11 @@ describe('FlagsmithProvider', () => {
325343
...config,
326344
});
327345
await OpenFeature.setProviderAndWait(provider);
328-
expect(provider.flagsmithClient.getState().identity).toEqual(undefined);
346+
expect(provider.flagsmithClient.getContext().identity?.identifier).toEqual(undefined);
329347
await OpenFeature.setContext({ targetingKey });
330-
expect(provider.flagsmithClient.getState().identity).toEqual(targetingKey);
348+
expect(provider.flagsmithClient.getContext().identity?.identifier).toEqual(targetingKey);
331349
await OpenFeature.setContext({});
332-
expect(provider.flagsmithClient.getState().identity).toEqual(null);
350+
expect(provider.flagsmithClient.getContext().identity).toEqual(null);
333351
expect(config.fetch).toHaveBeenNthCalledWith(
334352
1,
335353
`${provider.flagsmithClient.getState().api}flags/`,
@@ -339,9 +357,9 @@ describe('FlagsmithProvider', () => {
339357
);
340358
expect(config.fetch).toHaveBeenNthCalledWith(
341359
2,
342-
`${provider.flagsmithClient.getState().api}identities/`,
360+
`${provider.flagsmithClient.getState().api}identities/?identifier=test`,
343361
expect.objectContaining({
344-
body: '{"identifier":"test","traits":[]}',
362+
body: undefined,
345363
}),
346364
);
347365
expect(config.fetch).toHaveBeenNthCalledWith(
@@ -360,7 +378,7 @@ describe('FlagsmithProvider', () => {
360378
const provider = new FlagsmithClientProvider(config);
361379
await OpenFeature.setContext({ targetingKey, traits });
362380
await OpenFeature.setProviderAndWait(provider);
363-
expect(provider.flagsmithClient.getState().identity).toEqual(targetingKey);
381+
expect(provider.flagsmithClient.getContext().identity?.identifier).toEqual(targetingKey);
364382
expect(config.fetch).toHaveBeenCalledTimes(1);
365383
expect(config.fetch).toHaveBeenCalledWith(
366384
`${provider.flagsmithClient.getState().api}identities/`,
@@ -390,7 +408,7 @@ describe('FlagsmithProvider', () => {
390408
const provider = new FlagsmithClientProvider(config);
391409
await OpenFeature.setContext({ targetingKey, traits });
392410
await OpenFeature.setProviderAndWait(provider);
393-
expect(provider.flagsmithClient.getState().identity).toEqual(targetingKey);
411+
expect(provider.flagsmithClient.getContext().identity?.identifier).toEqual(targetingKey);
394412
expect(config.fetch).toHaveBeenCalledTimes(1);
395413
expect(config.fetch).toHaveBeenCalledWith(
396414
`${provider.flagsmithClient.getState().api}identities/`,
@@ -430,7 +448,9 @@ describe('FlagsmithProvider', () => {
430448
fetch: getFetchErrorMock(),
431449
environmentID: '',
432450
});
433-
await expect(OpenFeature.setProviderAndWait(provider)).rejects.toThrow('Please specify a environment id');
451+
await expect(OpenFeature.setProviderAndWait(provider)).rejects.toThrow(
452+
'Please provide `evaluationContext.environment` with non-empty `apiKey`',
453+
);
434454
});
435455
});
436456
});

libs/providers/flagsmith-client/src/lib/flagsmith-client-provider.ts

Lines changed: 53 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,12 @@ import type {
1010
} from '@openfeature/web-sdk';
1111
import { OpenFeatureEventEmitter, ProviderEvents, TypeMismatchError } from '@openfeature/web-sdk';
1212
import { createFlagsmithInstance } from 'flagsmith';
13-
import type { IFlagsmith, IInitConfig, IState } from 'flagsmith/types';
13+
import type { ClientEvaluationContext, IFlagsmith, IInitConfig, IState, ITraits } from 'flagsmith/types';
1414
import type { FlagType } from './type-factory';
1515
import { typeFactory } from './type-factory';
1616

17+
type OpenFeatureContext = EvaluationContext & Partial<IState>;
18+
1719
export class FlagsmithClientProvider implements Provider {
1820
readonly metadata: ProviderMetadata = {
1921
name: FlagsmithClientProvider.name,
@@ -39,31 +41,32 @@ export class FlagsmithClientProvider implements Provider {
3941
this._config = config;
4042
}
4143

42-
async initialize(context?: EvaluationContext & Partial<IState>) {
44+
async initialize(context?: OpenFeatureContext) {
4345
const identity = context?.targetingKey;
46+
const evaluationContext: ClientEvaluationContext = this.mapContextToEvaluationContext(
47+
context,
48+
this._config.environmentID,
49+
);
4450

4551
if (this._client?.initialised) {
46-
//Already initialised, set the state based on the new context, allow certain context props to be optional
47-
const defaultState = { ...this._client.getState(), identity: undefined, traits: {} };
48-
const isLogout = !!this._client.identity && !identity;
49-
this._client.identity = identity;
50-
this._client.setState({
51-
...defaultState,
52-
...(context || {}),
53-
});
52+
const isLogout = !!this._client.getContext().identity && !identity;
5453
this.events.emit(ProviderEvents.Stale, { message: 'context has changed' });
55-
return isLogout ? this._client.logout() : this._client.getFlags();
54+
55+
return isLogout ? this._client.logout() : this._client.setContext(evaluationContext);
5656
}
5757

5858
const serverState = this._config.state;
5959
if (serverState) {
6060
this._client.setState(serverState);
6161
this.events.emit(ProviderEvents.Ready, { message: 'flags provided by SSR state' });
6262
}
63+
if (!this._config.environmentID) {
64+
this.events.emit(ProviderEvents.Stale, { message: 'environmentID is required' });
65+
}
66+
6367
return this._client.init({
6468
...this._config,
65-
...context,
66-
identity,
69+
evaluationContext,
6770
onChange: (previousFlags, params, loadingState) => {
6871
const eventMeta = {
6972
metadata: this.getMetadata(),
@@ -84,7 +87,7 @@ export class FlagsmithClientProvider implements Provider {
8487
});
8588
}
8689

87-
onContextChange(oldContext: EvaluationContext, newContext: EvaluationContext & Partial<IState>) {
90+
onContextChange(oldContext: OpenFeatureContext, newContext: OpenFeatureContext) {
8891
this.events.emit(ProviderEvents.Stale, { message: 'Context Changed' });
8992
return this.initialize(newContext);
9093
}
@@ -111,11 +114,45 @@ export class FlagsmithClientProvider implements Provider {
111114
*/
112115
private getMetadata() {
113116
return {
114-
targetingKey: this._client.identity || '',
117+
targetingKey: this._client.getContext()?.identity?.identifier || '',
115118
...(this._client.getAllTraits() || {}),
116119
};
117120
}
118121

122+
/**
123+
* Map the Open Feature context to the Flagsmith evaluation context
124+
* @private
125+
*/
126+
private mapContextToEvaluationContext(context?: OpenFeatureContext, environmentID?: string) {
127+
if (!context) {
128+
return {
129+
environment: {
130+
apiKey: environmentID,
131+
},
132+
};
133+
}
134+
135+
const identity = context?.targetingKey;
136+
const traits = (context?.['traits'] as ITraits) || {};
137+
const hasTraits = Object.keys(traits).length > 0;
138+
const hasIdentifier = !!identity;
139+
140+
const evaluationContext: ClientEvaluationContext = {
141+
environment: {
142+
apiKey: this._config.environmentID,
143+
},
144+
identity:
145+
hasIdentifier || hasTraits
146+
? {
147+
...(hasIdentifier && { identifier: identity }),
148+
...(hasTraits && { traits }),
149+
}
150+
: undefined,
151+
};
152+
153+
return evaluationContext;
154+
}
155+
119156
/**
120157
* Based on Flagsmith's loading state, determine the Open Feature resolution reason
121158
* @private
@@ -128,6 +165,7 @@ export class FlagsmithClientProvider implements Provider {
128165
if (typeof value !== 'undefined' && typeof value !== type) {
129166
throw new TypeMismatchError(`flag key ${flagKey} is not of type ${type}`);
130167
}
168+
131169
return {
132170
value: (typeof value !== type ? defaultValue : value) as T,
133171
reason: this.parseReason(value),

0 commit comments

Comments
 (0)