Skip to content

Commit

Permalink
prepare 3.8.0 release (#74)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
Co-authored-by: Zach Davis <[email protected]>
Co-authored-by: LaunchDarklyCI <[email protected]>
Co-authored-by: Ben Woskow <[email protected]>
Co-authored-by: Ben Woskow <[email protected]>
Co-authored-by: Michael Siadak <[email protected]>
Co-authored-by: Jeff Wen <[email protected]>
Co-authored-by: Andrey Krasnov <[email protected]>
Co-authored-by: Gavin Whelan <[email protected]>
Co-authored-by: LaunchDarklyReleaseBot <[email protected]>
Co-authored-by: Louis Chan <[email protected]>
Co-authored-by: Louis Chan <[email protected]>
Co-authored-by: Ryan Lamb <[email protected]>
  • Loading branch information
14 people authored Oct 17, 2022
1 parent 9b82030 commit a00a689
Show file tree
Hide file tree
Showing 11 changed files with 700 additions and 10 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ lib
.vscode/
test-types.js
docs/build/
package-lock.json
118 changes: 118 additions & 0 deletions src/InspectorManager.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import * as messages from './messages';
import SafeInspector from './SafeInspector';
import { onNextTick } from './utils';

/**
* The types of supported inspectors.
*/
export 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.
*/
export 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(() => {
console.log('what?');
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;
}
32 changes: 32 additions & 0 deletions src/SafeInspector.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import * as messages from './messages';

/**
* Wrap an inspector ensuring that calling its methods are safe.
* @param {object} inspector Inspector to wrap.
*/
export default 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;
}
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 @@
import { AsyncQueue } from 'launchdarkly-js-test-helpers';
import { InspectorTypes, InspectorManager } from '../InspectorManager';
import * as stubPlatform from './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 a00a689

Please sign in to comment.