launchdarkly-js-sdk-common 5.2.0 → 5.4.0

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.
@@ -0,0 +1,16 @@
1
+ name: Publish Documentation
2
+ description: 'Publish documentation to github pages.'
3
+
4
+ inputs:
5
+ github_token:
6
+ description: 'The github token to use for committing'
7
+ required: true
8
+
9
+ runs:
10
+ using: composite
11
+ steps:
12
+ - uses: launchdarkly/gh-actions/actions/publish-pages@publish-pages-v1.0.2
13
+ name: 'Publish to Github pages'
14
+ with:
15
+ docs_path: docs
16
+ github_token: ${{ inputs.github_token }}
@@ -0,0 +1,20 @@
1
+ name: Publish to NPM
2
+ description: Publish an npm package.
3
+ inputs:
4
+ prerelease:
5
+ description: 'Is this a prerelease. If so, then the latest tag will not be updated in npm.'
6
+ required: false
7
+ dry-run:
8
+ description: 'Is this a dry run. If so no package will be published.'
9
+ required: false
10
+
11
+ runs:
12
+ using: composite
13
+ steps:
14
+ - name: Publish
15
+ shell: bash
16
+ run: |
17
+ ./scripts/publish-npm.sh
18
+ env:
19
+ LD_RELEASE_IS_PRERELEASE: ${{ inputs.prerelease }}
20
+ LD_RELEASE_IS_DRYRUN: ${{ inputs.dry-run }}
@@ -0,0 +1,41 @@
1
+ name: Build and Test
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ paths-ignore:
7
+ - '**.md' #Do not need to run CI for markdown changes.
8
+ pull_request:
9
+ branches: [main]
10
+ paths-ignore:
11
+ - '**.md'
12
+
13
+ jobs:
14
+ build-test:
15
+ strategy:
16
+ matrix:
17
+ variations: [
18
+ {os: ubuntu-latest, node: latest},
19
+ {os: ubuntu-latest, node: 18}
20
+ ]
21
+
22
+ runs-on: ${{ matrix.variations.os }}
23
+
24
+ steps:
25
+ - uses: actions/checkout@v4
26
+ - uses: actions/setup-node@v4
27
+ with:
28
+ node-version: ${{ matrix.variations.node }}
29
+ registry-url: 'https://registry.npmjs.org'
30
+ - name: Install
31
+ run: npm install
32
+ - name: Test
33
+ run: npm test
34
+ env:
35
+ JEST_JUNIT_OUTPUT_FILE: "reports/junit/js-test-results.xml"
36
+ - name: Lint
37
+ run: npm run lint:all
38
+ - name: Check typescript
39
+ run: npm run check-typescript
40
+ - name: Build Docs
41
+ run: npm run doc
@@ -0,0 +1,12 @@
1
+ name: Lint PR title
2
+
3
+ on:
4
+ pull_request_target:
5
+ types:
6
+ - opened
7
+ - edited
8
+ - synchronize
9
+
10
+ jobs:
11
+ lint-pr-title:
12
+ uses: launchdarkly/gh-actions/.github/workflows/lint-pr-title.yml@main
@@ -0,0 +1,57 @@
1
+ name: Release Please
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+
8
+ jobs:
9
+ release-please:
10
+ runs-on: ubuntu-latest
11
+ outputs:
12
+ release_created: ${{ steps.release.outputs.release_created }}
13
+ steps:
14
+ - uses: googleapis/release-please-action@v4
15
+ id: release
16
+ with:
17
+ token: ${{secrets.GITHUB_TOKEN}}
18
+
19
+ publish-package:
20
+ runs-on: ubuntu-latest
21
+ needs: ['release-please']
22
+ permissions:
23
+ id-token: write
24
+ contents: write
25
+ if: ${{ needs.release-please.outputs.release_created == 'true' }}
26
+ steps:
27
+ - uses: actions/checkout@v4
28
+
29
+ - uses: actions/setup-node@v4
30
+ with:
31
+ node-version: 20.x
32
+ registry-url: 'https://registry.npmjs.org'
33
+
34
+ - uses: launchdarkly/gh-actions/actions/release-secrets@release-secrets-v1.2.0
35
+ name: 'Get NPM token'
36
+ with:
37
+ aws_assume_role: ${{ vars.AWS_ROLE_ARN }}
38
+ ssm_parameter_pairs: '/production/common/releasing/npm/token = NODE_AUTH_TOKEN'
39
+
40
+ - name: Install Dependencies
41
+ run: npm install
42
+
43
+ - id: publish-npm
44
+ name: Publish NPM Package
45
+ uses: ./.github/actions/publish-npm
46
+ with:
47
+ dry-run: 'false'
48
+ prerelease: 'false'
49
+
50
+ - name: Build Documentation
51
+ run: npm run doc
52
+
53
+ - id: publish-docs
54
+ name: Publish Documentation
55
+ uses: ./.github/actions/publish-docs
56
+ with:
57
+ github_token: ${{ secrets.GITHUB_TOKEN }}
@@ -0,0 +1,3 @@
1
+ {
2
+ ".": "5.4.0"
3
+ }
package/CHANGELOG.md CHANGED
@@ -2,6 +2,31 @@
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
+
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)
13
+
14
+
15
+ ### Features
16
+
17
+ * Add inExperiment to evaluation reason. ([#105](https://github.com/launchdarkly/js-sdk-common/issues/105)) ([cf69770](https://github.com/launchdarkly/js-sdk-common/commit/cf6977080e67e4e54f773df410a671764dbcb304))
18
+ * Allow for synchronous inspectors. ([#103](https://github.com/launchdarkly/js-sdk-common/issues/103)) ([7e490f4](https://github.com/launchdarkly/js-sdk-common/commit/7e490f479299f772a9db78efd1c2235645785250))
19
+
20
+ ## [5.2.0] - 2024-05-01
21
+ ### Added:
22
+ - Added an optional timeout to the `waitForInitialization` method. When a timeout is specified the returned promise will be rejected after the timeout elapses if the client has not finished initializing within that time. When no timeout is specified the returned promise will not be resolved or rejected until the initialization either completes or fails.
23
+
24
+ ### Changed:
25
+ - The track method now validates that the provided metricValue is a number. If a metric value is provided, and it is not a number, then a warning will be logged.
26
+
27
+ ### Fixed:
28
+ - Fixed the documentation for `evaluationReasons` for the `identify` method.
29
+
5
30
  ## [5.1.0] - 2024-03-19
6
31
  ### Changed:
7
32
  - Redact anonymous attributes within feature events
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # LaunchDarkly Javascript SDK Core Components
2
2
 
3
- [![Circle CI](https://circleci.com/gh/launchdarkly/js-sdk-common/tree/master.svg?style=svg)](https://circleci.com/gh/launchdarkly/js-sdk-common/tree/master)
3
+ [![Actions Status][ci-badge]][ci]
4
4
 
5
5
  ## LaunchDarkly overview
6
6
 
@@ -33,3 +33,6 @@ We encourage pull requests and other contributions from the community. Check out
33
33
  * [docs.launchdarkly.com](https://docs.launchdarkly.com/ "LaunchDarkly Documentation") for our documentation and SDK reference guides
34
34
  * [apidocs.launchdarkly.com](https://apidocs.launchdarkly.com/ "LaunchDarkly API Documentation") for our API documentation
35
35
  * [blog.launchdarkly.com](https://blog.launchdarkly.com/ "LaunchDarkly Blog Documentation") for the latest product updates
36
+
37
+ [ci-badge]: https://github.com/launchdarkly/js-sdk-common/actions/workflows/ci.yml/badge.svg
38
+ [ci]: https://github.com/launchdarkly/js-sdk-common/actions/workflows/ci.yml
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "launchdarkly-js-sdk-common",
3
- "version": "5.2.0",
3
+ "version": "5.4.0",
4
4
  "description": "LaunchDarkly SDK for JavaScript - common code",
5
5
  "author": "LaunchDarkly <team@launchdarkly.com>",
6
6
  "license": "Apache-2.0",
@@ -17,7 +17,8 @@
17
17
  "format:test:md": "prettier --parser markdown --ignore-path .prettierignore --list-different '*.md'",
18
18
  "format:test:js": "prettier --ignore-path .prettierignore --list-different 'src/**/*.js'",
19
19
  "test": "cross-env NODE_ENV=test jest",
20
- "check-typescript": "node_modules/typescript/bin/tsc"
20
+ "check-typescript": "tsc",
21
+ "doc": "typedoc"
21
22
  },
22
23
  "devDependencies": {
23
24
  "@babel/cli": "^7.8.4",
@@ -43,7 +44,8 @@
43
44
  "launchdarkly-js-test-helpers": "1.1.0",
44
45
  "prettier": "1.19.1",
45
46
  "readline-sync": "^1.4.9",
46
- "typescript": "~4.4.4"
47
+ "typescript": "~5.4.5",
48
+ "typedoc": "^0.25.13"
47
49
  },
48
50
  "dependencies": {
49
51
  "base64-js": "^1.3.0",
@@ -0,0 +1,10 @@
1
+ {
2
+ "bootstrap-sha": "d49ca41718a593c071874950d301f2f00c71a371",
3
+ "packages": {
4
+ ".": {
5
+ "release-type": "node",
6
+ "include-v-in-tag": false,
7
+ "include-component-in-tag": false
8
+ }
9
+ }
10
+ }
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env bash
2
+ if $LD_RELEASE_IS_DRYRUN ; then
3
+ echo "Doing a dry run of publishing."
4
+ else
5
+ if $LD_RELEASE_IS_PRERELEASE ; then
6
+ echo "Publishing with prerelease tag."
7
+ npm publish --tag prerelease --provenance --access public || { echo "npm publish failed" >&2; exit 1; }
8
+ else
9
+ npm publish --provenance --access public || { echo "npm publish failed" >&2; exit 1; }
10
+ fi
11
+ fi
@@ -22,6 +22,9 @@ function InspectorManager(inspectors, logger) {
22
22
 
23
23
  /**
24
24
  * Collection of inspectors keyed by type.
25
+ *
26
+ * Inspectors are async by default.
27
+ *
25
28
  * @type {{[type: string]: object[]}}
26
29
  */
27
30
  const inspectorsByType = {
@@ -30,14 +33,30 @@ function InspectorManager(inspectors, logger) {
30
33
  [InspectorTypes.flagDetailChanged]: [],
31
34
  [InspectorTypes.clientIdentityChanged]: [],
32
35
  };
36
+ /**
37
+ * Collection synchronous of inspectors keyed by type.
38
+ *
39
+ * @type {{[type: string]: object[]}}
40
+ */
41
+ const synchronousInspectorsByType = {
42
+ [InspectorTypes.flagUsed]: [],
43
+ [InspectorTypes.flagDetailsChanged]: [],
44
+ [InspectorTypes.flagDetailChanged]: [],
45
+ [InspectorTypes.clientIdentityChanged]: [],
46
+ };
33
47
 
34
48
  const safeInspectors = inspectors && inspectors.map(inspector => SafeInspector(inspector, logger));
35
49
 
36
50
  safeInspectors &&
37
51
  safeInspectors.forEach(safeInspector => {
38
52
  // Only add inspectors of supported types.
39
- if (Object.prototype.hasOwnProperty.call(inspectorsByType, safeInspector.type)) {
53
+ if (Object.prototype.hasOwnProperty.call(inspectorsByType, safeInspector.type) && !safeInspector.synchronous) {
40
54
  inspectorsByType[safeInspector.type].push(safeInspector);
55
+ } else if (
56
+ Object.prototype.hasOwnProperty.call(synchronousInspectorsByType, safeInspector.type) &&
57
+ safeInspector.synchronous
58
+ ) {
59
+ synchronousInspectorsByType[safeInspector.type].push(safeInspector);
41
60
  } else {
42
61
  logger.warn(messages.invalidInspector(safeInspector.type, safeInspector.name));
43
62
  }
@@ -49,7 +68,9 @@ function InspectorManager(inspectors, logger) {
49
68
  * @param {string} type The type of the inspector to check.
50
69
  * @returns True if there are any inspectors of that type registered.
51
70
  */
52
- manager.hasListeners = type => inspectorsByType[type] && inspectorsByType[type].length;
71
+ manager.hasListeners = type =>
72
+ (inspectorsByType[type] && inspectorsByType[type].length) ||
73
+ (synchronousInspectorsByType[type] && synchronousInspectorsByType[type].length);
53
74
 
54
75
  /**
55
76
  * Notify registered inspectors of a flag being used.
@@ -61,9 +82,13 @@ function InspectorManager(inspectors, logger) {
61
82
  * @param {Object} context The LDContext for the flag.
62
83
  */
63
84
  manager.onFlagUsed = (flagKey, detail, context) => {
64
- if (inspectorsByType[InspectorTypes.flagUsed].length) {
85
+ const type = InspectorTypes.flagUsed;
86
+ if (synchronousInspectorsByType[type].length) {
87
+ synchronousInspectorsByType[type].forEach(inspector => inspector.method(flagKey, detail, context));
88
+ }
89
+ if (inspectorsByType[type].length) {
65
90
  onNextTick(() => {
66
- inspectorsByType[InspectorTypes.flagUsed].forEach(inspector => inspector.method(flagKey, detail, context));
91
+ inspectorsByType[type].forEach(inspector => inspector.method(flagKey, detail, context));
67
92
  });
68
93
  }
69
94
  };
@@ -76,9 +101,13 @@ function InspectorManager(inspectors, logger) {
76
101
  * @param {Record<string, Object>} flags The current flags as a Record<string, LDEvaluationDetail>.
77
102
  */
78
103
  manager.onFlags = flags => {
79
- if (inspectorsByType[InspectorTypes.flagDetailsChanged].length) {
104
+ const type = InspectorTypes.flagDetailsChanged;
105
+ if (synchronousInspectorsByType[type].length) {
106
+ synchronousInspectorsByType[type].forEach(inspector => inspector.method(flags));
107
+ }
108
+ if (inspectorsByType[type].length) {
80
109
  onNextTick(() => {
81
- inspectorsByType[InspectorTypes.flagDetailsChanged].forEach(inspector => inspector.method(flags));
110
+ inspectorsByType[type].forEach(inspector => inspector.method(flags));
82
111
  });
83
112
  }
84
113
  };
@@ -92,9 +121,13 @@ function InspectorManager(inspectors, logger) {
92
121
  * @param {Object} flag An `LDEvaluationDetail` for the flag.
93
122
  */
94
123
  manager.onFlagChanged = (flagKey, flag) => {
95
- if (inspectorsByType[InspectorTypes.flagDetailChanged].length) {
124
+ const type = InspectorTypes.flagDetailChanged;
125
+ if (synchronousInspectorsByType[type].length) {
126
+ synchronousInspectorsByType[type].forEach(inspector => inspector.method(flagKey, flag));
127
+ }
128
+ if (inspectorsByType[type].length) {
96
129
  onNextTick(() => {
97
- inspectorsByType[InspectorTypes.flagDetailChanged].forEach(inspector => inspector.method(flagKey, flag));
130
+ inspectorsByType[type].forEach(inspector => inspector.method(flagKey, flag));
98
131
  });
99
132
  }
100
133
  };
@@ -107,9 +140,13 @@ function InspectorManager(inspectors, logger) {
107
140
  * @param {Object} context The `LDContext` which is now identified.
108
141
  */
109
142
  manager.onIdentityChanged = context => {
110
- if (inspectorsByType[InspectorTypes.clientIdentityChanged].length) {
143
+ const type = InspectorTypes.clientIdentityChanged;
144
+ if (synchronousInspectorsByType[type].length) {
145
+ synchronousInspectorsByType[type].forEach(inspector => inspector.method(context));
146
+ }
147
+ if (inspectorsByType[type].length) {
111
148
  onNextTick(() => {
112
- inspectorsByType[InspectorTypes.clientIdentityChanged].forEach(inspector => inspector.method(context));
149
+ inspectorsByType[type].forEach(inspector => inspector.method(context));
113
150
  });
114
151
  }
115
152
  };
@@ -9,6 +9,7 @@ function SafeInspector(inspector, logger) {
9
9
  const wrapper = {
10
10
  type: inspector.type,
11
11
  name: inspector.name,
12
+ synchronous: inspector.synchronous,
12
13
  };
13
14
 
14
15
  wrapper.method = (...args) => {
@@ -28,7 +28,7 @@ describe('given an inspector manager with no registered inspectors', () => {
28
28
  });
29
29
  });
30
30
 
31
- describe('given an inspector with callbacks of every type', () => {
31
+ describe.each([true, false])('given an inspector with callbacks of every type: synchronous: %p', synchronous => {
32
32
  /**
33
33
  * @type {AsyncQueue}
34
34
  */
@@ -39,6 +39,7 @@ describe('given an inspector with callbacks of every type', () => {
39
39
  {
40
40
  type: 'flag-used',
41
41
  name: 'my-flag-used-inspector',
42
+ synchronous,
42
43
  method: (flagKey, flagDetail, context) => {
43
44
  eventQueue.add({ type: 'flag-used', flagKey, flagDetail, context });
44
45
  },
@@ -47,6 +48,7 @@ describe('given an inspector with callbacks of every type', () => {
47
48
  {
48
49
  type: 'flag-used',
49
50
  name: 'my-other-flag-used-inspector',
51
+ synchronous,
50
52
  method: (flagKey, flagDetail, context) => {
51
53
  eventQueue.add({ type: 'flag-used', flagKey, flagDetail, context });
52
54
  },
@@ -54,6 +56,7 @@ describe('given an inspector with callbacks of every type', () => {
54
56
  {
55
57
  type: 'flag-details-changed',
56
58
  name: 'my-flag-details-inspector',
59
+ synchronous,
57
60
  method: details => {
58
61
  eventQueue.add({
59
62
  type: 'flag-details-changed',
@@ -64,6 +67,7 @@ describe('given an inspector with callbacks of every type', () => {
64
67
  {
65
68
  type: 'flag-detail-changed',
66
69
  name: 'my-flag-detail-inspector',
70
+ synchronous,
67
71
  method: (flagKey, flagDetail) => {
68
72
  eventQueue.add({
69
73
  type: 'flag-detail-changed',
@@ -75,6 +79,7 @@ describe('given an inspector with callbacks of every type', () => {
75
79
  {
76
80
  type: 'client-identity-changed',
77
81
  name: 'my-identity-inspector',
82
+ synchronous,
78
83
  method: context => {
79
84
  eventQueue.add({
80
85
  type: 'client-identity-changed',
@@ -85,6 +90,7 @@ describe('given an inspector with callbacks of every type', () => {
85
90
  // Invalid inspector shouldn't have an effect.
86
91
  {
87
92
  type: 'potato',
93
+ synchronous,
88
94
  name: 'my-potato-inspector',
89
95
  method: () => {},
90
96
  },
@@ -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,12 +5,48 @@ const stubPlatform = require('./stubPlatform');
5
5
  const envName = 'UNKNOWN_ENVIRONMENT_ID';
6
6
  const context = { key: 'context-key' };
7
7
 
8
- describe('given a streaming client with registered inspectors', () => {
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
+
43
+ describe.each([true, false])('given a streaming client with registered inspectors, synchronous: %p', synchronous => {
9
44
  const eventQueue = new AsyncQueue();
10
45
 
11
46
  const inspectors = [
12
47
  {
13
48
  type: 'flag-used',
49
+ synchronous,
14
50
  method: (flagKey, flagDetail, context) => {
15
51
  eventQueue.add({ type: 'flag-used', flagKey, flagDetail, context });
16
52
  },
@@ -18,12 +54,14 @@ describe('given a streaming client with registered inspectors', () => {
18
54
  // 'flag-used registered twice.
19
55
  {
20
56
  type: 'flag-used',
57
+ synchronous,
21
58
  method: (flagKey, flagDetail, context) => {
22
59
  eventQueue.add({ type: 'flag-used', flagKey, flagDetail, context });
23
60
  },
24
61
  },
25
62
  {
26
63
  type: 'flag-details-changed',
64
+ synchronous,
27
65
  method: details => {
28
66
  eventQueue.add({
29
67
  type: 'flag-details-changed',
@@ -33,6 +71,7 @@ describe('given a streaming client with registered inspectors', () => {
33
71
  },
34
72
  {
35
73
  type: 'flag-detail-changed',
74
+ synchronous,
36
75
  method: (flagKey, flagDetail) => {
37
76
  eventQueue.add({
38
77
  type: 'flag-detail-changed',
@@ -43,6 +82,7 @@ describe('given a streaming client with registered inspectors', () => {
43
82
  },
44
83
  {
45
84
  type: 'client-identity-changed',
85
+ synchronous,
46
86
  method: context => {
47
87
  eventQueue.add({
48
88
  type: 'client-identity-changed',
@@ -58,7 +98,7 @@ describe('given a streaming client with registered inspectors', () => {
58
98
  beforeEach(async () => {
59
99
  platform = stubPlatform.defaults();
60
100
  const server = platform.testing.http.newServer();
61
- server.byDefault(respondJson({}));
101
+ server.byDefault(respondJson(flagPayload));
62
102
  const config = { streaming: true, baseUrl: server.url, inspectors, sendEvents: false };
63
103
  client = platform.testing.makeClient(envName, context, config);
64
104
  await client.waitUntilReady();
@@ -86,7 +126,29 @@ describe('given a streaming client with registered inspectors', () => {
86
126
  const flagsEvent = await eventQueue.take();
87
127
  expect(flagsEvent).toMatchObject({
88
128
  type: 'flag-details-changed',
89
- 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
+ },
90
152
  });
91
153
  });
92
154
 
@@ -124,4 +186,51 @@ describe('given a streaming client with registered inspectors', () => {
124
186
  flagDetail: { value: false },
125
187
  });
126
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
+ });
127
236
  });
package/src/index.js CHANGED
@@ -299,18 +299,19 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) {
299
299
  }
300
300
 
301
301
  function variation(key, defaultValue) {
302
- return variationDetailInternal(key, defaultValue, true, false, false).value;
302
+ return variationDetailInternal(key, defaultValue, true, false, false, true).value;
303
303
  }
304
304
 
305
305
  function variationDetail(key, defaultValue) {
306
- return variationDetailInternal(key, defaultValue, true, true, false);
306
+ return variationDetailInternal(key, defaultValue, true, true, false, true);
307
307
  }
308
308
 
309
- function variationDetailInternal(key, defaultValue, sendEvent, includeReasonInEvent, isAllFlags) {
309
+ function variationDetailInternal(key, defaultValue, sendEvent, includeReasonInEvent, isAllFlags, notifyInspection) {
310
310
  let detail;
311
+ let flag;
311
312
 
312
313
  if (flags && utils.objectHasOwnProperty(flags, key) && flags[key] && !flags[key].deleted) {
313
- const flag = flags[key];
314
+ flag = flags[key];
314
315
  detail = getFlagDetail(flag);
315
316
  if (flag.value === null || flag.value === undefined) {
316
317
  detail.value = defaultValue;
@@ -320,11 +321,18 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) {
320
321
  }
321
322
 
322
323
  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
+ }
323
331
  sendFlagEvent(key, detail, defaultValue, includeReasonInEvent);
324
332
  }
325
333
 
326
334
  // For the all flags case `onFlags` will be called instead.
327
- if (!isAllFlags) {
335
+ if (!isAllFlags && notifyInspection) {
328
336
  notifyInspectionFlagUsed(key, detail);
329
337
  }
330
338
 
@@ -351,7 +359,14 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) {
351
359
 
352
360
  for (const key in flags) {
353
361
  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;
355
370
  }
356
371
  }
357
372
 
@@ -462,8 +477,8 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) {
462
477
  } else {
463
478
  mods[data.key] = { current: newDetail };
464
479
  }
465
- handleFlagChanges(mods); // don't wait for this Promise to be resolved
466
480
  notifyInspectionFlagChanged(data, newFlag);
481
+ handleFlagChanges(mods); // don't wait for this Promise to be resolved
467
482
  } else {
468
483
  logger.debug(messages.debugStreamPatchIgnored(data.key));
469
484
  }
package/src/messages.js CHANGED
@@ -15,7 +15,7 @@ const clientInitialized = function() {
15
15
  };
16
16
 
17
17
  const docLink =
18
- ' Please see https://docs.launchdarkly.com/sdk/client-side/javascript#initializing-the-client for instructions on SDK initialization.';
18
+ ' Please see https://docs.launchdarkly.com/sdk/client-side/javascript#initialize-the-client for instructions on SDK initialization.';
19
19
 
20
20
  const clientNotReady = function() {
21
21
  return 'LaunchDarkly client is not ready';
package/typedoc.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "$schema": "https://typedoc.org/schema.json",
3
+ "name": "launchdarkly-js-sdk-common",
4
+ "includeVersion": true,
5
+ "entryPoints": [
6
+ "typings.d.ts",
7
+ ]
8
+ }
package/typings.d.ts CHANGED
@@ -552,7 +552,7 @@ declare module 'launchdarkly-js-sdk-common' {
552
552
 
553
553
  /**
554
554
  * 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}.
556
556
  */
557
557
  export interface LDEvaluationReason {
558
558
  /**
@@ -588,6 +588,14 @@ declare module 'launchdarkly-js-sdk-common' {
588
588
  * The key of the failed prerequisite flag, if the kind was `'PREREQUISITE_FAILED'`.
589
589
  */
590
590
  prerequisiteKey?: string;
591
+
592
+ /**
593
+ * Whether the evaluation was part of an experiment.
594
+ *
595
+ * This is true if the evaluation resulted in an experiment rollout and served one of
596
+ * the variations in the experiment. Otherwise it is false or undefined.
597
+ */
598
+ inExperiment?: boolean;
591
599
  }
592
600
 
593
601
  /**
@@ -1013,6 +1021,13 @@ declare module 'launchdarkly-js-sdk-common' {
1013
1021
  */
1014
1022
  name: string;
1015
1023
 
1024
+ /**
1025
+ * If `true`, then the inspector will be ran synchronously with evaluation.
1026
+ * Synchronous inspectors execute inline with evaluation and care should be taken to ensure
1027
+ * they have minimal performance overhead.
1028
+ */
1029
+ synchronous?: boolean,
1030
+
1016
1031
  /**
1017
1032
  * This method is called when a flag is accessed via a variation method, or it can be called based on actions in
1018
1033
  * wrapper SDKs which have different methods of tracking when a flag was accessed. It is not called when a call is made
@@ -1040,6 +1055,11 @@ declare module 'launchdarkly-js-sdk-common' {
1040
1055
  */
1041
1056
  name: string;
1042
1057
 
1058
+ /**
1059
+ * If `true`, then the inspector will be ran synchronously with flag updates.
1060
+ */
1061
+ synchronous?: boolean,
1062
+
1043
1063
  /**
1044
1064
  * This method is called when the flags in the store are replaced with new flags. It will contain all flags
1045
1065
  * regardless of if they have been evaluated.
@@ -1065,6 +1085,11 @@ declare module 'launchdarkly-js-sdk-common' {
1065
1085
  */
1066
1086
  name: string;
1067
1087
 
1088
+ /**
1089
+ * If `true`, then the inspector will be ran synchronously with flag updates.
1090
+ */
1091
+ synchronous?: boolean,
1092
+
1068
1093
  /**
1069
1094
  * This method is called when a flag is updated. It will not be called
1070
1095
  * when all flags are updated.
@@ -1088,6 +1113,11 @@ declare module 'launchdarkly-js-sdk-common' {
1088
1113
  */
1089
1114
  name: string;
1090
1115
 
1116
+ /**
1117
+ * If `true`, then the inspector will be ran synchronously with identification.
1118
+ */
1119
+ synchronous?: boolean,
1120
+
1091
1121
  /**
1092
1122
  * This method will be called when an identify operation completes.
1093
1123
  */
@@ -1,22 +0,0 @@
1
- version: 2
2
- jobs:
3
- build:
4
- docker:
5
- - image: cimg/node:12.22
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/
@@ -1,25 +0,0 @@
1
- version: 2
2
-
3
- repo:
4
- public: js-sdk-common
5
- private: js-sdk-common-private
6
-
7
- branches:
8
- - name: main
9
- description: 5.x
10
- - name: 4.x
11
- - name: 3.x
12
-
13
- publications:
14
- - url: https://www.npmjs.com/package/launchdarkly-js-sdk-common
15
- description: npm
16
-
17
- jobs:
18
- - docker:
19
- image: node:12-buster
20
- template:
21
- name: npm
22
-
23
- documentation:
24
- gitHubPages: true
25
- title: LaunchDarkly Javascript SDK Core Components
package/docs/typedoc.js DELETED
@@ -1,11 +0,0 @@
1
- module.exports = {
2
- out: '/tmp/project-releaser/project/docs/build/html',
3
- exclude: [
4
- '**/node_modules/**',
5
- 'test-types.ts'
6
- ],
7
- name: "LaunchDarkly Javascript SDK Core Components (4.0.2)",
8
- readme: 'none', // don't add a home page with a copy of README.md
9
- entryPoints: "/tmp/project-releaser/project/typings.d.ts",
10
- entryPointStrategy: "expand"
11
- };