launchdarkly-js-sdk-common 4.1.1 → 4.3.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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,20 @@
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
+ ## [4.2.0] - 2022-10-03
6
+ ### Removed:
7
+ - Removed `seenRequests` cache. This cache was used to de-duplicate events, but it has been supplanted with summary events.
8
+
9
+ ### Deprecated:
10
+ - The `allowFrequentDuplicateEvents` configuration has been deprecated because it controlled the behavior of the `seenRequests` cache.
11
+
12
+ ## [4.1.1] - 2022-06-07
13
+ ### Changed:
14
+ - Enforce a 64 character limit for `application.id` and `application.version` configuration options.
15
+
16
+ ### Fixed:
17
+ - Do not include deleted flags in `allFlags`.
18
+
5
19
  ## [4.1.0] - 2022-04-21
6
20
  ### Added:
7
21
  - `LDOptionsBase.application`, for configuration of application metadata that may be used in LaunchDarkly analytics or other product features. This does not affect feature flag evaluations.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "launchdarkly-js-sdk-common",
3
- "version": "4.1.1",
3
+ "version": "4.3.0",
4
4
  "description": "LaunchDarkly SDK for JavaScript - common code",
5
5
  "author": "LaunchDarkly <team@launchdarkly.com>",
6
6
  "license": "Apache-2.0",
@@ -0,0 +1,120 @@
1
+ const { messages } = require('.');
2
+ const SafeInspector = require('./SafeInspector');
3
+ const { onNextTick } = require('./utils');
4
+
5
+ /**
6
+ * The types of supported inspectors.
7
+ */
8
+ const InspectorTypes = {
9
+ flagUsed: 'flag-used',
10
+ flagDetailsChanged: 'flag-details-changed',
11
+ flagDetailChanged: 'flag-detail-changed',
12
+ clientIdentityChanged: 'client-identity-changed',
13
+ };
14
+
15
+ Object.freeze(InspectorTypes);
16
+
17
+ /**
18
+ * Manages dispatching of inspection data to registered inspectors.
19
+ */
20
+ function InspectorManager(inspectors, logger) {
21
+ const manager = {};
22
+
23
+ /**
24
+ * Collection of inspectors keyed by type.
25
+ * @type {{[type: string]: object[]}}
26
+ */
27
+ const inspectorsByType = {
28
+ [InspectorTypes.flagUsed]: [],
29
+ [InspectorTypes.flagDetailsChanged]: [],
30
+ [InspectorTypes.flagDetailChanged]: [],
31
+ [InspectorTypes.clientIdentityChanged]: [],
32
+ };
33
+
34
+ const safeInspectors = inspectors?.map(inspector => SafeInspector(inspector, logger));
35
+
36
+ safeInspectors.forEach(safeInspector => {
37
+ // Only add inspectors of supported types.
38
+ if (Object.prototype.hasOwnProperty.call(inspectorsByType, safeInspector.type)) {
39
+ inspectorsByType[safeInspector.type].push(safeInspector);
40
+ } else {
41
+ logger.warn(messages.invalidInspector(safeInspector.type, safeInspector.name));
42
+ }
43
+ });
44
+
45
+ /**
46
+ * Check if there is an inspector of a specific type registered.
47
+ *
48
+ * @param {string} type The type of the inspector to check.
49
+ * @returns True if there are any inspectors of that type registered.
50
+ */
51
+ manager.hasListeners = type => inspectorsByType[type]?.length;
52
+
53
+ /**
54
+ * Notify registered inspectors of a flag being used.
55
+ *
56
+ * The notification itself will be dispatched asynchronously.
57
+ *
58
+ * @param {string} flagKey The key for the flag.
59
+ * @param {Object} detail The LDEvaluationDetail for the flag.
60
+ * @param {Object} user The LDUser for the flag.
61
+ */
62
+ manager.onFlagUsed = (flagKey, detail, user) => {
63
+ if (inspectorsByType[InspectorTypes.flagUsed].length) {
64
+ onNextTick(() => {
65
+ inspectorsByType[InspectorTypes.flagUsed].forEach(inspector => inspector.method(flagKey, detail, user));
66
+ });
67
+ }
68
+ };
69
+
70
+ /**
71
+ * Notify registered inspectors that the flags have been replaced.
72
+ *
73
+ * The notification itself will be dispatched asynchronously.
74
+ *
75
+ * @param {Record<string, Object>} flags The current flags as a Record<string, LDEvaluationDetail>.
76
+ */
77
+ manager.onFlags = flags => {
78
+ if (inspectorsByType[InspectorTypes.flagDetailsChanged].length) {
79
+ onNextTick(() => {
80
+ inspectorsByType[InspectorTypes.flagDetailsChanged].forEach(inspector => inspector.method(flags));
81
+ });
82
+ }
83
+ };
84
+
85
+ /**
86
+ * Notify registered inspectors that a flag value has changed.
87
+ *
88
+ * The notification itself will be dispatched asynchronously.
89
+ *
90
+ * @param {string} flagKey The key for the flag that changed.
91
+ * @param {Object} flag An `LDEvaluationDetail` for the flag.
92
+ */
93
+ manager.onFlagChanged = (flagKey, flag) => {
94
+ if (inspectorsByType[InspectorTypes.flagDetailChanged].length) {
95
+ onNextTick(() => {
96
+ console.log('what?');
97
+ inspectorsByType[InspectorTypes.flagDetailChanged].forEach(inspector => inspector.method(flagKey, flag));
98
+ });
99
+ }
100
+ };
101
+
102
+ /**
103
+ * Notify the registered inspectors that the user identity has changed.
104
+ *
105
+ * The notification itself will be dispatched asynchronously.
106
+ *
107
+ * @param {Object} user The `LDUser` which is now identified.
108
+ */
109
+ manager.onIdentityChanged = user => {
110
+ if (inspectorsByType[InspectorTypes.clientIdentityChanged].length) {
111
+ onNextTick(() => {
112
+ inspectorsByType[InspectorTypes.clientIdentityChanged].forEach(inspector => inspector.method(user));
113
+ });
114
+ }
115
+ };
116
+
117
+ return manager;
118
+ }
119
+
120
+ module.exports = { InspectorTypes, InspectorManager };
@@ -0,0 +1,34 @@
1
+ const { messages } = require('.');
2
+
3
+ /**
4
+ * Wrap an inspector ensuring that calling its methods are safe.
5
+ * @param {object} inspector Inspector to wrap.
6
+ */
7
+ function SafeInspector(inspector, logger) {
8
+ let errorLogged = false;
9
+ const wrapper = {
10
+ type: inspector.type,
11
+ name: inspector.name,
12
+ };
13
+
14
+ wrapper.method = (...args) => {
15
+ try {
16
+ inspector.method(...args);
17
+ } catch {
18
+ // If something goes wrong in an inspector we want to log that something
19
+ // went wrong. We don't want to flood the logs, so we only log something
20
+ // the first time that something goes wrong.
21
+ // We do not include the exception in the log, because we do not know what
22
+ // kind of data it may contain.
23
+ if (!errorLogged) {
24
+ errorLogged = true;
25
+ logger.warn(messages.inspectorMethodError(wrapper.type, wrapper.name));
26
+ }
27
+ // Prevent errors.
28
+ }
29
+ };
30
+
31
+ return wrapper;
32
+ }
33
+
34
+ module.exports = SafeInspector;
@@ -0,0 +1,186 @@
1
+ const { AsyncQueue } = require('launchdarkly-js-test-helpers');
2
+ const { InspectorTypes, InspectorManager } = require('../InspectorManager');
3
+ const stubPlatform = require('./stubPlatform');
4
+
5
+ describe('given an inspector manager with no registered inspectors', () => {
6
+ const platform = stubPlatform.defaults();
7
+ const manager = InspectorManager([], platform.testing.logger);
8
+
9
+ it('does not cause errors', () => {
10
+ manager.onIdentityChanged({ key: 'key' });
11
+ manager.onFlagUsed(
12
+ 'flag-key',
13
+ {
14
+ value: null,
15
+ },
16
+ { key: 'key' }
17
+ );
18
+ manager.onFlags({});
19
+ manager.onFlagChanged('flag-key', { value: null });
20
+ });
21
+
22
+ it('does not report any registered listeners', () => {
23
+ expect(manager.hasListeners(InspectorTypes.clientIdentityChanged)).toBeFalsy();
24
+ expect(manager.hasListeners(InspectorTypes.flagDetailChanged)).toBeFalsy();
25
+ expect(manager.hasListeners(InspectorTypes.flagDetailsChanged)).toBeFalsy();
26
+ expect(manager.hasListeners(InspectorTypes.flagUsed)).toBeFalsy();
27
+ expect(manager.hasListeners('potato')).toBeFalsy();
28
+ });
29
+ });
30
+
31
+ describe('given an inspector with callbacks of every type', () => {
32
+ /**
33
+ * @type {AsyncQueue}
34
+ */
35
+ const eventQueue = new AsyncQueue();
36
+ const platform = stubPlatform.defaults();
37
+ const manager = InspectorManager(
38
+ [
39
+ {
40
+ type: 'flag-used',
41
+ name: 'my-flag-used-inspector',
42
+ method: (flagKey, flagDetail, user) => {
43
+ eventQueue.add({ type: 'flag-used', flagKey, flagDetail, user });
44
+ },
45
+ },
46
+ // 'flag-used registered twice.
47
+ {
48
+ type: 'flag-used',
49
+ name: 'my-other-flag-used-inspector',
50
+ method: (flagKey, flagDetail, user) => {
51
+ eventQueue.add({ type: 'flag-used', flagKey, flagDetail, user });
52
+ },
53
+ },
54
+ {
55
+ type: 'flag-details-changed',
56
+ name: 'my-flag-details-inspector',
57
+ method: details => {
58
+ eventQueue.add({
59
+ type: 'flag-details-changed',
60
+ details,
61
+ });
62
+ },
63
+ },
64
+ {
65
+ type: 'flag-detail-changed',
66
+ name: 'my-flag-detail-inspector',
67
+ method: (flagKey, flagDetail) => {
68
+ eventQueue.add({
69
+ type: 'flag-detail-changed',
70
+ flagKey,
71
+ flagDetail,
72
+ });
73
+ },
74
+ },
75
+ {
76
+ type: 'client-identity-changed',
77
+ name: 'my-identity-inspector',
78
+ method: user => {
79
+ eventQueue.add({
80
+ type: 'client-identity-changed',
81
+ user,
82
+ });
83
+ },
84
+ },
85
+ // Invalid inspector shouldn't have an effect.
86
+ {
87
+ type: 'potato',
88
+ name: 'my-potato-inspector',
89
+ method: () => {},
90
+ },
91
+ ],
92
+ platform.testing.logger
93
+ );
94
+
95
+ afterEach(() => {
96
+ expect(eventQueue.length()).toEqual(0);
97
+ });
98
+
99
+ afterAll(() => {
100
+ eventQueue.close();
101
+ });
102
+
103
+ it('logged that there was a bad inspector', () => {
104
+ expect(platform.testing.logger.output.warn).toEqual([
105
+ 'an inspector: "my-potato-inspector" of an invalid type (potato) was configured',
106
+ ]);
107
+ });
108
+
109
+ it('reports any registered listeners', () => {
110
+ expect(manager.hasListeners(InspectorTypes.clientIdentityChanged)).toBeTruthy();
111
+ expect(manager.hasListeners(InspectorTypes.flagDetailChanged)).toBeTruthy();
112
+ expect(manager.hasListeners(InspectorTypes.flagDetailsChanged)).toBeTruthy();
113
+ expect(manager.hasListeners(InspectorTypes.flagUsed)).toBeTruthy();
114
+ expect(manager.hasListeners('potato')).toBeFalsy();
115
+ });
116
+
117
+ it('executes `onFlagUsed` handlers', async () => {
118
+ manager.onFlagUsed(
119
+ 'flag-key',
120
+ {
121
+ value: 'test',
122
+ variationIndex: 1,
123
+ reason: {
124
+ kind: 'OFF',
125
+ },
126
+ },
127
+ { key: 'test-key' }
128
+ );
129
+
130
+ const expectedEvent = {
131
+ type: 'flag-used',
132
+ flagKey: 'flag-key',
133
+ flagDetail: {
134
+ value: 'test',
135
+ variationIndex: 1,
136
+ reason: {
137
+ kind: 'OFF',
138
+ },
139
+ },
140
+ user: { key: 'test-key' },
141
+ };
142
+ const event1 = await eventQueue.take();
143
+ expect(event1).toMatchObject(expectedEvent);
144
+
145
+ // There are two handlers, so there should be another event.
146
+ const event2 = await eventQueue.take();
147
+ expect(event2).toMatchObject(expectedEvent);
148
+ });
149
+
150
+ it('executes `onFlags` handler', async () => {
151
+ manager.onFlags({
152
+ example: { value: 'a-value' },
153
+ });
154
+
155
+ const event = await eventQueue.take();
156
+ expect(event).toMatchObject({
157
+ type: 'flag-details-changed',
158
+ details: {
159
+ example: { value: 'a-value' },
160
+ },
161
+ });
162
+ });
163
+
164
+ it('executes `onFlagChanged` handler', async () => {
165
+ manager.onFlagChanged('the-flag', { value: 'a-value' });
166
+
167
+ const event = await eventQueue.take();
168
+ expect(event).toMatchObject({
169
+ type: 'flag-detail-changed',
170
+ flagKey: 'the-flag',
171
+ flagDetail: {
172
+ value: 'a-value',
173
+ },
174
+ });
175
+ });
176
+
177
+ it('executes `onIdentityChanged` handler', async () => {
178
+ manager.onIdentityChanged({ key: 'the-key' });
179
+
180
+ const event = await eventQueue.take();
181
+ expect(event).toMatchObject({
182
+ type: 'client-identity-changed',
183
+ user: { key: 'the-key' },
184
+ });
185
+ });
186
+ });
@@ -0,0 +1,127 @@
1
+ const { AsyncQueue } = require('launchdarkly-js-test-helpers');
2
+ const { respondJson } = require('./mockHttp');
3
+ const stubPlatform = require('./stubPlatform');
4
+
5
+ const envName = 'UNKNOWN_ENVIRONMENT_ID';
6
+ const user = { key: 'user' };
7
+
8
+ describe('given a streaming client with registered inspectors', () => {
9
+ const eventQueue = new AsyncQueue();
10
+
11
+ const inspectors = [
12
+ {
13
+ type: 'flag-used',
14
+ method: (flagKey, flagDetail, user) => {
15
+ eventQueue.add({ type: 'flag-used', flagKey, flagDetail, user });
16
+ },
17
+ },
18
+ // 'flag-used registered twice.
19
+ {
20
+ type: 'flag-used',
21
+ method: (flagKey, flagDetail, user) => {
22
+ eventQueue.add({ type: 'flag-used', flagKey, flagDetail, user });
23
+ },
24
+ },
25
+ {
26
+ type: 'flag-details-changed',
27
+ method: details => {
28
+ eventQueue.add({
29
+ type: 'flag-details-changed',
30
+ details,
31
+ });
32
+ },
33
+ },
34
+ {
35
+ type: 'flag-detail-changed',
36
+ method: (flagKey, flagDetail) => {
37
+ eventQueue.add({
38
+ type: 'flag-detail-changed',
39
+ flagKey,
40
+ flagDetail,
41
+ });
42
+ },
43
+ },
44
+ {
45
+ type: 'client-identity-changed',
46
+ method: user => {
47
+ eventQueue.add({
48
+ type: 'client-identity-changed',
49
+ user,
50
+ });
51
+ },
52
+ },
53
+ ];
54
+
55
+ let client;
56
+ let platform;
57
+
58
+ beforeEach(async () => {
59
+ platform = stubPlatform.defaults();
60
+ const server = platform.testing.http.newServer();
61
+ server.byDefault(respondJson({}));
62
+ const config = { streaming: true, baseUrl: server.url, inspectors };
63
+ client = platform.testing.makeClient(envName, user, config);
64
+ await client.waitUntilReady();
65
+ });
66
+
67
+ afterEach(() => {
68
+ expect(eventQueue.length()).toEqual(0);
69
+ });
70
+
71
+ afterAll(() => {
72
+ eventQueue.close();
73
+ });
74
+
75
+ afterEach(async () => {
76
+ await client.close();
77
+ });
78
+
79
+ it('has an initial identify event and flag payload', async () => {
80
+ // These events cover the initial identify and a polling response.
81
+ const ident = await eventQueue.take();
82
+ expect(ident).toMatchObject({
83
+ type: 'client-identity-changed',
84
+ user,
85
+ });
86
+ const flagsEvent = await eventQueue.take();
87
+ expect(flagsEvent).toMatchObject({
88
+ type: 'flag-details-changed',
89
+ details: {},
90
+ });
91
+ });
92
+
93
+ it('emits an event for the stream put replacing all flags', async () => {
94
+ // Take initial events.
95
+ eventQueue.take();
96
+ eventQueue.take();
97
+
98
+ const stream = await platform.testing.eventSourcesCreated.take();
99
+ stream.eventSource.mockEmit('put', {
100
+ data: '{"flagKey":{"value":true,"version":1}}',
101
+ });
102
+ const updateEvent = await eventQueue.take();
103
+ expect(updateEvent).toMatchObject({
104
+ type: 'flag-details-changed',
105
+ details: {
106
+ flagKey: { value: true },
107
+ },
108
+ });
109
+ });
110
+
111
+ it('emits an event for a stream patch changing a flag', async () => {
112
+ // Take initial events.
113
+ eventQueue.take();
114
+ eventQueue.take();
115
+
116
+ const stream = await platform.testing.eventSourcesCreated.take();
117
+ stream.eventSource.mockEmit('patch', {
118
+ data: '{"key": "flagKey", "value":false,"version":2}',
119
+ });
120
+ const updateEvent = await eventQueue.take();
121
+ expect(updateEvent).toMatchObject({
122
+ type: 'flag-detail-changed',
123
+ flagKey: 'flagKey',
124
+ flagDetail: { value: false },
125
+ });
126
+ });
127
+ });
@@ -0,0 +1,61 @@
1
+ const SafeInspector = require('../SafeInspector');
2
+ const stubPlatform = require('./stubPlatform');
3
+
4
+ describe('given a safe inspector', () => {
5
+ const platform = stubPlatform.defaults();
6
+ const mockInspector = {
7
+ type: 'the-inspector-type',
8
+ name: 'the-inspector-name',
9
+ method: () => {
10
+ throw new Error('evil inspector');
11
+ },
12
+ };
13
+ const safeInspector = SafeInspector(mockInspector, platform.testing.logger);
14
+
15
+ it('has the correct type', () => {
16
+ expect(safeInspector.type).toEqual('the-inspector-type');
17
+ });
18
+
19
+ it('does not allow exceptions to propagate', () => {
20
+ safeInspector.method();
21
+ });
22
+
23
+ it('only logs one error', () => {
24
+ safeInspector.method();
25
+ safeInspector.method();
26
+ expect(platform.testing.logger.output.warn).toEqual([
27
+ 'an inspector: "the-inspector-name" of type: "the-inspector-type" generated an exception',
28
+ ]);
29
+ });
30
+ });
31
+
32
+ // Type and name are required by the schema, but it should operate fine if they are not specified.
33
+ describe('given a safe inspector with no name or type', () => {
34
+ const platform = stubPlatform.defaults();
35
+ const mockInspector = {
36
+ method: () => {
37
+ throw new Error('evil inspector');
38
+ },
39
+ };
40
+ const safeInspector = SafeInspector(mockInspector, platform.testing.logger);
41
+
42
+ it('has undefined type', () => {
43
+ expect(safeInspector.type).toBeUndefined();
44
+ });
45
+
46
+ it('has undefined name', () => {
47
+ expect(safeInspector.name).toBeUndefined();
48
+ });
49
+
50
+ it('does not allow exceptions to propagate', () => {
51
+ safeInspector.method();
52
+ });
53
+
54
+ it('only logs one error', () => {
55
+ safeInspector.method();
56
+ safeInspector.method();
57
+ expect(platform.testing.logger.output.warn).toEqual([
58
+ 'an inspector: "undefined" of type: "undefined" generated an exception',
59
+ ]);
60
+ });
61
+ });
@@ -44,26 +44,24 @@ describe('configuration', () => {
44
44
  await listener.expectNoErrors();
45
45
  }
46
46
 
47
- // As of the latest major version, there are no deprecated options. This logic can be restored
48
- // the next time we deprecate something.
49
- // function checkDeprecated(oldName, newName, value) {
50
- // const desc = newName
51
- // ? 'allows "' + oldName + '" as a deprecated equivalent to "' + newName + '"'
52
- // : 'warns that "' + oldName + '" is deprecated';
53
- // it(desc, async () => {
54
- // const listener = errorListener();
55
- // const config0 = {};
56
- // config0[oldName] = value;
57
- // const config1 = configuration.validate(config0, listener.emitter, null, listener.logger);
58
- // if (newName) {
59
- // expect(config1[newName]).toBe(value);
60
- // expect(config1[oldName]).toBeUndefined();
61
- // } else {
62
- // expect(config1[oldName]).toEqual(value);
63
- // }
64
- // await listener.expectWarningOnly(messages.deprecated(oldName, newName));
65
- // });
66
- // }
47
+ function checkDeprecated(oldName, newName, value) {
48
+ const desc = newName
49
+ ? 'allows "' + oldName + '" as a deprecated equivalent to "' + newName + '"'
50
+ : 'warns that "' + oldName + '" is deprecated';
51
+ it(desc, async () => {
52
+ const listener = errorListener();
53
+ const config0 = {};
54
+ config0[oldName] = value;
55
+ const config1 = configuration.validate(config0, listener.emitter, null, listener.logger);
56
+ if (newName) {
57
+ expect(config1[newName]).toBe(value);
58
+ expect(config1[oldName]).toBeUndefined();
59
+ } else {
60
+ expect(config1[oldName]).toEqual(value);
61
+ }
62
+ await listener.expectWarningOnly(messages.deprecated(oldName, newName));
63
+ });
64
+ }
67
65
 
68
66
  function checkBooleanProperty(name) {
69
67
  it('enforces boolean type and default for "' + name + '"', async () => {
@@ -103,13 +101,14 @@ describe('configuration', () => {
103
101
  checkBooleanProperty('allAttributesPrivate');
104
102
  checkBooleanProperty('sendLDHeaders');
105
103
  checkBooleanProperty('inlineUsersInEvents');
106
- checkBooleanProperty('allowFrequentDuplicateEvents');
107
104
  checkBooleanProperty('sendEventsOnlyForVariation');
108
105
  checkBooleanProperty('useReport');
109
106
  checkBooleanProperty('evaluationReasons');
110
107
  checkBooleanProperty('diagnosticOptOut');
111
108
  checkBooleanProperty('streaming');
112
109
 
110
+ checkDeprecated('allowFrequentDuplicateEvents', undefined, true);
111
+
113
112
  function checkNumericProperty(name, validValue) {
114
113
  it('enforces numeric type and default for "' + name + '"', async () => {
115
114
  await expectDefault(name);
@@ -101,7 +101,6 @@ describe('DiagnosticsManager', () => {
101
101
  };
102
102
  const defaultConfigInEvent = {
103
103
  allAttributesPrivate: false,
104
- allowFrequentDuplicateEvents: false,
105
104
  autoAliasingOptOut: false,
106
105
  bootstrapMode: false,
107
106
  customBaseURI: false,
@@ -189,7 +188,6 @@ describe('DiagnosticsManager', () => {
189
188
  it('sends init event on start() with custom config', async () => {
190
189
  const configAndResultValues = [
191
190
  [{ allAttributesPrivate: true }, { allAttributesPrivate: true }],
192
- [{ allowFrequentDuplicateEvents: true }, { allowFrequentDuplicateEvents: true }],
193
191
  [{ bootstrap: {} }, { bootstrapMode: true }],
194
192
  [{ baseUrl: 'http://other' }, { customBaseURI: true }],
195
193
  [{ eventsUrl: 'http://other' }, { customEventsURI: true }],
@@ -39,6 +39,7 @@ const baseOptionDefs = {
39
39
  stateProvider: { type: 'object' }, // not a public option, used internally
40
40
  autoAliasingOptOut: { default: false },
41
41
  application: { validator: applicationConfigValidator },
42
+ inspectors: { default: [] },
42
43
  };
43
44
 
44
45
  /**
@@ -78,9 +79,10 @@ function validate(options, emitter, extraOptionDefs, logger) {
78
79
  const optionDefs = utils.extend({ logger: { default: logger } }, baseOptionDefs, extraOptionDefs);
79
80
 
80
81
  const deprecatedOptions = {
81
- // As of the latest major version, there are no deprecated options. Next time we deprecate
82
- // something, add an item here where the property name is the deprecated name, and the
83
- // property value is the preferred name if any, or null/undefined if there is no replacement.
82
+ // The property name is the deprecated name, and the property value is the preferred name if
83
+ // any, or null/undefined if there is no replacement. This should be removed, along with
84
+ // the option, in the next major version.
85
+ allowFrequentDuplicateEvents: undefined,
84
86
  };
85
87
 
86
88
  function checkDeprecatedOptions(config) {
@@ -201,7 +201,6 @@ function DiagnosticsManager(
201
201
  usingSecureMode: !!config.hash,
202
202
  bootstrapMode: !!config.bootstrap,
203
203
  fetchGoalsDisabled: !config.fetchGoals,
204
- allowFrequentDuplicateEvents: !!config.allowFrequentDuplicateEvents,
205
204
  sendEventsOnlyForVariation: !!config.sendEventsOnlyForVariation,
206
205
  autoAliasingOptOut: !!config.autoAliasingOptOut,
207
206
  };
package/src/index.js CHANGED
@@ -14,6 +14,7 @@ const { commonBasicLogger } = require('./loggers');
14
14
  const utils = require('./utils');
15
15
  const errors = require('./errors');
16
16
  const messages = require('./messages');
17
+ const { InspectorTypes, InspectorManager } = require('./InspectorManager');
17
18
 
18
19
  const changeEvent = 'change';
19
20
  const internalChangeEvent = 'internal-change';
@@ -32,6 +33,7 @@ function initialize(env, user, specifiedOptions, platform, extraOptionDefs) {
32
33
  const emitter = EventEmitter(logger);
33
34
  const initializationStateTracker = InitializationStateTracker(emitter);
34
35
  const options = configuration.validate(specifiedOptions, emitter, extraOptionDefs, logger);
36
+ const inspectorManager = InspectorManager(options.inspectors, logger);
35
37
  const sendEvents = options.sendEvents;
36
38
  let environment = env;
37
39
  let hash = options.hash;
@@ -63,7 +65,6 @@ function initialize(env, user, specifiedOptions, platform, extraOptionDefs) {
63
65
 
64
66
  const requestor = Requestor(platform, options, environment);
65
67
 
66
- const seenRequests = {};
67
68
  let flags = {};
68
69
  let useLocalStorage;
69
70
  let streamActive;
@@ -155,11 +156,38 @@ function initialize(env, user, specifiedOptions, platform, extraOptionDefs) {
155
156
  }
156
157
  }
157
158
 
159
+ function notifyInspectionIdentityChanged() {
160
+ if (inspectorManager.hasListeners(InspectorTypes.clientIdentityChanged)) {
161
+ inspectorManager.onIdentityChanged(ident.getUser());
162
+ }
163
+ }
164
+
165
+ function notifyInspectionFlagChanged(data, newFlag) {
166
+ if (inspectorManager.hasListeners(InspectorTypes.flagDetailChanged)) {
167
+ inspectorManager.onFlagChanged(data.key, getFlagDetail(newFlag));
168
+ }
169
+ }
170
+
171
+ function notifyInspectionFlagsChanged() {
172
+ if (inspectorManager.hasListeners(InspectorTypes.flagDetailsChanged)) {
173
+ inspectorManager.onFlags(
174
+ Object.entries(flags)
175
+ .map(([key, value]) => ({ key, detail: getFlagDetail(value) }))
176
+ .reduce((acc, cur) => {
177
+ // eslint-disable-next-line no-param-reassign
178
+ acc[cur.key] = cur.detail;
179
+ return acc;
180
+ }, {})
181
+ );
182
+ }
183
+ }
184
+
158
185
  function onIdentifyChange(user, previousUser) {
159
186
  sendIdentifyEvent(user);
160
187
  if (!options.autoAliasingOptOut && previousUser && previousUser.anonymous && user && !user.anonymous) {
161
188
  alias(user, previousUser);
162
189
  }
190
+ notifyInspectionIdentityChanged();
163
191
  }
164
192
 
165
193
  function sendIdentifyEvent(user) {
@@ -181,15 +209,6 @@ function initialize(env, user, specifiedOptions, platform, extraOptionDefs) {
181
209
  const user = ident.getUser();
182
210
  const now = new Date();
183
211
  const value = detail ? detail.value : null;
184
- if (!options.allowFrequentDuplicateEvents) {
185
- const cacheKey = JSON.stringify(value) + (user && user.key ? user.key : '') + key; // see below
186
- const cached = seenRequests[cacheKey];
187
- // cache TTL is five minutes
188
- if (cached && now - cached < 300000) {
189
- return;
190
- }
191
- seenRequests[cacheKey] = now;
192
- }
193
212
 
194
213
  const event = {
195
214
  kind: 'feature',
@@ -267,14 +286,14 @@ function initialize(env, user, specifiedOptions, platform, extraOptionDefs) {
267
286
  }
268
287
 
269
288
  function variation(key, defaultValue) {
270
- return variationDetailInternal(key, defaultValue, true, false).value;
289
+ return variationDetailInternal(key, defaultValue, true, false, false).value;
271
290
  }
272
291
 
273
292
  function variationDetail(key, defaultValue) {
274
- return variationDetailInternal(key, defaultValue, true, true);
293
+ return variationDetailInternal(key, defaultValue, true, true, false);
275
294
  }
276
295
 
277
- function variationDetailInternal(key, defaultValue, sendEvent, includeReasonInEvent) {
296
+ function variationDetailInternal(key, defaultValue, sendEvent, includeReasonInEvent, isAllFlags) {
278
297
  let detail;
279
298
 
280
299
  if (flags && utils.objectHasOwnProperty(flags, key) && flags[key] && !flags[key].deleted) {
@@ -291,6 +310,11 @@ function initialize(env, user, specifiedOptions, platform, extraOptionDefs) {
291
310
  sendFlagEvent(key, detail, defaultValue, includeReasonInEvent);
292
311
  }
293
312
 
313
+ // For the all flags case `onFlags` will be called instead.
314
+ if (!isAllFlags) {
315
+ notifyInspectionIdentityChanged();
316
+ }
317
+
294
318
  return detail;
295
319
  }
296
320
 
@@ -314,7 +338,7 @@ function initialize(env, user, specifiedOptions, platform, extraOptionDefs) {
314
338
 
315
339
  for (const key in flags) {
316
340
  if (utils.objectHasOwnProperty(flags, key) && !flags[key].deleted) {
317
- results[key] = variationDetailInternal(key, null, !options.sendEventsOnlyForVariation).value;
341
+ results[key] = variationDetailInternal(key, null, !options.sendEventsOnlyForVariation, false, true).value;
318
342
  }
319
343
  }
320
344
 
@@ -443,6 +467,7 @@ function initialize(env, user, specifiedOptions, platform, extraOptionDefs) {
443
467
  mods[data.key] = { current: newDetail };
444
468
  }
445
469
  handleFlagChanges(mods); // don't wait for this Promise to be resolved
470
+ notifyInspectionFlagChanged(data, newFlag);
446
471
  } else {
447
472
  logger.debug(messages.debugStreamPatchIgnored(data.key));
448
473
  }
@@ -459,6 +484,7 @@ function initialize(env, user, specifiedOptions, platform, extraOptionDefs) {
459
484
  mods[data.key] = { previous: flags[data.key].value };
460
485
  }
461
486
  flags[data.key] = { version: data.version, deleted: true };
487
+ notifyInspectionFlagChanged(data, flags[data.key]);
462
488
  handleFlagChanges(mods); // don't wait for this Promise to be resolved
463
489
  } else {
464
490
  logger.debug(messages.debugStreamDeleteIgnored(data.key));
@@ -500,6 +526,9 @@ function initialize(env, user, specifiedOptions, platform, extraOptionDefs) {
500
526
  }
501
527
 
502
528
  flags = { ...newFlags };
529
+
530
+ notifyInspectionFlagsChanged();
531
+
503
532
  return handleFlagChanges(changes).catch(() => {}); // swallow any exceptions from this Promise
504
533
  }
505
534
 
@@ -673,6 +702,8 @@ function initialize(env, user, specifiedOptions, platform, extraOptionDefs) {
673
702
  .fetchFlagSettings(ident.getUser(), hash)
674
703
  .then(requestedFlags => {
675
704
  flags = requestedFlags || {};
705
+
706
+ notifyInspectionFlagsChanged();
676
707
  // Note, we don't need to call updateSettings here because local storage and change events are not relevant
677
708
  signalSuccessfulInit();
678
709
  })
package/src/messages.js CHANGED
@@ -180,6 +180,10 @@ const debugPostingDiagnosticEvent = function(event) {
180
180
  return 'sending diagnostic event (' + event.kind + ')';
181
181
  };
182
182
 
183
+ const invalidInspector = (type, name) => `an inspector: "${name}" of an invalid type (${type}) was configured`;
184
+
185
+ const inspectorMethodError = (type, name) => `an inspector: "${name}" of type: "${type}" generated an exception`;
186
+
183
187
  const invalidTagValue = name => `Config option "${name}" must only contain letters, numbers, ., _ or -.`;
184
188
 
185
189
  const tagValueTooLong = name => `Value of "${name}" was longer than 64 characters and was discarded.`;
@@ -208,8 +212,10 @@ module.exports = {
208
212
  httpErrorMessage,
209
213
  httpUnavailable,
210
214
  identifyDisabled,
215
+ inspectorMethodError,
211
216
  invalidContentType,
212
217
  invalidData,
218
+ invalidInspector,
213
219
  invalidKey,
214
220
  invalidTagValue,
215
221
  invalidUser,
package/typings.d.ts CHANGED
@@ -173,10 +173,13 @@ declare module 'launchdarkly-js-sdk-common' {
173
173
  inlineUsersInEvents?: boolean;
174
174
 
175
175
  /**
176
- * Whether or not to send an analytics event for a flag evaluation even if the same flag was
177
- * evaluated with the same value within the last five minutes.
176
+ * This option is deprecated, and setting it has no effect.
178
177
  *
179
- * By default, this is false (duplicate events within five minutes will be dropped).
178
+ * The behavior is now to allow frequent duplicate events.
179
+ *
180
+ * This is not a problem because most events will be summarized, and
181
+ * events which are not summarized are important to the operation of features such as
182
+ * experimentation.
180
183
  */
181
184
  allowFrequentDuplicateEvents?: boolean;
182
185
 
@@ -284,6 +287,11 @@ declare module 'launchdarkly-js-sdk-common' {
284
287
  */
285
288
  version?: string;
286
289
  }
290
+
291
+ /**
292
+ * Inspectors can be used for collecting information for monitoring, analytics, and debugging.
293
+ */
294
+ inspectors?: LDInspection[];
287
295
  }
288
296
 
289
297
  /**
@@ -818,4 +826,105 @@ declare module 'launchdarkly-js-sdk-common' {
818
826
  * You can also specify `'none'` instead to disable all logging.
819
827
  */
820
828
  export type LDLogLevel = 'debug' | 'info' | 'warn' | 'error' | 'none';
829
+
830
+ /**
831
+ * Callback interface for collecting information about the SDK at runtime.
832
+ *
833
+ * This interface is used to collect information about flag usage.
834
+ *
835
+ * This interface should not be used by the application to access flags for the purpose of controlling application
836
+ * flow. It is intended for monitoring, analytics, or debugging purposes.
837
+ */
838
+ interface LDInspectionFlagUsedHandler {
839
+ type: 'flag-used',
840
+
841
+ /**
842
+ * Name of the inspector. Will be used for logging issues with the inspector.
843
+ */
844
+ name: string,
845
+
846
+ /**
847
+ * This method is called when a flag is accessed via a variation method, or it can be called based on actions in
848
+ * wrapper SDKs which have different methods of tracking when a flag was accessed. It is not called when a call is made
849
+ * to allFlags.
850
+ */
851
+ method: (flagKey: string, flagDetail: LDEvaluationDetail, user: LDUser) => void;
852
+ }
853
+
854
+ /**
855
+ * Callback interface for collecting information about the SDK at runtime.
856
+ *
857
+ * This interface is used to collect information about flag data. In order to understand the
858
+ * current flag state it should be combined with {@link LDInspectionFlagValueChangedHandler}.
859
+ * This interface will get the initial flag information, and
860
+ * {@link LDInspectionFlagValueChangedHandler} will provide changes to individual flags.
861
+ *
862
+ * This interface should not be used by the application to access flags for the purpose of controlling application
863
+ * flow. It is intended for monitoring, analytics, or debugging purposes.
864
+ */
865
+ interface LDInspectionFlagDetailsChangedHandler {
866
+ type: 'flag-details-changed',
867
+
868
+ /**
869
+ * Name of the inspector. Will be used for logging issues with the inspector.
870
+ */
871
+ name: string,
872
+
873
+ /**
874
+ * This method is called when the flags in the store are replaced with new flags. It will contain all flags
875
+ * regardless of if they have been evaluated.
876
+ */
877
+ method: (details: Record<string, LDEvaluationDetail>) => void;
878
+ }
879
+
880
+
881
+ /**
882
+ * Callback interface for collecting information about the SDK at runtime.
883
+ *
884
+ * This interface is used to collect changes to flag data, but does not provide the initial
885
+ * data. It can be combined with {@link LDInspectionFlagValuesChangedHandler} to track the
886
+ * entire flag state.
887
+ *
888
+ * This interface should not be used by the application to access flags for the purpose of controlling application
889
+ * flow. It is intended for monitoring, analytics, or debugging purposes.
890
+ */
891
+ interface LDInspectionFlagDetailChangedHandler {
892
+ type: 'flag-detail-changed',
893
+
894
+ /**
895
+ * Name of the inspector. Will be used for logging issues with the inspector.
896
+ */
897
+ name: string,
898
+
899
+ /**
900
+ * This method is called when a flag is updated. It will not be called
901
+ * when all flags are updated.
902
+ */
903
+ method: (flagKey: string, detail: LDEvaluationDetail) => void;
904
+ }
905
+
906
+ /**
907
+ * Callback interface for collecting information about the SDK at runtime.
908
+ *
909
+ * This interface is used to track current identity state of the SDK.
910
+ *
911
+ * This interface should not be used by the application to access flags for the purpose of controlling application
912
+ * flow. It is intended for monitoring, analytics, or debugging purposes.
913
+ */
914
+ interface LDInspectionIdentifyHandler {
915
+ type: 'client-identity-changed',
916
+
917
+ /**
918
+ * Name of the inspector. Will be used for logging issues with the inspector.
919
+ */
920
+ name: string,
921
+
922
+ /**
923
+ * This method will be called when an identify operation completes.
924
+ */
925
+ method: (user: LDUser) => void;
926
+ }
927
+
928
+ type LDInspection = LDInspectionFlagUsedHandler | LDInspectionFlagDetailsChangedHandler
929
+ | LDInspectionFlagDetailChangedHandler | LDInspectionIdentifyHandler;
821
930
  }