Skip to content

Commit a00a689

Browse files
LaunchDarklyReleaseBoteli-darklyzmdavisLaunchDarklyCIbwoskow-ld
authored
prepare 3.8.0 release (#74)
* initial move of code from js-client-sdk-private * changelog note * rm obsolete comment * add npm audit helper * update babel, jest, rollup * fix rollup config * fix ES build, dependency cleanup * add Releaser metadata * Update babel config to work in `test` without `useBuiltIns` * copyedits * fix misnamed directory * use spread operator instead of Object.assign * add issue templates * add babel-eslint * add event capacity config property * re-add deprecation warning on samplingInterval * better config validation * remove rollup-plugins-node-resolve * use newer Rollup node-resolve plugin * rm rollup-plugin-includepaths (unused) * npm audit fix (handlebars dependency from jest) * comment * copyedit * use new test helpers + misc test cleanup * clean up stream testing logic * fix hash parameter * linter * clearer way to model the config option defaults/types * test improvements * change internal param name * comment * fix default logger logic * simpler way to enforce minimum values * implement diagnostic events in common JS package (#11) * add support for function type in config options * add support for function type in config options (#13) * add wrapper metadata options and fix custom header logic * lint * lint * remove image-loading logic from common code, replace it with an abstraction * add validation for options.streaming * typo * rm unused params * typo in comment * misc fixes to merged code from external PR * add event payload ID header * npm audit fix * change exact dependencies to best-compatible * standardize linting * disallow "window" and "document" * improve diag event tests + debug logging * misc cleanup * fix updating secure mode hash with identify() * don't omit streamInits.failed when it's false * clean up init state logic, prevent unhandled rejections * lint * less strict matching of json content-type header * remove unsafe usage of hasOwnProperty * console logger must tolerate console object not always existing * fix double initialization of diagnostics manager * fix TypeScript declaration for track() and add more TS compilation tests (#27) * remove startsWith usage (#28) * prevent poll caused by a stream ping from overwriting later poll for another user (#29) * upgrade jest dependency and transitive yargs-parser dependency (#30) * Add null to LDEvaluationDetail.reason type (#31) * Revert "Add null to LDEvaluationDetail.reason type (#31)" This reverts commit bcb1573. * Revert "Add null to LDEvaluationDetail.reason type (#31)" This reverts commit bcb1573. * nullable evaluation reason (#32) * adding alias event functionality (#33) * set stream read timeout * Add prepare script (#34) * add a missing typescript verification (#36) * Removed the guides link * Correct doc link (#36) * Fix typo in LDClient.on jsdoc (#37) * add inlineUsersInEvents option in TypeScript (#37) * Filter private attributes on debug event users. Send variation for debug events. * update uuid package version (#39) * use Releaser v2 config + newer CI image * First half, add the type, create the new options, add the new util method, and add tests * Second half, call the tranform util method before calling any HTTP requests * Update the transform to work on a copy of headers instead of mutating it * add comments about removing custom event warning logic in the future * revert updating of UUID dependency (#43) * Revert "update uuid package version (#39)" This reverts commit 3b2ff6c. * update package-lock.json * better error handling for local storage operations (#44) * better error handling for local storage operations * lint * fix obsolete comments * add basic logger similar to server-side Node SDK (#45) * fix exports and add validation of custom logger (#46) * remove typedoc.js file that interferes with Releaser's docs build * update typescript version * add maintenance branch * backport sc-142333 fix * make URL path concatenation work right whether base URL has a trailing slash or not (#61) * make URL path concatenation work right whether base URL has a trailing slash or not * lint * Implement application tags for 3.x. (#62) * don't include deleted flags in allFlags (#66) * Backport changes to seen request cache. (#74) * Backport changes for inspectors V2 and tag length enforcement. (#76) Co-authored-by: Eli Bishop <eli@launchdarkly.com> Co-authored-by: Zach Davis <zach@launchdarkly.com> Co-authored-by: LaunchDarklyCI <dev@launchdarkly.com> Co-authored-by: Ben Woskow <bwoskow@launchdarkly.com> Co-authored-by: Ben Woskow <48036130+bwoskow-ld@users.noreply.github.com> Co-authored-by: Michael Siadak <mike.siadak@gmail.com> Co-authored-by: Jeff Wen <sinchangwen@gmail.com> Co-authored-by: Andrey Krasnov <34657799+Doesntmeananything@users.noreply.github.com> Co-authored-by: Gavin Whelan <gwhelan@launchdarkly.com> Co-authored-by: LaunchDarklyReleaseBot <launchdarklyreleasebot@launchdarkly.com> Co-authored-by: Louis Chan <lchan@launchdarkly.com> Co-authored-by: Louis Chan <91093020+louis-launchdarkly@users.noreply.github.com> Co-authored-by: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com>
1 parent 9b82030 commit a00a689

File tree

11 files changed

+700
-10
lines changed

11 files changed

+700
-10
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ lib
99
.vscode/
1010
test-types.js
1111
docs/build/
12+
package-lock.json

src/InspectorManager.js

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import * as messages from './messages';
2+
import SafeInspector from './SafeInspector';
3+
import { onNextTick } from './utils';
4+
5+
/**
6+
* The types of supported inspectors.
7+
*/
8+
export 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+
export 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+
}

src/SafeInspector.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import * as messages from './messages';
2+
3+
/**
4+
* Wrap an inspector ensuring that calling its methods are safe.
5+
* @param {object} inspector Inspector to wrap.
6+
*/
7+
export default 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+
}
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
import { AsyncQueue } from 'launchdarkly-js-test-helpers';
2+
import { InspectorTypes, InspectorManager } from '../InspectorManager';
3+
import * as stubPlatform from './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 commit comments

Comments
 (0)