-
Notifications
You must be signed in to change notification settings - Fork 27
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Update U2C branch with inspection interfaces. (#80)
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> Co-authored-by: LaunchDarklyReleaseBot <86431345+LaunchDarklyReleaseBot@users.noreply.github.com>
- Loading branch information
1 parent
c6ca9d2
commit 1ce97b2
Showing
11 changed files
with
709 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
const { messages } = require('.'); | ||
const SafeInspector = require('./SafeInspector'); | ||
const { onNextTick } = require('./utils'); | ||
|
||
/** | ||
* The types of supported inspectors. | ||
*/ | ||
const InspectorTypes = { | ||
flagUsed: 'flag-used', | ||
flagDetailsChanged: 'flag-details-changed', | ||
flagDetailChanged: 'flag-detail-changed', | ||
clientIdentityChanged: 'client-identity-changed', | ||
}; | ||
|
||
Object.freeze(InspectorTypes); | ||
|
||
/** | ||
* Manages dispatching of inspection data to registered inspectors. | ||
*/ | ||
function InspectorManager(inspectors, logger) { | ||
const manager = {}; | ||
|
||
/** | ||
* Collection of inspectors keyed by type. | ||
* @type {{[type: string]: object[]}} | ||
*/ | ||
const inspectorsByType = { | ||
[InspectorTypes.flagUsed]: [], | ||
[InspectorTypes.flagDetailsChanged]: [], | ||
[InspectorTypes.flagDetailChanged]: [], | ||
[InspectorTypes.clientIdentityChanged]: [], | ||
}; | ||
|
||
const safeInspectors = inspectors?.map(inspector => SafeInspector(inspector, logger)); | ||
|
||
safeInspectors.forEach(safeInspector => { | ||
// Only add inspectors of supported types. | ||
if (Object.prototype.hasOwnProperty.call(inspectorsByType, safeInspector.type)) { | ||
inspectorsByType[safeInspector.type].push(safeInspector); | ||
} else { | ||
logger.warn(messages.invalidInspector(safeInspector.type, safeInspector.name)); | ||
} | ||
}); | ||
|
||
/** | ||
* Check if there is an inspector of a specific type registered. | ||
* | ||
* @param {string} type The type of the inspector to check. | ||
* @returns True if there are any inspectors of that type registered. | ||
*/ | ||
manager.hasListeners = type => inspectorsByType[type]?.length; | ||
|
||
/** | ||
* Notify registered inspectors of a flag being used. | ||
* | ||
* The notification itself will be dispatched asynchronously. | ||
* | ||
* @param {string} flagKey The key for the flag. | ||
* @param {Object} detail The LDEvaluationDetail for the flag. | ||
* @param {Object} user The LDUser for the flag. | ||
*/ | ||
manager.onFlagUsed = (flagKey, detail, user) => { | ||
if (inspectorsByType[InspectorTypes.flagUsed].length) { | ||
onNextTick(() => { | ||
inspectorsByType[InspectorTypes.flagUsed].forEach(inspector => inspector.method(flagKey, detail, user)); | ||
}); | ||
} | ||
}; | ||
|
||
/** | ||
* Notify registered inspectors that the flags have been replaced. | ||
* | ||
* The notification itself will be dispatched asynchronously. | ||
* | ||
* @param {Record<string, Object>} flags The current flags as a Record<string, LDEvaluationDetail>. | ||
*/ | ||
manager.onFlags = flags => { | ||
if (inspectorsByType[InspectorTypes.flagDetailsChanged].length) { | ||
onNextTick(() => { | ||
inspectorsByType[InspectorTypes.flagDetailsChanged].forEach(inspector => inspector.method(flags)); | ||
}); | ||
} | ||
}; | ||
|
||
/** | ||
* Notify registered inspectors that a flag value has changed. | ||
* | ||
* The notification itself will be dispatched asynchronously. | ||
* | ||
* @param {string} flagKey The key for the flag that changed. | ||
* @param {Object} flag An `LDEvaluationDetail` for the flag. | ||
*/ | ||
manager.onFlagChanged = (flagKey, flag) => { | ||
if (inspectorsByType[InspectorTypes.flagDetailChanged].length) { | ||
onNextTick(() => { | ||
inspectorsByType[InspectorTypes.flagDetailChanged].forEach(inspector => inspector.method(flagKey, flag)); | ||
}); | ||
} | ||
}; | ||
|
||
/** | ||
* Notify the registered inspectors that the user identity has changed. | ||
* | ||
* The notification itself will be dispatched asynchronously. | ||
* | ||
* @param {Object} user The `LDUser` which is now identified. | ||
*/ | ||
manager.onIdentityChanged = user => { | ||
if (inspectorsByType[InspectorTypes.clientIdentityChanged].length) { | ||
onNextTick(() => { | ||
inspectorsByType[InspectorTypes.clientIdentityChanged].forEach(inspector => inspector.method(user)); | ||
}); | ||
} | ||
}; | ||
|
||
return manager; | ||
} | ||
|
||
module.exports = { InspectorTypes, InspectorManager }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
const { messages } = require('.'); | ||
|
||
/** | ||
* Wrap an inspector ensuring that calling its methods are safe. | ||
* @param {object} inspector Inspector to wrap. | ||
*/ | ||
function SafeInspector(inspector, logger) { | ||
let errorLogged = false; | ||
const wrapper = { | ||
type: inspector.type, | ||
name: inspector.name, | ||
}; | ||
|
||
wrapper.method = (...args) => { | ||
try { | ||
inspector.method(...args); | ||
} catch { | ||
// If something goes wrong in an inspector we want to log that something | ||
// went wrong. We don't want to flood the logs, so we only log something | ||
// the first time that something goes wrong. | ||
// We do not include the exception in the log, because we do not know what | ||
// kind of data it may contain. | ||
if (!errorLogged) { | ||
errorLogged = true; | ||
logger.warn(messages.inspectorMethodError(wrapper.type, wrapper.name)); | ||
} | ||
// Prevent errors. | ||
} | ||
}; | ||
|
||
return wrapper; | ||
} | ||
|
||
module.exports = SafeInspector; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,186 @@ | ||
const { AsyncQueue } = require('launchdarkly-js-test-helpers'); | ||
const { InspectorTypes, InspectorManager } = require('../InspectorManager'); | ||
const stubPlatform = require('./stubPlatform'); | ||
|
||
describe('given an inspector manager with no registered inspectors', () => { | ||
const platform = stubPlatform.defaults(); | ||
const manager = InspectorManager([], platform.testing.logger); | ||
|
||
it('does not cause errors', () => { | ||
manager.onIdentityChanged({ key: 'key' }); | ||
manager.onFlagUsed( | ||
'flag-key', | ||
{ | ||
value: null, | ||
}, | ||
{ key: 'key' } | ||
); | ||
manager.onFlags({}); | ||
manager.onFlagChanged('flag-key', { value: null }); | ||
}); | ||
|
||
it('does not report any registered listeners', () => { | ||
expect(manager.hasListeners(InspectorTypes.clientIdentityChanged)).toBeFalsy(); | ||
expect(manager.hasListeners(InspectorTypes.flagDetailChanged)).toBeFalsy(); | ||
expect(manager.hasListeners(InspectorTypes.flagDetailsChanged)).toBeFalsy(); | ||
expect(manager.hasListeners(InspectorTypes.flagUsed)).toBeFalsy(); | ||
expect(manager.hasListeners('potato')).toBeFalsy(); | ||
}); | ||
}); | ||
|
||
describe('given an inspector with callbacks of every type', () => { | ||
/** | ||
* @type {AsyncQueue} | ||
*/ | ||
const eventQueue = new AsyncQueue(); | ||
const platform = stubPlatform.defaults(); | ||
const manager = InspectorManager( | ||
[ | ||
{ | ||
type: 'flag-used', | ||
name: 'my-flag-used-inspector', | ||
method: (flagKey, flagDetail, user) => { | ||
eventQueue.add({ type: 'flag-used', flagKey, flagDetail, user }); | ||
}, | ||
}, | ||
// 'flag-used registered twice. | ||
{ | ||
type: 'flag-used', | ||
name: 'my-other-flag-used-inspector', | ||
method: (flagKey, flagDetail, user) => { | ||
eventQueue.add({ type: 'flag-used', flagKey, flagDetail, user }); | ||
}, | ||
}, | ||
{ | ||
type: 'flag-details-changed', | ||
name: 'my-flag-details-inspector', | ||
method: details => { | ||
eventQueue.add({ | ||
type: 'flag-details-changed', | ||
details, | ||
}); | ||
}, | ||
}, | ||
{ | ||
type: 'flag-detail-changed', | ||
name: 'my-flag-detail-inspector', | ||
method: (flagKey, flagDetail) => { | ||
eventQueue.add({ | ||
type: 'flag-detail-changed', | ||
flagKey, | ||
flagDetail, | ||
}); | ||
}, | ||
}, | ||
{ | ||
type: 'client-identity-changed', | ||
name: 'my-identity-inspector', | ||
method: user => { | ||
eventQueue.add({ | ||
type: 'client-identity-changed', | ||
user, | ||
}); | ||
}, | ||
}, | ||
// Invalid inspector shouldn't have an effect. | ||
{ | ||
type: 'potato', | ||
name: 'my-potato-inspector', | ||
method: () => {}, | ||
}, | ||
], | ||
platform.testing.logger | ||
); | ||
|
||
afterEach(() => { | ||
expect(eventQueue.length()).toEqual(0); | ||
}); | ||
|
||
afterAll(() => { | ||
eventQueue.close(); | ||
}); | ||
|
||
it('logged that there was a bad inspector', () => { | ||
expect(platform.testing.logger.output.warn).toEqual([ | ||
'an inspector: "my-potato-inspector" of an invalid type (potato) was configured', | ||
]); | ||
}); | ||
|
||
it('reports any registered listeners', () => { | ||
expect(manager.hasListeners(InspectorTypes.clientIdentityChanged)).toBeTruthy(); | ||
expect(manager.hasListeners(InspectorTypes.flagDetailChanged)).toBeTruthy(); | ||
expect(manager.hasListeners(InspectorTypes.flagDetailsChanged)).toBeTruthy(); | ||
expect(manager.hasListeners(InspectorTypes.flagUsed)).toBeTruthy(); | ||
expect(manager.hasListeners('potato')).toBeFalsy(); | ||
}); | ||
|
||
it('executes `onFlagUsed` handlers', async () => { | ||
manager.onFlagUsed( | ||
'flag-key', | ||
{ | ||
value: 'test', | ||
variationIndex: 1, | ||
reason: { | ||
kind: 'OFF', | ||
}, | ||
}, | ||
{ key: 'test-key' } | ||
); | ||
|
||
const expectedEvent = { | ||
type: 'flag-used', | ||
flagKey: 'flag-key', | ||
flagDetail: { | ||
value: 'test', | ||
variationIndex: 1, | ||
reason: { | ||
kind: 'OFF', | ||
}, | ||
}, | ||
user: { key: 'test-key' }, | ||
}; | ||
const event1 = await eventQueue.take(); | ||
expect(event1).toMatchObject(expectedEvent); | ||
|
||
// There are two handlers, so there should be another event. | ||
const event2 = await eventQueue.take(); | ||
expect(event2).toMatchObject(expectedEvent); | ||
}); | ||
|
||
it('executes `onFlags` handler', async () => { | ||
manager.onFlags({ | ||
example: { value: 'a-value' }, | ||
}); | ||
|
||
const event = await eventQueue.take(); | ||
expect(event).toMatchObject({ | ||
type: 'flag-details-changed', | ||
details: { | ||
example: { value: 'a-value' }, | ||
}, | ||
}); | ||
}); | ||
|
||
it('executes `onFlagChanged` handler', async () => { | ||
manager.onFlagChanged('the-flag', { value: 'a-value' }); | ||
|
||
const event = await eventQueue.take(); | ||
expect(event).toMatchObject({ | ||
type: 'flag-detail-changed', | ||
flagKey: 'the-flag', | ||
flagDetail: { | ||
value: 'a-value', | ||
}, | ||
}); | ||
}); | ||
|
||
it('executes `onIdentityChanged` handler', async () => { | ||
manager.onIdentityChanged({ key: 'the-key' }); | ||
|
||
const event = await eventQueue.take(); | ||
expect(event).toMatchObject({ | ||
type: 'client-identity-changed', | ||
user: { key: 'the-key' }, | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.