launchdarkly-js-sdk-common 5.3.0 → 5.5.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/manual-publish.yml +42 -0
- package/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +7 -0
- package/package.json +1 -1
- package/release-please-config.json +3 -1
- package/src/HookRunner.js +119 -0
- package/src/__tests__/LDClient-events-test.js +92 -0
- package/src/__tests__/LDClient-inspectors-test.js +106 -2
- package/src/index.js +40 -6
- package/typings.d.ts +109 -1
- package/.circleci/config.yml +0 -22
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
name: Manual Publish Package
|
|
2
|
+
on:
|
|
3
|
+
workflow_dispatch:
|
|
4
|
+
inputs:
|
|
5
|
+
dry-run:
|
|
6
|
+
description: 'Is this a dry run. If so no package will be published.'
|
|
7
|
+
type: boolean
|
|
8
|
+
required: true
|
|
9
|
+
prerelease:
|
|
10
|
+
description: 'Is this a prerelease. If so, then the latest tag will not be updated in npm.'
|
|
11
|
+
type: boolean
|
|
12
|
+
required: true
|
|
13
|
+
|
|
14
|
+
jobs:
|
|
15
|
+
publish-package:
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
permissions:
|
|
18
|
+
id-token: write
|
|
19
|
+
contents: write
|
|
20
|
+
steps:
|
|
21
|
+
- uses: actions/checkout@v4
|
|
22
|
+
|
|
23
|
+
- uses: actions/setup-node@v4
|
|
24
|
+
with:
|
|
25
|
+
node-version: 20.x
|
|
26
|
+
registry-url: 'https://registry.npmjs.org'
|
|
27
|
+
|
|
28
|
+
- uses: launchdarkly/gh-actions/actions/release-secrets@release-secrets-v1.2.0
|
|
29
|
+
name: 'Get NPM token'
|
|
30
|
+
with:
|
|
31
|
+
aws_assume_role: ${{ vars.AWS_ROLE_ARN }}
|
|
32
|
+
ssm_parameter_pairs: '/production/common/releasing/npm/token = NODE_AUTH_TOKEN'
|
|
33
|
+
|
|
34
|
+
- name: Install Dependencies
|
|
35
|
+
run: npm install
|
|
36
|
+
|
|
37
|
+
- id: publish-npm
|
|
38
|
+
name: Publish NPM Package
|
|
39
|
+
uses: ./.github/actions/publish-npm
|
|
40
|
+
with:
|
|
41
|
+
dry-run: ${{ inputs.dry-run }}
|
|
42
|
+
prerelease: ${{ inputs.prerelease }}
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to the `launchdarkly-js-sdk-common` package will be documented in this file. Changes that affect the dependent SDKs such as `launchdarkly-js-client-sdk` should also be logged in those projects, in the next release that uses the updated version of this package. This project adheres to [Semantic Versioning](http://semver.org).
|
|
4
4
|
|
|
5
|
+
## [5.4.0](https://github.com/launchdarkly/js-sdk-common/compare/5.3.0...5.4.0) (2024-10-18)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* Add support for client-side prerequisite events. ([#112](https://github.com/launchdarkly/js-sdk-common/issues/112)) ([9d1708b](https://github.com/launchdarkly/js-sdk-common/commit/9d1708b212246c5650794af99f79cd6a95cfbcd1))
|
|
11
|
+
|
|
5
12
|
## [5.3.0](https://github.com/launchdarkly/js-sdk-common/compare/launchdarkly-js-sdk-common-v5.2.0...launchdarkly-js-sdk-common-v5.3.0) (2024-06-18)
|
|
6
13
|
|
|
7
14
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
const UNKNOWN_HOOK_NAME = 'unknown hook';
|
|
2
|
+
const BEFORE_EVALUATION_STAGE_NAME = 'beforeEvaluation';
|
|
3
|
+
const AFTER_EVALUATION_STAGE_NAME = 'afterEvaluation';
|
|
4
|
+
|
|
5
|
+
function tryExecuteStage(logger, method, hookName, stage, def) {
|
|
6
|
+
try {
|
|
7
|
+
return stage();
|
|
8
|
+
} catch (err) {
|
|
9
|
+
logger?.error(`An error was encountered in "${method}" of the "${hookName}" hook: ${err}`);
|
|
10
|
+
return def;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function getHookName(logger, hook) {
|
|
15
|
+
try {
|
|
16
|
+
return hook.getMetadata().name || UNKNOWN_HOOK_NAME;
|
|
17
|
+
} catch {
|
|
18
|
+
logger.error(`Exception thrown getting metadata for hook. Unable to get hook name.`);
|
|
19
|
+
return UNKNOWN_HOOK_NAME;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function executeBeforeEvaluation(logger, hooks, hookContext) {
|
|
24
|
+
return hooks.map(hook =>
|
|
25
|
+
tryExecuteStage(
|
|
26
|
+
logger,
|
|
27
|
+
BEFORE_EVALUATION_STAGE_NAME,
|
|
28
|
+
getHookName(logger, hook),
|
|
29
|
+
() => hook?.beforeEvaluation?.(hookContext, {}) ?? {},
|
|
30
|
+
{}
|
|
31
|
+
)
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function executeAfterEvaluation(logger, hooks, hookContext, updatedData, result) {
|
|
36
|
+
// This iterates in reverse, versus reversing a shallow copy of the hooks,
|
|
37
|
+
// for efficiency.
|
|
38
|
+
for (let hookIndex = hooks.length - 1; hookIndex >= 0; hookIndex -= 1) {
|
|
39
|
+
const hook = hooks[hookIndex];
|
|
40
|
+
const data = updatedData[hookIndex];
|
|
41
|
+
tryExecuteStage(
|
|
42
|
+
logger,
|
|
43
|
+
AFTER_EVALUATION_STAGE_NAME,
|
|
44
|
+
getHookName(logger, hook),
|
|
45
|
+
() => hook?.afterEvaluation?.(hookContext, data, result) ?? {},
|
|
46
|
+
{}
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function executeBeforeIdentify(logger, hooks, hookContext) {
|
|
52
|
+
return hooks.map(hook =>
|
|
53
|
+
tryExecuteStage(
|
|
54
|
+
logger,
|
|
55
|
+
BEFORE_EVALUATION_STAGE_NAME,
|
|
56
|
+
getHookName(logger, hook),
|
|
57
|
+
() => hook?.beforeIdentify?.(hookContext, {}) ?? {},
|
|
58
|
+
{}
|
|
59
|
+
)
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function executeAfterIdentify(logger, hooks, hookContext, updatedData, result) {
|
|
64
|
+
// This iterates in reverse, versus reversing a shallow copy of the hooks,
|
|
65
|
+
// for efficiency.
|
|
66
|
+
for (let hookIndex = hooks.length - 1; hookIndex >= 0; hookIndex -= 1) {
|
|
67
|
+
const hook = hooks[hookIndex];
|
|
68
|
+
const data = updatedData[hookIndex];
|
|
69
|
+
tryExecuteStage(
|
|
70
|
+
logger,
|
|
71
|
+
AFTER_EVALUATION_STAGE_NAME,
|
|
72
|
+
getHookName(logger, hook),
|
|
73
|
+
() => hook?.afterIdentify?.(hookContext, data, result) ?? {},
|
|
74
|
+
{}
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
class HookRunner {
|
|
80
|
+
constructor(logger, initialHooks) {
|
|
81
|
+
this._logger = logger;
|
|
82
|
+
this._hooks = initialHooks ? [...initialHooks] : [];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
withEvaluation(key, context, defaultValue, method) {
|
|
86
|
+
if (this._hooks.length === 0) {
|
|
87
|
+
return method();
|
|
88
|
+
}
|
|
89
|
+
const hooks = [...this._hooks];
|
|
90
|
+
const hookContext = {
|
|
91
|
+
flagKey: key,
|
|
92
|
+
context,
|
|
93
|
+
defaultValue,
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const hookData = executeBeforeEvaluation(this._logger, hooks, hookContext);
|
|
97
|
+
const result = method();
|
|
98
|
+
executeAfterEvaluation(this._logger, hooks, hookContext, hookData, result);
|
|
99
|
+
return result;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
identify(context, timeout) {
|
|
103
|
+
const hooks = [...this._hooks];
|
|
104
|
+
const hookContext = {
|
|
105
|
+
context,
|
|
106
|
+
timeout,
|
|
107
|
+
};
|
|
108
|
+
const hookData = executeBeforeIdentify(this._logger, hooks, hookContext);
|
|
109
|
+
return result => {
|
|
110
|
+
executeAfterIdentify(this._logger, hooks, hookContext, hookData, result);
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
addHook(hook) {
|
|
115
|
+
this._hooks.push(hook);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
module.exports = HookRunner;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
1
2
|
import * as messages from '../messages';
|
|
2
3
|
|
|
3
4
|
import { withCloseable, sleepAsync } from 'launchdarkly-js-test-helpers';
|
|
@@ -253,6 +254,81 @@ describe('LDClient events', () => {
|
|
|
253
254
|
});
|
|
254
255
|
});
|
|
255
256
|
|
|
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
|
+
|
|
256
332
|
it('sends a feature event on receiving a new flag value', async () => {
|
|
257
333
|
const oldFlags = { foo: { value: 'a', variation: 1, version: 2, flagVersion: 2000 } };
|
|
258
334
|
const newFlags = { foo: { value: 'b', variation: 2, version: 3, flagVersion: 2001 } };
|
|
@@ -327,6 +403,22 @@ describe('LDClient events', () => {
|
|
|
327
403
|
});
|
|
328
404
|
});
|
|
329
405
|
|
|
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
|
+
|
|
330
422
|
it('does not send feature events for allFlags() if sendEventsOnlyForVariation is set', async () => {
|
|
331
423
|
const initData = makeBootstrap({
|
|
332
424
|
foo: { value: 'a', variation: 1, version: 2 },
|
|
@@ -5,6 +5,41 @@ const stubPlatform = require('./stubPlatform');
|
|
|
5
5
|
const envName = 'UNKNOWN_ENVIRONMENT_ID';
|
|
6
6
|
const context = { key: 'context-key' };
|
|
7
7
|
|
|
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
|
+
|
|
8
43
|
describe.each([true, false])('given a streaming client with registered inspectors, synchronous: %p', synchronous => {
|
|
9
44
|
const eventQueue = new AsyncQueue();
|
|
10
45
|
|
|
@@ -63,7 +98,7 @@ describe.each([true, false])('given a streaming client with registered inspector
|
|
|
63
98
|
beforeEach(async () => {
|
|
64
99
|
platform = stubPlatform.defaults();
|
|
65
100
|
const server = platform.testing.http.newServer();
|
|
66
|
-
server.byDefault(respondJson(
|
|
101
|
+
server.byDefault(respondJson(flagPayload));
|
|
67
102
|
const config = { streaming: true, baseUrl: server.url, inspectors, sendEvents: false };
|
|
68
103
|
client = platform.testing.makeClient(envName, context, config);
|
|
69
104
|
await client.waitUntilReady();
|
|
@@ -91,7 +126,29 @@ describe.each([true, false])('given a streaming client with registered inspector
|
|
|
91
126
|
const flagsEvent = await eventQueue.take();
|
|
92
127
|
expect(flagsEvent).toMatchObject({
|
|
93
128
|
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
|
+
},
|
|
95
152
|
});
|
|
96
153
|
});
|
|
97
154
|
|
|
@@ -129,4 +186,51 @@ describe.each([true, false])('given a streaming client with registered inspector
|
|
|
129
186
|
flagDetail: { value: false },
|
|
130
187
|
});
|
|
131
188
|
});
|
|
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
|
+
});
|
|
132
236
|
});
|
package/src/index.js
CHANGED
|
@@ -17,6 +17,7 @@ const messages = require('./messages');
|
|
|
17
17
|
const { checkContext, getContextKeys } = require('./context');
|
|
18
18
|
const { InspectorTypes, InspectorManager } = require('./InspectorManager');
|
|
19
19
|
const timedPromise = require('./timedPromise');
|
|
20
|
+
const HookRunner = require('./HookRunner');
|
|
20
21
|
|
|
21
22
|
const changeEvent = 'change';
|
|
22
23
|
const internalChangeEvent = 'internal-change';
|
|
@@ -40,6 +41,7 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) {
|
|
|
40
41
|
const sendEvents = options.sendEvents;
|
|
41
42
|
let environment = env;
|
|
42
43
|
let hash = options.hash;
|
|
44
|
+
const hookRunner = new HookRunner(logger, options.hooks);
|
|
43
45
|
|
|
44
46
|
const persistentStorage = PersistentStorage(platform.localStorage, logger);
|
|
45
47
|
|
|
@@ -256,11 +258,16 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) {
|
|
|
256
258
|
logger.warn(messages.identifyDisabled());
|
|
257
259
|
return utils.wrapPromiseCallback(Promise.resolve(utils.transformVersionedValuesToValues(flags)), onDone);
|
|
258
260
|
}
|
|
261
|
+
let afterIdentify;
|
|
259
262
|
const clearFirst = useLocalStorage && persistentFlagStore ? persistentFlagStore.clearFlags() : Promise.resolve();
|
|
260
263
|
return utils.wrapPromiseCallback(
|
|
261
264
|
clearFirst
|
|
262
265
|
.then(() => anonymousContextProcessor.processContext(context))
|
|
263
266
|
.then(verifyContext)
|
|
267
|
+
.then(context => {
|
|
268
|
+
afterIdentify = hookRunner.identify(context, undefined);
|
|
269
|
+
return context;
|
|
270
|
+
})
|
|
264
271
|
.then(validatedContext =>
|
|
265
272
|
requestor
|
|
266
273
|
.fetchFlagSettings(validatedContext, newHash)
|
|
@@ -277,12 +284,14 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) {
|
|
|
277
284
|
})
|
|
278
285
|
)
|
|
279
286
|
.then(flagValueMap => {
|
|
287
|
+
afterIdentify?.({ status: 'completed' });
|
|
280
288
|
if (streamActive) {
|
|
281
289
|
connectStream();
|
|
282
290
|
}
|
|
283
291
|
return flagValueMap;
|
|
284
292
|
})
|
|
285
293
|
.catch(err => {
|
|
294
|
+
afterIdentify?.({ status: 'error' });
|
|
286
295
|
emitter.maybeReportError(err);
|
|
287
296
|
return Promise.reject(err);
|
|
288
297
|
}),
|
|
@@ -299,18 +308,24 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) {
|
|
|
299
308
|
}
|
|
300
309
|
|
|
301
310
|
function variation(key, defaultValue) {
|
|
302
|
-
|
|
311
|
+
const { value } = hookRunner.withEvaluation(key, ident.getContext(), defaultValue, () =>
|
|
312
|
+
variationDetailInternal(key, defaultValue, true, false, false, true)
|
|
313
|
+
);
|
|
314
|
+
return value;
|
|
303
315
|
}
|
|
304
316
|
|
|
305
317
|
function variationDetail(key, defaultValue) {
|
|
306
|
-
return
|
|
318
|
+
return hookRunner.withEvaluation(key, ident.getContext(), defaultValue, () =>
|
|
319
|
+
variationDetailInternal(key, defaultValue, true, true, false, true)
|
|
320
|
+
);
|
|
307
321
|
}
|
|
308
322
|
|
|
309
|
-
function variationDetailInternal(key, defaultValue, sendEvent, includeReasonInEvent, isAllFlags) {
|
|
323
|
+
function variationDetailInternal(key, defaultValue, sendEvent, includeReasonInEvent, isAllFlags, notifyInspection) {
|
|
310
324
|
let detail;
|
|
325
|
+
let flag;
|
|
311
326
|
|
|
312
327
|
if (flags && utils.objectHasOwnProperty(flags, key) && flags[key] && !flags[key].deleted) {
|
|
313
|
-
|
|
328
|
+
flag = flags[key];
|
|
314
329
|
detail = getFlagDetail(flag);
|
|
315
330
|
if (flag.value === null || flag.value === undefined) {
|
|
316
331
|
detail.value = defaultValue;
|
|
@@ -320,11 +335,18 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) {
|
|
|
320
335
|
}
|
|
321
336
|
|
|
322
337
|
if (sendEvent) {
|
|
338
|
+
// For an all-flags evaluation, with events enabled, each flag will get an event, so we do not
|
|
339
|
+
// need to duplicate the prerequisites.
|
|
340
|
+
if (!isAllFlags) {
|
|
341
|
+
flag?.prerequisites?.forEach(key => {
|
|
342
|
+
variationDetailInternal(key, undefined, sendEvent, false, false, false);
|
|
343
|
+
});
|
|
344
|
+
}
|
|
323
345
|
sendFlagEvent(key, detail, defaultValue, includeReasonInEvent);
|
|
324
346
|
}
|
|
325
347
|
|
|
326
348
|
// For the all flags case `onFlags` will be called instead.
|
|
327
|
-
if (!isAllFlags) {
|
|
349
|
+
if (!isAllFlags && notifyInspection) {
|
|
328
350
|
notifyInspectionFlagUsed(key, detail);
|
|
329
351
|
}
|
|
330
352
|
|
|
@@ -351,7 +373,14 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) {
|
|
|
351
373
|
|
|
352
374
|
for (const key in flags) {
|
|
353
375
|
if (utils.objectHasOwnProperty(flags, key) && !flags[key].deleted) {
|
|
354
|
-
results[key] = variationDetailInternal(
|
|
376
|
+
results[key] = variationDetailInternal(
|
|
377
|
+
key,
|
|
378
|
+
null,
|
|
379
|
+
!options.sendEventsOnlyForVariation,
|
|
380
|
+
false,
|
|
381
|
+
true,
|
|
382
|
+
false
|
|
383
|
+
).value;
|
|
355
384
|
}
|
|
356
385
|
}
|
|
357
386
|
|
|
@@ -811,6 +840,10 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) {
|
|
|
811
840
|
return initializationStateTracker.getInitializationPromise();
|
|
812
841
|
}
|
|
813
842
|
|
|
843
|
+
function addHook(hook) {
|
|
844
|
+
hookRunner.addHook(hook);
|
|
845
|
+
}
|
|
846
|
+
|
|
814
847
|
const client = {
|
|
815
848
|
waitForInitialization,
|
|
816
849
|
waitUntilReady: () => initializationStateTracker.getReadyPromise(),
|
|
@@ -825,6 +858,7 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) {
|
|
|
825
858
|
flush: flush,
|
|
826
859
|
allFlags: allFlags,
|
|
827
860
|
close: close,
|
|
861
|
+
addHook: addHook,
|
|
828
862
|
};
|
|
829
863
|
|
|
830
864
|
return {
|
package/typings.d.ts
CHANGED
|
@@ -40,6 +40,85 @@ declare module 'launchdarkly-js-sdk-common' {
|
|
|
40
40
|
error: (message: string) => void;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Contextual information provided to evaluation stages.
|
|
45
|
+
*/
|
|
46
|
+
export interface EvaluationSeriesContext {
|
|
47
|
+
readonly flagKey: string;
|
|
48
|
+
readonly context: LDContext;
|
|
49
|
+
readonly defaultValue: unknown;
|
|
50
|
+
readonly method: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Implementation specific hook data for evaluation stages.
|
|
55
|
+
*
|
|
56
|
+
* Hook implementations can use this to store data needed between stages.
|
|
57
|
+
*/
|
|
58
|
+
export interface EvaluationSeriesData {
|
|
59
|
+
readonly [index: string]: unknown;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Meta-data about a hook implementation.
|
|
64
|
+
*/
|
|
65
|
+
export interface HookMetadata {
|
|
66
|
+
readonly name: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Interface for extending SDK functionality via hooks.
|
|
71
|
+
*/
|
|
72
|
+
export interface Hook {
|
|
73
|
+
/**
|
|
74
|
+
* Get metadata about the hook implementation.
|
|
75
|
+
*/
|
|
76
|
+
getMetadata(): HookMetadata;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The before method is called during the execution of a variation method
|
|
80
|
+
* before the flag value has been determined. The method is executed synchronously.
|
|
81
|
+
*
|
|
82
|
+
* @param hookContext Contains information about the evaluation being performed. This is not
|
|
83
|
+
* mutable.
|
|
84
|
+
* @param data A record associated with each stage of hook invocations. Each stage is called with
|
|
85
|
+
* the data of the previous stage for a series. The input record should not be modified.
|
|
86
|
+
* @returns Data to use when executing the next state of the hook in the evaluation series. It is
|
|
87
|
+
* recommended to expand the previous input into the return. This helps ensure your stage remains
|
|
88
|
+
* compatible moving forward as more stages are added.
|
|
89
|
+
* ```js
|
|
90
|
+
* return {...data, "my-new-field": /*my data/*}
|
|
91
|
+
* ```
|
|
92
|
+
*/
|
|
93
|
+
beforeEvaluation?(
|
|
94
|
+
hookContext: EvaluationSeriesContext,
|
|
95
|
+
data: EvaluationSeriesData,
|
|
96
|
+
): EvaluationSeriesData;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* The after method is called during the execution of the variation method
|
|
100
|
+
* after the flag value has been determined. The method is executed synchronously.
|
|
101
|
+
*
|
|
102
|
+
* @param hookContext Contains read-only information about the evaluation
|
|
103
|
+
* being performed.
|
|
104
|
+
* @param data A record associated with each stage of hook invocations. Each
|
|
105
|
+
* stage is called with the data of the previous stage for a series.
|
|
106
|
+
* @param detail The result of the evaluation. This value should not be
|
|
107
|
+
* modified.
|
|
108
|
+
* @returns Data to use when executing the next state of the hook in the evaluation series. It is
|
|
109
|
+
* recommended to expand the previous input into the return. This helps ensure your stage remains
|
|
110
|
+
* compatible moving forward as more stages are added.
|
|
111
|
+
* ```js
|
|
112
|
+
* return {...data, "my-new-field": /*my data/*}
|
|
113
|
+
* ```
|
|
114
|
+
*/
|
|
115
|
+
afterEvaluation?(
|
|
116
|
+
hookContext: EvaluationSeriesContext,
|
|
117
|
+
data: EvaluationSeriesData,
|
|
118
|
+
detail: LDEvaluationDetail,
|
|
119
|
+
): EvaluationSeriesData;
|
|
120
|
+
}
|
|
121
|
+
|
|
43
122
|
/**
|
|
44
123
|
* LaunchDarkly initialization options that are supported by all variants of the JS client.
|
|
45
124
|
* The browser SDK and Electron SDK may support additional options.
|
|
@@ -277,6 +356,25 @@ declare module 'launchdarkly-js-sdk-common' {
|
|
|
277
356
|
* Inspectors can be used for collecting information for monitoring, analytics, and debugging.
|
|
278
357
|
*/
|
|
279
358
|
inspectors?: LDInspection[];
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Initial set of hooks for the client.
|
|
362
|
+
*
|
|
363
|
+
* Hooks provide entrypoints which allow for observation of SDK functions.
|
|
364
|
+
*
|
|
365
|
+
* LaunchDarkly provides integration packages, and most applications will not
|
|
366
|
+
* need to implement their own hooks. Refer to the `@launchdarkly/node-server-sdk-otel`
|
|
367
|
+
* for instrumentation for the `@launchdarkly/node-server-sdk`.
|
|
368
|
+
*
|
|
369
|
+
* Example:
|
|
370
|
+
* ```typescript
|
|
371
|
+
* import { init } from '@launchdarkly/node-server-sdk';
|
|
372
|
+
* import { TheHook } from '@launchdarkly/some-hook';
|
|
373
|
+
*
|
|
374
|
+
* const client = init('my-sdk-key', { hooks: [new TheHook()] });
|
|
375
|
+
* ```
|
|
376
|
+
*/
|
|
377
|
+
hooks?: Hook[];
|
|
280
378
|
}
|
|
281
379
|
|
|
282
380
|
/**
|
|
@@ -552,7 +650,7 @@ declare module 'launchdarkly-js-sdk-common' {
|
|
|
552
650
|
|
|
553
651
|
/**
|
|
554
652
|
* 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
|
|
653
|
+
* part of the {@link LDEvaluationDetail} object returned by {@link LDClient.variationDetail}.
|
|
556
654
|
*/
|
|
557
655
|
export interface LDEvaluationReason {
|
|
558
656
|
/**
|
|
@@ -909,6 +1007,16 @@ declare module 'launchdarkly-js-sdk-common' {
|
|
|
909
1007
|
* closing is finished. It will never be rejected.
|
|
910
1008
|
*/
|
|
911
1009
|
close(onDone?: () => void): Promise<void>;
|
|
1010
|
+
|
|
1011
|
+
/**
|
|
1012
|
+
* Add a hook to the client. In order to register a hook before the client
|
|
1013
|
+
* starts, please use the `hooks` property of {@link LDOptions}.
|
|
1014
|
+
*
|
|
1015
|
+
* Hooks provide entrypoints which allow for observation of SDK functions.
|
|
1016
|
+
*
|
|
1017
|
+
* @param Hook The hook to add.
|
|
1018
|
+
*/
|
|
1019
|
+
addHook(hook: Hook): void;
|
|
912
1020
|
}
|
|
913
1021
|
|
|
914
1022
|
/**
|
package/.circleci/config.yml
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
version: 2
|
|
2
|
-
jobs:
|
|
3
|
-
build:
|
|
4
|
-
docker:
|
|
5
|
-
- image: cimg/node:22.2.0
|
|
6
|
-
steps:
|
|
7
|
-
- checkout
|
|
8
|
-
|
|
9
|
-
- run: npm install
|
|
10
|
-
- run: npm run lint:all
|
|
11
|
-
- run:
|
|
12
|
-
command: npm test
|
|
13
|
-
environment:
|
|
14
|
-
JEST_JUNIT_OUTPUT: "reports/junit/js-test-results.xml"
|
|
15
|
-
- run: npm run check-typescript
|
|
16
|
-
- run:
|
|
17
|
-
name: dependency audit
|
|
18
|
-
command: ./scripts/better-audit.sh
|
|
19
|
-
- store_test_results:
|
|
20
|
-
path: reports/junit/
|
|
21
|
-
- store_artifacts:
|
|
22
|
-
path: reports/junit/
|