Skip to content

Commit 9d1708b

Browse files
authored
feat: Add support for client-side prerequisite events. (#112)
1 parent 60e2dee commit 9d1708b

File tree

4 files changed

+220
-9
lines changed

4 files changed

+220
-9
lines changed

src/__tests__/LDClient-events-test.js

+92
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
// @ts-nocheck
12
import * as messages from '../messages';
23

34
import { withCloseable, sleepAsync } from 'launchdarkly-js-test-helpers';
@@ -253,6 +254,81 @@ describe('LDClient events', () => {
253254
});
254255
});
255256

257+
it('sends events for prerequisites', async () => {
258+
const initData = makeBootstrap({
259+
'is-prereq': {
260+
value: true,
261+
variation: 1,
262+
reason: {
263+
kind: 'FALLTHROUGH',
264+
},
265+
version: 1,
266+
trackEvents: true,
267+
trackReason: true,
268+
},
269+
'has-prereq-depth-1': {
270+
value: true,
271+
variation: 0,
272+
prerequisites: ['is-prereq'],
273+
reason: {
274+
kind: 'FALLTHROUGH',
275+
},
276+
version: 4,
277+
trackEvents: true,
278+
trackReason: true,
279+
},
280+
'has-prereq-depth-2': {
281+
value: true,
282+
variation: 0,
283+
prerequisites: ['has-prereq-depth-1'],
284+
reason: {
285+
kind: 'FALLTHROUGH',
286+
},
287+
version: 5,
288+
trackEvents: true,
289+
trackReason: true,
290+
},
291+
});
292+
await withClientAndEventProcessor(user, { bootstrap: initData }, async (client, ep) => {
293+
await client.waitForInitialization(5);
294+
client.variation('has-prereq-depth-2', false);
295+
296+
// An identify event and 3 feature events.
297+
expect(ep.events.length).toEqual(4);
298+
expectIdentifyEvent(ep.events[0], user);
299+
expect(ep.events[1]).toMatchObject({
300+
kind: 'feature',
301+
key: 'is-prereq',
302+
variation: 1,
303+
value: true,
304+
version: 1,
305+
reason: {
306+
kind: 'FALLTHROUGH',
307+
},
308+
});
309+
expect(ep.events[2]).toMatchObject({
310+
kind: 'feature',
311+
key: 'has-prereq-depth-1',
312+
variation: 0,
313+
value: true,
314+
version: 4,
315+
reason: {
316+
kind: 'FALLTHROUGH',
317+
},
318+
});
319+
expect(ep.events[3]).toMatchObject({
320+
kind: 'feature',
321+
key: 'has-prereq-depth-2',
322+
variation: 0,
323+
value: true,
324+
version: 5,
325+
reason: {
326+
kind: 'FALLTHROUGH',
327+
},
328+
});
329+
});
330+
});
331+
256332
it('sends a feature event on receiving a new flag value', async () => {
257333
const oldFlags = { foo: { value: 'a', variation: 1, version: 2, flagVersion: 2000 } };
258334
const newFlags = { foo: { value: 'b', variation: 2, version: 3, flagVersion: 2001 } };
@@ -327,6 +403,22 @@ describe('LDClient events', () => {
327403
});
328404
});
329405

406+
it('does not send duplicate events for prerequisites with all flags.', async () => {
407+
const initData = makeBootstrap({
408+
foo: { value: 'a', variation: 1, version: 2 },
409+
bar: { value: 'b', variation: 1, version: 3, prerequisites: ['foo'] },
410+
});
411+
await withClientAndEventProcessor(user, { bootstrap: initData }, async (client, ep) => {
412+
await client.waitForInitialization(5);
413+
client.allFlags();
414+
415+
expect(ep.events.length).toEqual(3);
416+
expectIdentifyEvent(ep.events[0], user);
417+
expectFeatureEvent({ e: ep.events[1], key: 'foo', user, value: 'a', variation: 1, version: 2, defaultVal: null });
418+
expectFeatureEvent({ e: ep.events[2], key: 'bar', user, value: 'b', variation: 1, version: 3, defaultVal: null });
419+
});
420+
});
421+
330422
it('does not send feature events for allFlags() if sendEventsOnlyForVariation is set', async () => {
331423
const initData = makeBootstrap({
332424
foo: { value: 'a', variation: 1, version: 2 },

src/__tests__/LDClient-inspectors-test.js

+106-2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,41 @@ const stubPlatform = require('./stubPlatform');
55
const envName = 'UNKNOWN_ENVIRONMENT_ID';
66
const context = { key: 'context-key' };
77

8+
const flagPayload = {
9+
'is-prereq': {
10+
value: true,
11+
variation: 1,
12+
reason: {
13+
kind: 'FALLTHROUGH',
14+
},
15+
version: 1,
16+
trackEvents: true,
17+
trackReason: true,
18+
},
19+
'has-prereq-depth-1': {
20+
value: true,
21+
variation: 0,
22+
prerequisites: ['is-prereq'],
23+
reason: {
24+
kind: 'FALLTHROUGH',
25+
},
26+
version: 4,
27+
trackEvents: true,
28+
trackReason: true,
29+
},
30+
'has-prereq-depth-2': {
31+
value: true,
32+
variation: 0,
33+
prerequisites: ['has-prereq-depth-1'],
34+
reason: {
35+
kind: 'FALLTHROUGH',
36+
},
37+
version: 5,
38+
trackEvents: true,
39+
trackReason: true,
40+
},
41+
};
42+
843
describe.each([true, false])('given a streaming client with registered inspectors, synchronous: %p', synchronous => {
944
const eventQueue = new AsyncQueue();
1045

@@ -63,7 +98,7 @@ describe.each([true, false])('given a streaming client with registered inspector
6398
beforeEach(async () => {
6499
platform = stubPlatform.defaults();
65100
const server = platform.testing.http.newServer();
66-
server.byDefault(respondJson({}));
101+
server.byDefault(respondJson(flagPayload));
67102
const config = { streaming: true, baseUrl: server.url, inspectors, sendEvents: false };
68103
client = platform.testing.makeClient(envName, context, config);
69104
await client.waitUntilReady();
@@ -91,7 +126,29 @@ describe.each([true, false])('given a streaming client with registered inspector
91126
const flagsEvent = await eventQueue.take();
92127
expect(flagsEvent).toMatchObject({
93128
type: 'flag-details-changed',
94-
details: {},
129+
details: {
130+
'is-prereq': {
131+
value: true,
132+
variationIndex: 1,
133+
reason: {
134+
kind: 'FALLTHROUGH',
135+
},
136+
},
137+
'has-prereq-depth-1': {
138+
value: true,
139+
variationIndex: 0,
140+
reason: {
141+
kind: 'FALLTHROUGH',
142+
},
143+
},
144+
'has-prereq-depth-2': {
145+
value: true,
146+
variationIndex: 0,
147+
reason: {
148+
kind: 'FALLTHROUGH',
149+
},
150+
},
151+
},
95152
});
96153
});
97154

@@ -129,4 +186,51 @@ describe.each([true, false])('given a streaming client with registered inspector
129186
flagDetail: { value: false },
130187
});
131188
});
189+
190+
it('emits an event when a flag is used', async () => {
191+
// Take initial events.
192+
eventQueue.take();
193+
eventQueue.take();
194+
195+
await platform.testing.eventSourcesCreated.take();
196+
client.variation('is-prereq', false);
197+
const updateEvent = await eventQueue.take();
198+
expect(updateEvent).toMatchObject({
199+
type: 'flag-used',
200+
flagKey: 'is-prereq',
201+
flagDetail: { value: true },
202+
});
203+
// Two inspectors are handling this
204+
const updateEvent2 = await eventQueue.take();
205+
expect(updateEvent2).toMatchObject({
206+
type: 'flag-used',
207+
flagKey: 'is-prereq',
208+
flagDetail: { value: true },
209+
});
210+
});
211+
212+
it('does not execute flag-used for prerequisites', async () => {
213+
// Take initial events.
214+
eventQueue.take();
215+
eventQueue.take();
216+
217+
await platform.testing.eventSourcesCreated.take();
218+
client.variation('has-prereq-depth-2', false);
219+
// There would be many more than 2 events if prerequisites were inspected.
220+
const updateEvent = await eventQueue.take();
221+
expect(updateEvent).toMatchObject({
222+
type: 'flag-used',
223+
flagKey: 'has-prereq-depth-2',
224+
flagDetail: { value: true },
225+
});
226+
// Two inspectors are handling this
227+
const updateEvent2 = await eventQueue.take();
228+
expect(updateEvent2).toMatchObject({
229+
type: 'flag-used',
230+
flagKey: 'has-prereq-depth-2',
231+
flagDetail: { value: true },
232+
});
233+
234+
expect(eventQueue.length()).toEqual(0);
235+
});
132236
});

src/index.js

+21-6
Original file line numberDiff line numberDiff line change
@@ -299,18 +299,19 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) {
299299
}
300300

301301
function variation(key, defaultValue) {
302-
return variationDetailInternal(key, defaultValue, true, false, false).value;
302+
return variationDetailInternal(key, defaultValue, true, false, false, true).value;
303303
}
304304

305305
function variationDetail(key, defaultValue) {
306-
return variationDetailInternal(key, defaultValue, true, true, false);
306+
return variationDetailInternal(key, defaultValue, true, true, false, true);
307307
}
308308

309-
function variationDetailInternal(key, defaultValue, sendEvent, includeReasonInEvent, isAllFlags) {
309+
function variationDetailInternal(key, defaultValue, sendEvent, includeReasonInEvent, isAllFlags, notifyInspection) {
310310
let detail;
311+
let flag;
311312

312313
if (flags && utils.objectHasOwnProperty(flags, key) && flags[key] && !flags[key].deleted) {
313-
const flag = flags[key];
314+
flag = flags[key];
314315
detail = getFlagDetail(flag);
315316
if (flag.value === null || flag.value === undefined) {
316317
detail.value = defaultValue;
@@ -320,11 +321,18 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) {
320321
}
321322

322323
if (sendEvent) {
324+
// For an all-flags evaluation, with events enabled, each flag will get an event, so we do not
325+
// need to duplicate the prerequisites.
326+
if (!isAllFlags) {
327+
flag?.prerequisites?.forEach(key => {
328+
variationDetailInternal(key, undefined, sendEvent, false, false, false);
329+
});
330+
}
323331
sendFlagEvent(key, detail, defaultValue, includeReasonInEvent);
324332
}
325333

326334
// For the all flags case `onFlags` will be called instead.
327-
if (!isAllFlags) {
335+
if (!isAllFlags && notifyInspection) {
328336
notifyInspectionFlagUsed(key, detail);
329337
}
330338

@@ -351,7 +359,14 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) {
351359

352360
for (const key in flags) {
353361
if (utils.objectHasOwnProperty(flags, key) && !flags[key].deleted) {
354-
results[key] = variationDetailInternal(key, null, !options.sendEventsOnlyForVariation, false, true).value;
362+
results[key] = variationDetailInternal(
363+
key,
364+
null,
365+
!options.sendEventsOnlyForVariation,
366+
false,
367+
true,
368+
false
369+
).value;
355370
}
356371
}
357372

typings.d.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -552,7 +552,7 @@ declare module 'launchdarkly-js-sdk-common' {
552552

553553
/**
554554
* Describes the reason that a flag evaluation produced a particular value. This is
555-
* part of the {@link LDEvaluationDetail} object returned by {@link LDClient.variationDetail]].
555+
* part of the {@link LDEvaluationDetail} object returned by {@link LDClient.variationDetail}.
556556
*/
557557
export interface LDEvaluationReason {
558558
/**

0 commit comments

Comments
 (0)