From 8c84e0149a5621c6fcb95f2cfdbd6112f3540191 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Tue, 15 Oct 2024 13:06:38 -0700 Subject: [PATCH] feat: Add support for client-side prerequisite events. (#606) This PR contains the server-side implementation for allFlagsState evaluation for bootstrap, server-side LDEvaluationDetail, client-side LDEvaluationDetail, and client-side events for prerequisites. This version only includes direct pre-requisites, and the client-side evaluation uses variation methods versus directly sending events. BEGIN_COMMIT_OVERRIDE feat: Add support for client-side prerequisite events. feat: Add support for prerequisite details to evaluation detail. feat: Add prerequisite information to server-side allFlagsState. END_COMMIT_OVERRIDE SDK-686 SDK-682 --- contract-tests/index.js | 1 + .../entity/src/TestHarnessWebSocket.ts | 1 + packages/sdk/browser/rollup.config.js | 12 +- .../LDCLientImpl.inspections.test.ts | 2 + .../__tests__/LDClientImpl.events.test.ts | 41 ++++ .../__tests__/LDClientImpl.storage.test.ts | 10 + .../sdk-client/__tests__/LDClientImpl.test.ts | 2 + .../__tests__/LDClientImpl.timeout.test.ts | 4 + .../__tests__/evaluation/mockResponse.json | 19 ++ .../shared/sdk-client/src/LDClientImpl.ts | 6 +- .../src/evaluation/evaluationDetail.ts | 3 +- packages/shared/sdk-client/src/types/index.ts | 1 + .../__tests__/LDClient.allFlags.test.ts | 54 ++++++ .../evaluation/Evaluator.prerequisite.test.ts | 181 ++++++++++++++++++ .../sdk-server/src/FlagsStateBuilder.ts | 5 + .../shared/sdk-server/src/LDClientImpl.ts | 1 + .../sdk-server/src/evaluation/EvalResult.ts | 1 + .../sdk-server/src/evaluation/Evaluator.ts | 37 ++-- 18 files changed, 352 insertions(+), 29 deletions(-) create mode 100644 packages/shared/sdk-server/__tests__/evaluation/Evaluator.prerequisite.test.ts diff --git a/contract-tests/index.js b/contract-tests/index.js index e3716a367..761a431dd 100644 --- a/contract-tests/index.js +++ b/contract-tests/index.js @@ -38,6 +38,7 @@ app.get('/', (req, res) => { 'anonymous-redaction', 'evaluation-hooks', 'wrapper', + 'client-prereq-events', ], }); }); diff --git a/packages/sdk/browser/contract-tests/entity/src/TestHarnessWebSocket.ts b/packages/sdk/browser/contract-tests/entity/src/TestHarnessWebSocket.ts index 83707c52e..29f5e6fa9 100644 --- a/packages/sdk/browser/contract-tests/entity/src/TestHarnessWebSocket.ts +++ b/packages/sdk/browser/contract-tests/entity/src/TestHarnessWebSocket.ts @@ -41,6 +41,7 @@ export default class TestHarnessWebSocket { 'inline-context', 'anonymous-redaction', 'strongly-typed', + 'client-prereq-events', ]; break; diff --git a/packages/sdk/browser/rollup.config.js b/packages/sdk/browser/rollup.config.js index 550533ccf..a2c260896 100644 --- a/packages/sdk/browser/rollup.config.js +++ b/packages/sdk/browser/rollup.config.js @@ -28,9 +28,9 @@ const terserOpts = { regex: /^_/, // Do not mangle '_meta', because this is part of our JSON // data model. - reserved: ['_meta'] + reserved: ['_meta'], }, - } + }, }; export default [ @@ -53,12 +53,6 @@ export default [ }, { ...getSharedConfig('cjs', 'dist/index.cjs.js'), - plugins: [ - typescript(), - common(), - resolve(), - terser(terserOpts), - json(), - ], + plugins: [typescript(), common(), resolve(), terser(terserOpts), json()], }, ]; diff --git a/packages/shared/sdk-client/__tests__/LDCLientImpl.inspections.test.ts b/packages/shared/sdk-client/__tests__/LDCLientImpl.inspections.test.ts index 487299d6c..13b5f1b78 100644 --- a/packages/shared/sdk-client/__tests__/LDCLientImpl.inspections.test.ts +++ b/packages/shared/sdk-client/__tests__/LDCLientImpl.inspections.test.ts @@ -187,5 +187,7 @@ it('calls flag-details-changed inspectors when all flag values change', async () 'moonshot-demo': { reason: null, value: true, variationIndex: 0 }, test1: { reason: null, value: 's1', variationIndex: 0 }, 'this-is-a-test': { reason: null, value: true, variationIndex: 0 }, + 'has-prereq-depth-1': { reason: { kind: 'FALLTHROUGH' }, value: true, variationIndex: 0 }, + 'is-prereq': { reason: { kind: 'FALLTHROUGH' }, value: true, variationIndex: 0 }, }); }); diff --git a/packages/shared/sdk-client/__tests__/LDClientImpl.events.test.ts b/packages/shared/sdk-client/__tests__/LDClientImpl.events.test.ts index 946a76d9b..3d18f013c 100644 --- a/packages/shared/sdk-client/__tests__/LDClientImpl.events.test.ts +++ b/packages/shared/sdk-client/__tests__/LDClientImpl.events.test.ts @@ -199,4 +199,45 @@ describe('sdk-client object', () => { expect.stringMatching(/was called with a non-numeric/), ); }); + + it('sends events for prerequisite flags', async () => { + await ldc.identify({ kind: 'user', key: 'bob' }); + ldc.variation('has-prereq-depth-1', false); + ldc.flush(); + + // Prerequisite evaluation event should be emitted before the evaluation event for the flag + // being evaluated. + expect(mockedSendEvent).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ + context: expect.anything(), + creationDate: expect.any(Number), + default: undefined, + key: 'is-prereq', + kind: 'feature', + samplingRatio: 1, + trackEvents: true, + value: true, + variation: 0, + version: 1, + withReasons: false, + }), + ); + expect(mockedSendEvent).toHaveBeenNthCalledWith( + 3, + expect.objectContaining({ + context: expect.anything(), + creationDate: expect.any(Number), + default: false, + key: 'has-prereq-depth-1', + kind: 'feature', + samplingRatio: 1, + trackEvents: true, + value: true, + variation: 0, + version: 4, + withReasons: false, + }), + ); + }); }); diff --git a/packages/shared/sdk-client/__tests__/LDClientImpl.storage.test.ts b/packages/shared/sdk-client/__tests__/LDClientImpl.storage.test.ts index 4fe6fe50f..e46d06a53 100644 --- a/packages/shared/sdk-client/__tests__/LDClientImpl.storage.test.ts +++ b/packages/shared/sdk-client/__tests__/LDClientImpl.storage.test.ts @@ -104,6 +104,8 @@ describe('sdk-client storage', () => { 'easter-i-tunes-special': false, 'easter-specials': 'no specials', fdsafdsafdsafdsa: true, + 'has-prereq-depth-1': true, + 'is-prereq': true, 'log-level': 'warn', 'moonshot-demo': true, test1: 's1', @@ -156,6 +158,8 @@ describe('sdk-client storage', () => { 'easter-i-tunes-special': false, 'easter-specials': 'no specials', fdsafdsafdsafdsa: true, + 'has-prereq-depth-1': true, + 'is-prereq': true, 'log-level': 'warn', 'moonshot-demo': true, test1: 's1', @@ -218,6 +222,8 @@ describe('sdk-client storage', () => { 'easter-i-tunes-special': false, 'easter-specials': 'no specials', fdsafdsafdsafdsa: true, + 'has-prereq-depth-1': true, + 'is-prereq': true, 'log-level': 'warn', 'moonshot-demo': true, test1: 's1', @@ -388,6 +394,8 @@ describe('sdk-client storage', () => { 'easter-i-tunes-special': false, 'easter-specials': 'no specials', fdsafdsafdsafdsa: true, + 'has-prereq-depth-1': true, + 'is-prereq': true, 'log-level': 'warn', 'moonshot-demo': true, test1: 's1', @@ -517,6 +525,8 @@ describe('sdk-client storage', () => { 'easter-i-tunes-special': false, 'easter-specials': 'no specials', fdsafdsafdsafdsa: true, + 'has-prereq-depth-1': true, + 'is-prereq': true, 'log-level': 'warn', 'moonshot-demo': true, test1: 's1', diff --git a/packages/shared/sdk-client/__tests__/LDClientImpl.test.ts b/packages/shared/sdk-client/__tests__/LDClientImpl.test.ts index f6308e9cb..cdbe32cb2 100644 --- a/packages/shared/sdk-client/__tests__/LDClientImpl.test.ts +++ b/packages/shared/sdk-client/__tests__/LDClientImpl.test.ts @@ -103,6 +103,8 @@ describe('sdk-client object', () => { 'easter-i-tunes-special': false, 'easter-specials': 'no specials', fdsafdsafdsafdsa: true, + 'has-prereq-depth-1': true, + 'is-prereq': true, 'log-level': 'warn', 'moonshot-demo': true, test1: 's1', diff --git a/packages/shared/sdk-client/__tests__/LDClientImpl.timeout.test.ts b/packages/shared/sdk-client/__tests__/LDClientImpl.timeout.test.ts index 51a1503e7..f4f5ca70b 100644 --- a/packages/shared/sdk-client/__tests__/LDClientImpl.timeout.test.ts +++ b/packages/shared/sdk-client/__tests__/LDClientImpl.timeout.test.ts @@ -89,6 +89,8 @@ describe('sdk-client identify timeout', () => { 'easter-i-tunes-special': false, 'easter-specials': 'no specials', fdsafdsafdsafdsa: true, + 'has-prereq-depth-1': true, + 'is-prereq': true, 'log-level': 'warn', 'moonshot-demo': true, test1: 's1', @@ -112,6 +114,8 @@ describe('sdk-client identify timeout', () => { 'easter-i-tunes-special': false, 'easter-specials': 'no specials', fdsafdsafdsafdsa: true, + 'has-prereq-depth-1': true, + 'is-prereq': true, 'log-level': 'warn', 'moonshot-demo': true, test1: 's1', diff --git a/packages/shared/sdk-client/__tests__/evaluation/mockResponse.json b/packages/shared/sdk-client/__tests__/evaluation/mockResponse.json index d8f8eb5ea..10c3b4882 100644 --- a/packages/shared/sdk-client/__tests__/evaluation/mockResponse.json +++ b/packages/shared/sdk-client/__tests__/evaluation/mockResponse.json @@ -54,5 +54,24 @@ "value": true, "variation": 0, "trackEvents": false + }, + "is-prereq": { + "value": true, + "variation": 0, + "reason": { + "kind": "FALLTHROUGH" + }, + "version": 1, + "trackEvents": true + }, + "has-prereq-depth-1": { + "value": true, + "variation": 0, + "prerequisites": ["is-prereq"], + "reason": { + "kind": "FALLTHROUGH" + }, + "version": 4, + "trackEvents": true } } diff --git a/packages/shared/sdk-client/src/LDClientImpl.ts b/packages/shared/sdk-client/src/LDClientImpl.ts index e62081624..c29f5cf9e 100644 --- a/packages/shared/sdk-client/src/LDClientImpl.ts +++ b/packages/shared/sdk-client/src/LDClientImpl.ts @@ -327,7 +327,7 @@ export default class LDClientImpl implements LDClient { return createErrorEvaluationDetail(ErrorKinds.FlagNotFound, defaultValue); } - const { reason, value, variation } = foundItem.flag; + const { reason, value, variation, prerequisites } = foundItem.flag; if (typeChecker) { const [matched, type] = typeChecker(value); @@ -355,6 +355,10 @@ export default class LDClientImpl implements LDClient { this.logger.debug('Result value is null. Providing default value.'); successDetail.value = defaultValue; } + + prerequisites?.forEach((prereqKey) => { + this.variation(prereqKey, undefined); + }); this._eventProcessor?.sendEvent( eventFactory.evalEventClient( flagKey, diff --git a/packages/shared/sdk-client/src/evaluation/evaluationDetail.ts b/packages/shared/sdk-client/src/evaluation/evaluationDetail.ts index f0e8d741c..16de25bfd 100644 --- a/packages/shared/sdk-client/src/evaluation/evaluationDetail.ts +++ b/packages/shared/sdk-client/src/evaluation/evaluationDetail.ts @@ -18,9 +18,10 @@ export function createSuccessEvaluationDetail( variationIndex?: number, reason?: LDEvaluationReason, ): LDEvaluationDetail { - return { + const res: LDEvaluationDetail = { value, variationIndex: variationIndex ?? null, reason: reason ?? null, }; + return res; } diff --git a/packages/shared/sdk-client/src/types/index.ts b/packages/shared/sdk-client/src/types/index.ts index f79ffc01f..c424c0e3d 100644 --- a/packages/shared/sdk-client/src/types/index.ts +++ b/packages/shared/sdk-client/src/types/index.ts @@ -10,6 +10,7 @@ export interface Flag { reason?: LDEvaluationReason; debugEventsUntilDate?: number; deleted?: boolean; + prerequisites?: string[]; } export interface PatchFlag extends Flag { diff --git a/packages/shared/sdk-server/__tests__/LDClient.allFlags.test.ts b/packages/shared/sdk-server/__tests__/LDClient.allFlags.test.ts index 585c6a9b5..830f25100 100644 --- a/packages/shared/sdk-server/__tests__/LDClient.allFlags.test.ts +++ b/packages/shared/sdk-server/__tests__/LDClient.allFlags.test.ts @@ -268,6 +268,60 @@ describe('given an LDClient with test data', () => { done(); }); }); + + it('includes prerequisites in flag meta', async () => { + await td.update(td.flag('is-prereq').valueForAll(true)); + await td.usePreconfiguredFlag({ + key: 'has-prereq-depth-1', + on: true, + prerequisites: [ + { + key: 'is-prereq', + variation: 0, + }, + ], + fallthrough: { + variation: 0, + }, + offVariation: 1, + variations: [true, false], + clientSideAvailability: { + usingMobileKey: true, + usingEnvironmentId: true, + }, + clientSide: true, + version: 4, + }); + + const state = await client.allFlagsState(defaultUser, { + withReasons: true, + detailsOnlyForTrackedFlags: false, + }); + expect(state.valid).toEqual(true); + expect(state.allValues()).toEqual({ 'is-prereq': true, 'has-prereq-depth-1': true }); + expect(state.toJSON()).toEqual({ + 'is-prereq': true, + 'has-prereq-depth-1': true, + $flagsState: { + 'is-prereq': { + variation: 0, + reason: { + kind: 'FALLTHROUGH', + }, + version: 1, + }, + 'has-prereq-depth-1': { + variation: 0, + prerequisites: ['is-prereq'], + reason: { + kind: 'FALLTHROUGH', + }, + version: 4, + }, + }, + $valid: true, + }); + }); }); describe('given an offline client', () => { diff --git a/packages/shared/sdk-server/__tests__/evaluation/Evaluator.prerequisite.test.ts b/packages/shared/sdk-server/__tests__/evaluation/Evaluator.prerequisite.test.ts new file mode 100644 index 000000000..400fc937a --- /dev/null +++ b/packages/shared/sdk-server/__tests__/evaluation/Evaluator.prerequisite.test.ts @@ -0,0 +1,181 @@ +import { Context, deserializePoll } from '../../src'; +import { BigSegmentStoreMembership } from '../../src/api/interfaces'; +import { Flag } from '../../src/evaluation/data/Flag'; +import { Segment } from '../../src/evaluation/data/Segment'; +import Evaluator from '../../src/evaluation/Evaluator'; +import { Queries } from '../../src/evaluation/Queries'; +import EventFactory from '../../src/events/EventFactory'; +import { FlagsAndSegments } from '../../src/store/serialization'; +import { createBasicPlatform } from '../createBasicPlatform'; + +describe('given a flag payload with prerequisites', () => { + let evaluator: Evaluator; + const basePayload = { + segments: {}, + flags: { + 'has-prereq-depth-1': { + key: 'has-prereq-depth-1', + on: true, + prerequisites: [ + { + key: 'is-prereq', + variation: 0, + }, + ], + fallthrough: { + variation: 0, + }, + offVariation: 1, + variations: [true, false], + clientSideAvailability: { + usingMobileKey: true, + usingEnvironmentId: true, + }, + clientSide: true, + version: 4, + }, + 'has-prereq-depth-2': { + key: 'has-prereq-depth-2', + on: true, + prerequisites: [ + { + key: 'has-prereq-depth-1', + variation: 0, + }, + ], + fallthrough: { + variation: 0, + }, + offVariation: 1, + variations: [true, false], + clientSideAvailability: { + usingMobileKey: true, + usingEnvironmentId: true, + }, + clientSide: true, + version: 3, + }, + 'has-prereq-depth-3': { + key: 'has-prereq-depth-3', + on: true, + prerequisites: [ + { + key: 'has-prereq-depth-1', + variation: 0, + }, + { + key: 'has-prereq-depth-2', + variation: 0, + }, + { + key: 'is-prereq', + variation: 0, + }, + ], + fallthrough: { + variation: 0, + }, + offVariation: 1, + variations: [true, false], + clientSideAvailability: { + usingMobileKey: true, + usingEnvironmentId: true, + }, + clientSide: true, + version: 3, + }, + 'is-prereq': { + key: 'is-prereq', + on: true, + fallthrough: { + variation: 0, + }, + offVariation: 1, + variations: [true, false], + clientSideAvailability: { + usingMobileKey: true, + usingEnvironmentId: true, + }, + clientSide: true, + version: 3, + }, + }, + }; + + let testPayload: FlagsAndSegments; + + class TestQueries implements Queries { + constructor(private readonly _data: FlagsAndSegments) {} + + getFlag(key: string, cb: (flag: Flag | undefined) => void): void { + const res = this._data.flags[key]; + cb(res); + } + + getSegment(key: string, cb: (segment: Segment | undefined) => void): void { + const res = this._data.segments[key]; + cb(res); + } + + getBigSegmentsMembership( + _userKey: string, + ): Promise<[BigSegmentStoreMembership | null, string] | undefined> { + throw new Error('Method not implemented.'); + } + } + + beforeEach(() => { + testPayload = deserializePoll(JSON.stringify(basePayload))!; + evaluator = new Evaluator(createBasicPlatform(), new TestQueries(testPayload!)); + }); + + it('can track prerequisites for a basic prereq', async () => { + const res = await evaluator.evaluate( + testPayload?.flags['has-prereq-depth-1']!, + Context.fromLDContext({ kind: 'user', key: 'bob' }), + new EventFactory(true), + ); + + expect(res.detail.reason.kind).toEqual('FALLTHROUGH'); + + expect(res.prerequisites).toEqual(['is-prereq']); + }); + + it('can track prerequisites for a prereq of a prereq', async () => { + const res = await evaluator.evaluate( + testPayload?.flags['has-prereq-depth-2']!, + Context.fromLDContext({ kind: 'user', key: 'bob' }), + new EventFactory(true), + ); + + expect(res.detail.reason.kind).toEqual('FALLTHROUGH'); + + expect(res.prerequisites).toEqual(['has-prereq-depth-1']); + }); + + it('can track prerequisites for a flag with multiple prereqs with and without additional prereqs', async () => { + const res = await evaluator.evaluate( + testPayload?.flags['has-prereq-depth-3']!, + Context.fromLDContext({ kind: 'user', key: 'bob' }), + new EventFactory(true), + ); + + expect(res.detail.reason.kind).toEqual('FALLTHROUGH'); + + expect(res.prerequisites).toEqual(['has-prereq-depth-1', 'has-prereq-depth-2', 'is-prereq']); + }); + + it('has can handle a prerequisite failure', async () => { + testPayload.flags['is-prereq'].on = false; + const res = await evaluator.evaluate( + testPayload?.flags['has-prereq-depth-3']!, + Context.fromLDContext({ kind: 'user', key: 'bob' }), + new EventFactory(true), + ); + + expect(res.detail.reason.kind).toEqual('PREREQUISITE_FAILED'); + expect(res.detail.reason.prerequisiteKey).toEqual('has-prereq-depth-1'); + + expect(res.prerequisites).toEqual(['has-prereq-depth-1']); + }); +}); diff --git a/packages/shared/sdk-server/src/FlagsStateBuilder.ts b/packages/shared/sdk-server/src/FlagsStateBuilder.ts index ed78f3305..1ca0613cc 100644 --- a/packages/shared/sdk-server/src/FlagsStateBuilder.ts +++ b/packages/shared/sdk-server/src/FlagsStateBuilder.ts @@ -10,6 +10,7 @@ interface FlagMeta { trackEvents?: boolean; trackReason?: boolean; debugEventsUntilDate?: number; + prerequisites?: string[]; } export default class FlagsStateBuilder { @@ -30,6 +31,7 @@ export default class FlagsStateBuilder { trackEvents: boolean, trackReason: boolean, detailsOnlyIfTracked: boolean, + prerequisites?: string[], ) { this._flagValues[flag.key] = value; const meta: FlagMeta = {}; @@ -56,6 +58,9 @@ export default class FlagsStateBuilder { if (flag.debugEventsUntilDate !== undefined) { meta.debugEventsUntilDate = flag.debugEventsUntilDate; } + if (prerequisites && prerequisites.length) { + meta.prerequisites = prerequisites; + } this._flagMetadata[flag.key] = meta; } diff --git a/packages/shared/sdk-server/src/LDClientImpl.ts b/packages/shared/sdk-server/src/LDClientImpl.ts index baddcb883..bdc4df775 100644 --- a/packages/shared/sdk-server/src/LDClientImpl.ts +++ b/packages/shared/sdk-server/src/LDClientImpl.ts @@ -667,6 +667,7 @@ export default class LDClientImpl implements LDClient { flag.trackEvents || requireExperimentData, requireExperimentData, detailsOnlyIfTracked, + res.prerequisites, ); iterCb(true); }); diff --git a/packages/shared/sdk-server/src/evaluation/EvalResult.ts b/packages/shared/sdk-server/src/evaluation/EvalResult.ts index cc1f70dcc..3799dc252 100644 --- a/packages/shared/sdk-server/src/evaluation/EvalResult.ts +++ b/packages/shared/sdk-server/src/evaluation/EvalResult.ts @@ -10,6 +10,7 @@ import Reasons from './Reasons'; */ export default class EvalResult { public events?: internal.InputEvalEvent[]; + public prerequisites?: string[]; protected constructor( public readonly isError: boolean, diff --git a/packages/shared/sdk-server/src/evaluation/Evaluator.ts b/packages/shared/sdk-server/src/evaluation/Evaluator.ts index 2215762ae..a5ecf7bca 100644 --- a/packages/shared/sdk-server/src/evaluation/Evaluator.ts +++ b/packages/shared/sdk-server/src/evaluation/Evaluator.ts @@ -68,6 +68,7 @@ function computeUpdatedBigSegmentsStatus( interface EvalState { events?: internal.InputEvalEvent[]; + prerequisites?: string[]; bigSegmentsStatus?: BigSegmentStoreStatusString; @@ -116,24 +117,7 @@ export default class Evaluator { async evaluate(flag: Flag, context: Context, eventFactory?: EventFactory): Promise { return new Promise((resolve) => { - const state: EvalState = {}; - this._evaluateInternal( - flag, - context, - state, - [], - (res) => { - if (state.bigSegmentsStatus) { - res.detail.reason = { - ...res.detail.reason, - bigSegmentsStatus: state.bigSegmentsStatus, - }; - } - res.events = state.events; - resolve(res); - }, - eventFactory, - ); + this.evaluateCb(flag, context, resolve, eventFactory); }); } @@ -156,9 +140,13 @@ export default class Evaluator { bigSegmentsStatus: state.bigSegmentsStatus, }; } + if (state.prerequisites) { + res.prerequisites = state.prerequisites; + } res.events = state.events; cb(res); }, + true, eventFactory, ); } @@ -172,6 +160,8 @@ export default class Evaluator { * @param state The current evaluation state. * @param visitedFlags The flags that have been visited during this evaluation. * This is not part of the state, because it needs to be forked during prerequisite evaluations. + * @param topLevel True when this function is being called in the direct evaluation of a flag, + * versus the evaluataion of a prerequisite. */ private _evaluateInternal( flag: Flag, @@ -179,6 +169,7 @@ export default class Evaluator { state: EvalState, visitedFlags: string[], cb: (res: EvalResult) => void, + topLevel: boolean, eventFactory?: EventFactory, ): void { if (!flag.on) { @@ -214,6 +205,7 @@ export default class Evaluator { cb(this._variationForContext(flag.fallthrough, context, flag, Reasons.Fallthrough)); }); }, + topLevel, eventFactory, ); } @@ -227,6 +219,8 @@ export default class Evaluator { * @param cb A callback which is executed when prerequisite checks are complete it is called with * an {@link EvalResult} containing an error result or `undefined` if the prerequisites * are met. + * @param topLevel True when this function is being called in the direct evaluation of a flag, + * versus the evaluataion of a prerequisite. */ private _checkPrerequisites( flag: Flag, @@ -234,6 +228,7 @@ export default class Evaluator { state: EvalState, visitedFlags: string[], cb: (res: EvalResult | undefined) => void, + topLevel: boolean, eventFactory?: EventFactory, ): void { let prereqResult: EvalResult | undefined; @@ -273,7 +268,12 @@ export default class Evaluator { (res) => { // eslint-disable-next-line no-param-reassign state.events ??= []; + if (topLevel) { + // eslint-disable-next-line no-param-reassign + state.prerequisites ??= []; + state.prerequisites.push(prereqFlag.key); + } if (eventFactory) { state.events.push( eventFactory.evalEventServer(prereqFlag, context, res.detail, null, flag), @@ -291,6 +291,7 @@ export default class Evaluator { } return iterCb(true); }, + false, // topLevel false evaluating the prerequisite. eventFactory, ); });