Skip to content

Commit c8685e8

Browse files
authored
Inspector proposal V2. (#71)
1 parent 26dd615 commit c8685e8

9 files changed

+686
-4
lines changed

src/InspectorManager.js

+120
Original file line numberDiff line numberDiff line change
@@ -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 };

src/SafeInspector.js

+34
Original file line numberDiff line numberDiff line change
@@ -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;
+186
Original file line numberDiff line numberDiff line change
@@ -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 commit comments

Comments
 (0)