Skip to content

Commit

Permalink
feat: Add support for client-side prerequisite events. (#112)
Browse files Browse the repository at this point in the history
  • Loading branch information
kinyoklion authored Oct 18, 2024
1 parent 60e2dee commit 9d1708b
Show file tree
Hide file tree
Showing 4 changed files with 220 additions and 9 deletions.
92 changes: 92 additions & 0 deletions src/__tests__/LDClient-events-test.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// @ts-nocheck
import * as messages from '../messages';

import { withCloseable, sleepAsync } from 'launchdarkly-js-test-helpers';
Expand Down Expand Up @@ -253,6 +254,81 @@ describe('LDClient events', () => {
});
});

it('sends events for prerequisites', async () => {
const initData = makeBootstrap({
'is-prereq': {
value: true,
variation: 1,
reason: {
kind: 'FALLTHROUGH',
},
version: 1,
trackEvents: true,
trackReason: true,
},
'has-prereq-depth-1': {
value: true,
variation: 0,
prerequisites: ['is-prereq'],
reason: {
kind: 'FALLTHROUGH',
},
version: 4,
trackEvents: true,
trackReason: true,
},
'has-prereq-depth-2': {
value: true,
variation: 0,
prerequisites: ['has-prereq-depth-1'],
reason: {
kind: 'FALLTHROUGH',
},
version: 5,
trackEvents: true,
trackReason: true,
},
});
await withClientAndEventProcessor(user, { bootstrap: initData }, async (client, ep) => {
await client.waitForInitialization(5);
client.variation('has-prereq-depth-2', false);

// An identify event and 3 feature events.
expect(ep.events.length).toEqual(4);
expectIdentifyEvent(ep.events[0], user);
expect(ep.events[1]).toMatchObject({
kind: 'feature',
key: 'is-prereq',
variation: 1,
value: true,
version: 1,
reason: {
kind: 'FALLTHROUGH',
},
});
expect(ep.events[2]).toMatchObject({
kind: 'feature',
key: 'has-prereq-depth-1',
variation: 0,
value: true,
version: 4,
reason: {
kind: 'FALLTHROUGH',
},
});
expect(ep.events[3]).toMatchObject({
kind: 'feature',
key: 'has-prereq-depth-2',
variation: 0,
value: true,
version: 5,
reason: {
kind: 'FALLTHROUGH',
},
});
});
});

it('sends a feature event on receiving a new flag value', async () => {
const oldFlags = { foo: { value: 'a', variation: 1, version: 2, flagVersion: 2000 } };
const newFlags = { foo: { value: 'b', variation: 2, version: 3, flagVersion: 2001 } };
Expand Down Expand Up @@ -327,6 +403,22 @@ describe('LDClient events', () => {
});
});

it('does not send duplicate events for prerequisites with all flags.', async () => {
const initData = makeBootstrap({
foo: { value: 'a', variation: 1, version: 2 },
bar: { value: 'b', variation: 1, version: 3, prerequisites: ['foo'] },
});
await withClientAndEventProcessor(user, { bootstrap: initData }, async (client, ep) => {
await client.waitForInitialization(5);
client.allFlags();

expect(ep.events.length).toEqual(3);
expectIdentifyEvent(ep.events[0], user);
expectFeatureEvent({ e: ep.events[1], key: 'foo', user, value: 'a', variation: 1, version: 2, defaultVal: null });
expectFeatureEvent({ e: ep.events[2], key: 'bar', user, value: 'b', variation: 1, version: 3, defaultVal: null });
});
});

it('does not send feature events for allFlags() if sendEventsOnlyForVariation is set', async () => {
const initData = makeBootstrap({
foo: { value: 'a', variation: 1, version: 2 },
Expand Down
108 changes: 106 additions & 2 deletions src/__tests__/LDClient-inspectors-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,41 @@ const stubPlatform = require('./stubPlatform');
const envName = 'UNKNOWN_ENVIRONMENT_ID';
const context = { key: 'context-key' };

const flagPayload = {
'is-prereq': {
value: true,
variation: 1,
reason: {
kind: 'FALLTHROUGH',
},
version: 1,
trackEvents: true,
trackReason: true,
},
'has-prereq-depth-1': {
value: true,
variation: 0,
prerequisites: ['is-prereq'],
reason: {
kind: 'FALLTHROUGH',
},
version: 4,
trackEvents: true,
trackReason: true,
},
'has-prereq-depth-2': {
value: true,
variation: 0,
prerequisites: ['has-prereq-depth-1'],
reason: {
kind: 'FALLTHROUGH',
},
version: 5,
trackEvents: true,
trackReason: true,
},
};

describe.each([true, false])('given a streaming client with registered inspectors, synchronous: %p', synchronous => {
const eventQueue = new AsyncQueue();

Expand Down Expand Up @@ -63,7 +98,7 @@ describe.each([true, false])('given a streaming client with registered inspector
beforeEach(async () => {
platform = stubPlatform.defaults();
const server = platform.testing.http.newServer();
server.byDefault(respondJson({}));
server.byDefault(respondJson(flagPayload));
const config = { streaming: true, baseUrl: server.url, inspectors, sendEvents: false };
client = platform.testing.makeClient(envName, context, config);
await client.waitUntilReady();
Expand Down Expand Up @@ -91,7 +126,29 @@ describe.each([true, false])('given a streaming client with registered inspector
const flagsEvent = await eventQueue.take();
expect(flagsEvent).toMatchObject({
type: 'flag-details-changed',
details: {},
details: {
'is-prereq': {
value: true,
variationIndex: 1,
reason: {
kind: 'FALLTHROUGH',
},
},
'has-prereq-depth-1': {
value: true,
variationIndex: 0,
reason: {
kind: 'FALLTHROUGH',
},
},
'has-prereq-depth-2': {
value: true,
variationIndex: 0,
reason: {
kind: 'FALLTHROUGH',
},
},
},
});
});

Expand Down Expand Up @@ -129,4 +186,51 @@ describe.each([true, false])('given a streaming client with registered inspector
flagDetail: { value: false },
});
});

it('emits an event when a flag is used', async () => {
// Take initial events.
eventQueue.take();
eventQueue.take();

await platform.testing.eventSourcesCreated.take();
client.variation('is-prereq', false);
const updateEvent = await eventQueue.take();
expect(updateEvent).toMatchObject({
type: 'flag-used',
flagKey: 'is-prereq',
flagDetail: { value: true },
});
// Two inspectors are handling this
const updateEvent2 = await eventQueue.take();
expect(updateEvent2).toMatchObject({
type: 'flag-used',
flagKey: 'is-prereq',
flagDetail: { value: true },
});
});

it('does not execute flag-used for prerequisites', async () => {
// Take initial events.
eventQueue.take();
eventQueue.take();

await platform.testing.eventSourcesCreated.take();
client.variation('has-prereq-depth-2', false);
// There would be many more than 2 events if prerequisites were inspected.
const updateEvent = await eventQueue.take();
expect(updateEvent).toMatchObject({
type: 'flag-used',
flagKey: 'has-prereq-depth-2',
flagDetail: { value: true },
});
// Two inspectors are handling this
const updateEvent2 = await eventQueue.take();
expect(updateEvent2).toMatchObject({
type: 'flag-used',
flagKey: 'has-prereq-depth-2',
flagDetail: { value: true },
});

expect(eventQueue.length()).toEqual(0);
});
});
27 changes: 21 additions & 6 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -299,18 +299,19 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) {
}

function variation(key, defaultValue) {
return variationDetailInternal(key, defaultValue, true, false, false).value;
return variationDetailInternal(key, defaultValue, true, false, false, true).value;
}

function variationDetail(key, defaultValue) {
return variationDetailInternal(key, defaultValue, true, true, false);
return variationDetailInternal(key, defaultValue, true, true, false, true);
}

function variationDetailInternal(key, defaultValue, sendEvent, includeReasonInEvent, isAllFlags) {
function variationDetailInternal(key, defaultValue, sendEvent, includeReasonInEvent, isAllFlags, notifyInspection) {
let detail;
let flag;

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

if (sendEvent) {
// For an all-flags evaluation, with events enabled, each flag will get an event, so we do not
// need to duplicate the prerequisites.
if (!isAllFlags) {
flag?.prerequisites?.forEach(key => {
variationDetailInternal(key, undefined, sendEvent, false, false, false);
});
}
sendFlagEvent(key, detail, defaultValue, includeReasonInEvent);
}

// For the all flags case `onFlags` will be called instead.
if (!isAllFlags) {
if (!isAllFlags && notifyInspection) {
notifyInspectionFlagUsed(key, detail);
}

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

for (const key in flags) {
if (utils.objectHasOwnProperty(flags, key) && !flags[key].deleted) {
results[key] = variationDetailInternal(key, null, !options.sendEventsOnlyForVariation, false, true).value;
results[key] = variationDetailInternal(
key,
null,
!options.sendEventsOnlyForVariation,
false,
true,
false
).value;
}
}

Expand Down
2 changes: 1 addition & 1 deletion typings.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -552,7 +552,7 @@ declare module 'launchdarkly-js-sdk-common' {

/**
* Describes the reason that a flag evaluation produced a particular value. This is
* part of the {@link LDEvaluationDetail} object returned by {@link LDClient.variationDetail]].
* part of the {@link LDEvaluationDetail} object returned by {@link LDClient.variationDetail}.
*/
export interface LDEvaluationReason {
/**
Expand Down

0 comments on commit 9d1708b

Please sign in to comment.