Skip to content

Commit

Permalink
Update U2C branch with inspection interfaces. (#80)
Browse files Browse the repository at this point in the history
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
14 people authored Oct 20, 2022
1 parent c6ca9d2 commit 1ce97b2
Show file tree
Hide file tree
Showing 11 changed files with 709 additions and 7 deletions.
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,21 @@

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.3.1] - 2022-10-17
### Fixed:
- Fixed an issue that prevented the `flag-used` inspector from being called.

## [4.3.0] - 2022-10-17
### Added:
- Added support for `Inspectors` that can be used for collecting information for monitoring, analytics, and debugging.

## [4.2.0] - 2022-10-03
### Removed:
- Removed `seenRequests` cache. This cache was used to de-duplicate events, but it has been supplanted with summary events.

### Deprecated:
- The `allowFrequentDuplicateEvents` configuration has been deprecated because it controlled the behavior of the `seenRequests` cache.

## [4.1.1] - 2022-06-07
### Changed:
- Enforce a 64 character limit for `application.id` and `application.version` configuration options.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "launchdarkly-js-sdk-common",
"version": "4.1.1",
"version": "4.3.1",
"description": "LaunchDarkly SDK for JavaScript - common code",
"author": "LaunchDarkly <team@launchdarkly.com>",
"license": "Apache-2.0",
Expand Down
119 changes: 119 additions & 0 deletions src/InspectorManager.js
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 };
34 changes: 34 additions & 0 deletions src/SafeInspector.js
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;
186 changes: 186 additions & 0 deletions src/__tests__/InspectorManager-test.js
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' },
});
});
});
Loading

0 comments on commit 1ce97b2

Please sign in to comment.