Skip to content

Commit 4d5ecfe

Browse files
authored
[Feature Flags] Reevaluate on context change (#251268)
1 parent 217cb82 commit 4d5ecfe

4 files changed

Lines changed: 58 additions & 4 deletions

File tree

‎src/core/packages/feature-flags/browser-internal/src/feature_flags_service.test.ts‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,11 @@ describe('FeatureFlagsService Browser', () => {
295295
addHandlerSpy.mock.calls[0][1]({ flagsChanged: ['my-flag'] });
296296
await expect(firstValueFrom(flag$)).resolves.toEqual(value);
297297
expect(observedValues).toHaveLength(2);
298+
299+
// Reevaluates and emits when the context is changed
300+
await startContract.appendContext({ kind: 'multi', kibana: { key: 'kibana-2' } });
301+
await expect(firstValueFrom(flag$)).resolves.toEqual(value);
302+
expect(observedValues).toHaveLength(3);
298303
});
299304

300305
test('observe a string flag', async () => {
@@ -316,6 +321,11 @@ describe('FeatureFlagsService Browser', () => {
316321
addHandlerSpy.mock.calls[0][1]({ flagsChanged: ['my-flag'] });
317322
await expect(firstValueFrom(flag$)).resolves.toEqual(value);
318323
expect(observedValues).toHaveLength(2);
324+
325+
// Reevaluates and emits when the context is changed
326+
await startContract.appendContext({ kind: 'multi', kibana: { key: 'kibana-2' } });
327+
await expect(firstValueFrom(flag$)).resolves.toEqual(value);
328+
expect(observedValues).toHaveLength(3);
319329
});
320330

321331
test('observe a number flag', async () => {
@@ -337,6 +347,11 @@ describe('FeatureFlagsService Browser', () => {
337347
addHandlerSpy.mock.calls[0][1]({ flagsChanged: ['my-flag'] });
338348
await expect(firstValueFrom(flag$)).resolves.toEqual(value);
339349
expect(observedValues).toHaveLength(2);
350+
351+
// Reevaluates and emits when the context is changed
352+
await startContract.appendContext({ kind: 'multi', kibana: { key: 'kibana-2' } });
353+
await expect(firstValueFrom(flag$)).resolves.toEqual(value);
354+
expect(observedValues).toHaveLength(3);
340355
});
341356

342357
test('with overrides', async () => {

‎src/core/packages/feature-flags/browser-internal/src/feature_flags_service.ts‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import type {
1919
import { apm } from '@elastic/apm-rum';
2020
import { type Client, ClientProviderEvents, OpenFeature } from '@openfeature/web-sdk';
2121
import deepMerge from 'deepmerge';
22-
import { filter, map, startWith, Subject } from 'rxjs';
22+
import { filter, map, merge, startWith, Subject } from 'rxjs';
2323
import { get } from 'lodash';
2424

2525
/**
@@ -40,6 +40,7 @@ export interface FeatureFlagsSetupDeps {
4040
export class FeatureFlagsService {
4141
private readonly featureFlagsClient: Client;
4242
private readonly logger: Logger;
43+
private readonly contextChanged$ = new Subject<void>();
4344
private isProviderReadyPromise?: Promise<void>;
4445
private context: MultiContextEvaluationContext = { kind: 'multi' };
4546
private overrides: Record<string, unknown> = {};
@@ -104,7 +105,12 @@ export class FeatureFlagsService {
104105
}
105106
});
106107
const observeFeatureFlag$ = (flagName: string) =>
107-
featureFlagsChanged$.pipe(
108+
merge(
109+
// Flag changes
110+
featureFlagsChanged$,
111+
// Context changes (we need to reevaluate)
112+
this.contextChanged$.pipe(map(() => [flagName]))
113+
).pipe(
108114
filter((flagNames) => flagNames.includes(flagName)),
109115
startWith([flagName]) // only to emit on the first call
110116
);
@@ -221,5 +227,6 @@ export class FeatureFlagsService {
221227
// Merge the formatted context to append to the global context, and set it in the OpenFeature client.
222228
this.context = deepMerge(this.context, formattedContextToAppend);
223229
await OpenFeature.setContext(this.context);
230+
this.contextChanged$.next();
224231
}
225232
}

‎src/core/packages/feature-flags/server-internal/src/feature_flags_service.test.ts‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,11 @@ describe('FeatureFlagsService Server', () => {
201201
addHandlerSpy.mock.calls[0][1]({ flagsChanged: ['my-flag'] });
202202
await expect(firstValueFrom(flag$)).resolves.toEqual(value);
203203
expect(observedValues).toHaveLength(2);
204+
205+
// Reevaluates and emits when the context is changed
206+
startContract.appendContext({ kind: 'multi', kibana: { key: 'kibana-2' } });
207+
await expect(firstValueFrom(flag$)).resolves.toEqual(value);
208+
expect(observedValues).toHaveLength(3);
204209
});
205210

206211
test('observe a string flag', async () => {
@@ -222,6 +227,11 @@ describe('FeatureFlagsService Server', () => {
222227
addHandlerSpy.mock.calls[0][1]({ flagsChanged: ['my-flag'] });
223228
await expect(firstValueFrom(flag$)).resolves.toEqual(value);
224229
expect(observedValues).toHaveLength(2);
230+
231+
// Reevaluates and emits when the context is changed
232+
startContract.appendContext({ kind: 'multi', kibana: { key: 'kibana-2' } });
233+
await expect(firstValueFrom(flag$)).resolves.toEqual(value);
234+
expect(observedValues).toHaveLength(3);
225235
});
226236

227237
test('observe a number flag', async () => {
@@ -243,6 +253,11 @@ describe('FeatureFlagsService Server', () => {
243253
addHandlerSpy.mock.calls[0][1]({ flagsChanged: ['my-flag'] });
244254
await expect(firstValueFrom(flag$)).resolves.toEqual(value);
245255
expect(observedValues).toHaveLength(2);
256+
257+
// Reevaluates and emits when the context is changed
258+
startContract.appendContext({ kind: 'multi', kibana: { key: 'kibana-2' } });
259+
await expect(firstValueFrom(flag$)).resolves.toEqual(value);
260+
expect(observedValues).toHaveLength(3);
246261
});
247262

248263
test('with overrides', async () => {

‎src/core/packages/feature-flags/server-internal/src/feature_flags_service.ts‎

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,17 @@ import {
2424
NOOP_PROVIDER,
2525
} from '@openfeature/server-sdk';
2626
import deepMerge from 'deepmerge';
27-
import { filter, switchMap, startWith, Subject, BehaviorSubject, pairwise, takeUntil } from 'rxjs';
27+
import {
28+
filter,
29+
switchMap,
30+
startWith,
31+
Subject,
32+
BehaviorSubject,
33+
pairwise,
34+
takeUntil,
35+
merge,
36+
map,
37+
} from 'rxjs';
2838
import { get } from 'lodash';
2939
import type { InitialFeatureFlagsGetter } from '@kbn/core-feature-flags-server/src/contracts';
3040
import { createOpenFeatureLogger } from './create_open_feature_logger';
@@ -56,6 +66,7 @@ export class FeatureFlagsService {
5666
private readonly logger: Logger;
5767
private readonly stop$ = new Subject<void>();
5868
private readonly overrides$ = new BehaviorSubject<Record<string, unknown>>({});
69+
private readonly contextChanged$ = new Subject<void>();
5970
private context: MultiContextEvaluationContext = { kind: 'multi' };
6071
private initialFeatureFlagsGetter: InitialFeatureFlagsGetter = async () => ({});
6172

@@ -117,7 +128,12 @@ export class FeatureFlagsService {
117128
featureFlagsChanged$.next(keys);
118129
});
119130
const observeFeatureFlag$ = (flagName: string) =>
120-
featureFlagsChanged$.pipe(
131+
merge(
132+
// Flag changes
133+
featureFlagsChanged$,
134+
// Context changes (we need to reevaluate)
135+
this.contextChanged$.pipe(map(() => [flagName]))
136+
).pipe(
121137
filter((flagNames) => flagNames.includes(flagName)),
122138
startWith([flagName]), // only to emit on the first call
123139
takeUntil(this.stop$) // stop the observable when the service stops
@@ -221,5 +237,6 @@ export class FeatureFlagsService {
221237
// Merge the formatted context to append to the global context, and set it in the OpenFeature client.
222238
this.context = deepMerge(this.context, formattedContextToAppend);
223239
OpenFeature.setContext(this.context);
240+
this.contextChanged$.next();
224241
}
225242
}

0 commit comments

Comments
 (0)