launchdarkly-js-sdk-common 4.2.0 → 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,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
+ ## [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
+
5
12
  ## [4.1.1] - 2022-06-07
6
13
  ### Changed:
7
14
  - Enforce a 64 character limit for `application.id` and `application.version` configuration options.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "launchdarkly-js-sdk-common",
3
- "version": "4.2.0",
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
+ });
@@ -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
  /**
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;
@@ -154,11 +156,38 @@ function initialize(env, user, specifiedOptions, platform, extraOptionDefs) {
154
156
  }
155
157
  }
156
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
+
157
185
  function onIdentifyChange(user, previousUser) {
158
186
  sendIdentifyEvent(user);
159
187
  if (!options.autoAliasingOptOut && previousUser && previousUser.anonymous && user && !user.anonymous) {
160
188
  alias(user, previousUser);
161
189
  }
190
+ notifyInspectionIdentityChanged();
162
191
  }
163
192
 
164
193
  function sendIdentifyEvent(user) {
@@ -257,14 +286,14 @@ function initialize(env, user, specifiedOptions, platform, extraOptionDefs) {
257
286
  }
258
287
 
259
288
  function variation(key, defaultValue) {
260
- return variationDetailInternal(key, defaultValue, true, false).value;
289
+ return variationDetailInternal(key, defaultValue, true, false, false).value;
261
290
  }
262
291
 
263
292
  function variationDetail(key, defaultValue) {
264
- return variationDetailInternal(key, defaultValue, true, true);
293
+ return variationDetailInternal(key, defaultValue, true, true, false);
265
294
  }
266
295
 
267
- function variationDetailInternal(key, defaultValue, sendEvent, includeReasonInEvent) {
296
+ function variationDetailInternal(key, defaultValue, sendEvent, includeReasonInEvent, isAllFlags) {
268
297
  let detail;
269
298
 
270
299
  if (flags && utils.objectHasOwnProperty(flags, key) && flags[key] && !flags[key].deleted) {
@@ -281,6 +310,11 @@ function initialize(env, user, specifiedOptions, platform, extraOptionDefs) {
281
310
  sendFlagEvent(key, detail, defaultValue, includeReasonInEvent);
282
311
  }
283
312
 
313
+ // For the all flags case `onFlags` will be called instead.
314
+ if (!isAllFlags) {
315
+ notifyInspectionIdentityChanged();
316
+ }
317
+
284
318
  return detail;
285
319
  }
286
320
 
@@ -304,7 +338,7 @@ function initialize(env, user, specifiedOptions, platform, extraOptionDefs) {
304
338
 
305
339
  for (const key in flags) {
306
340
  if (utils.objectHasOwnProperty(flags, key) && !flags[key].deleted) {
307
- results[key] = variationDetailInternal(key, null, !options.sendEventsOnlyForVariation).value;
341
+ results[key] = variationDetailInternal(key, null, !options.sendEventsOnlyForVariation, false, true).value;
308
342
  }
309
343
  }
310
344
 
@@ -433,6 +467,7 @@ function initialize(env, user, specifiedOptions, platform, extraOptionDefs) {
433
467
  mods[data.key] = { current: newDetail };
434
468
  }
435
469
  handleFlagChanges(mods); // don't wait for this Promise to be resolved
470
+ notifyInspectionFlagChanged(data, newFlag);
436
471
  } else {
437
472
  logger.debug(messages.debugStreamPatchIgnored(data.key));
438
473
  }
@@ -449,6 +484,7 @@ function initialize(env, user, specifiedOptions, platform, extraOptionDefs) {
449
484
  mods[data.key] = { previous: flags[data.key].value };
450
485
  }
451
486
  flags[data.key] = { version: data.version, deleted: true };
487
+ notifyInspectionFlagChanged(data, flags[data.key]);
452
488
  handleFlagChanges(mods); // don't wait for this Promise to be resolved
453
489
  } else {
454
490
  logger.debug(messages.debugStreamDeleteIgnored(data.key));
@@ -490,6 +526,9 @@ function initialize(env, user, specifiedOptions, platform, extraOptionDefs) {
490
526
  }
491
527
 
492
528
  flags = { ...newFlags };
529
+
530
+ notifyInspectionFlagsChanged();
531
+
493
532
  return handleFlagChanges(changes).catch(() => {}); // swallow any exceptions from this Promise
494
533
  }
495
534
 
@@ -663,6 +702,8 @@ function initialize(env, user, specifiedOptions, platform, extraOptionDefs) {
663
702
  .fetchFlagSettings(ident.getUser(), hash)
664
703
  .then(requestedFlags => {
665
704
  flags = requestedFlags || {};
705
+
706
+ notifyInspectionFlagsChanged();
666
707
  // Note, we don't need to call updateSettings here because local storage and change events are not relevant
667
708
  signalSuccessfulInit();
668
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
@@ -287,6 +287,11 @@ declare module 'launchdarkly-js-sdk-common' {
287
287
  */
288
288
  version?: string;
289
289
  }
290
+
291
+ /**
292
+ * Inspectors can be used for collecting information for monitoring, analytics, and debugging.
293
+ */
294
+ inspectors?: LDInspection[];
290
295
  }
291
296
 
292
297
  /**
@@ -821,4 +826,105 @@ declare module 'launchdarkly-js-sdk-common' {
821
826
  * You can also specify `'none'` instead to disable all logging.
822
827
  */
823
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;
824
930
  }