launchdarkly-js-sdk-common 5.1.0 → 5.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.circleci/config.yml +1 -1
- package/.github/actions/publish-docs/action.yml +16 -0
- package/.github/actions/publish-npm/action.yml +20 -0
- package/.github/workflows/ci.yml +41 -0
- package/.github/workflows/lint-pr-title.yml +12 -0
- package/.github/workflows/release-please.yml +57 -0
- package/.release-please-manifest.json +3 -0
- package/CHANGELOG.md +29 -0
- package/README.md +4 -1
- package/package.json +7 -5
- package/release-please-config.json +8 -0
- package/scripts/publish-npm.sh +11 -0
- package/src/ContextFilter.js +2 -2
- package/src/InspectorManager.js +47 -10
- package/src/SafeInspector.js +1 -0
- package/src/__tests__/EventProcessor-test.js +4 -1
- package/src/__tests__/EventSummarizer-test.js +4 -1
- package/src/__tests__/InspectorManager-test.js +7 -1
- package/src/__tests__/LDClient-events-test.js +41 -29
- package/src/__tests__/LDClient-inspectors-test.js +6 -1
- package/src/__tests__/LDClient-localstorage-test.js +8 -8
- package/src/__tests__/LDClient-streaming-test.js +41 -41
- package/src/__tests__/LDClient-test.js +37 -37
- package/src/__tests__/LDClient-timeout-test.js +90 -0
- package/src/__tests__/diagnosticEvents-test.js +8 -2
- package/src/errors.js +2 -0
- package/src/headers.js +2 -2
- package/src/index.js +41 -2
- package/src/messages.js +5 -1
- package/src/timedPromise.js +17 -0
- package/test-types.ts +1 -1
- package/typedoc.json +8 -0
- package/typings.d.ts +107 -67
- package/.ldrelease/config.yml +0 -25
- package/docs/typedoc.js +0 -11
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
jest.mock('../InitializationState', () => jest.fn());
|
|
2
|
+
|
|
3
|
+
import { initialize } from '../index';
|
|
4
|
+
import InitializationState from '../InitializationState';
|
|
5
|
+
import * as stubPlatform from './stubPlatform';
|
|
6
|
+
|
|
7
|
+
const createHangingPromise = () =>
|
|
8
|
+
new Promise(() => {
|
|
9
|
+
// never resolves
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
describe('timeout', () => {
|
|
13
|
+
let ldc;
|
|
14
|
+
let mockGetInitializationPromise;
|
|
15
|
+
let mockGetReadyPromise;
|
|
16
|
+
let logger;
|
|
17
|
+
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
mockGetInitializationPromise = jest.fn();
|
|
20
|
+
mockGetReadyPromise = jest.fn();
|
|
21
|
+
logger = stubPlatform.logger();
|
|
22
|
+
InitializationState.mockImplementation(() => ({
|
|
23
|
+
getInitializationPromise: mockGetInitializationPromise,
|
|
24
|
+
getReadyPromise: mockGetReadyPromise,
|
|
25
|
+
signalFailure: jest.fn(),
|
|
26
|
+
}));
|
|
27
|
+
mockGetInitializationPromise.mockImplementation(createHangingPromise);
|
|
28
|
+
mockGetReadyPromise.mockImplementation(createHangingPromise);
|
|
29
|
+
({ client: ldc } = initialize(
|
|
30
|
+
'abc',
|
|
31
|
+
{ kind: 'user', key: 'test-user' },
|
|
32
|
+
{
|
|
33
|
+
logger: logger,
|
|
34
|
+
},
|
|
35
|
+
{}
|
|
36
|
+
));
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
afterEach(() => {
|
|
40
|
+
jest.resetAllMocks();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('waitForInitialization times out if initialization does not resolve', async () => {
|
|
44
|
+
const p = ldc.waitForInitialization(1);
|
|
45
|
+
await expect(p).rejects.toThrow(/timed out/);
|
|
46
|
+
|
|
47
|
+
// No warnings in this configuration.
|
|
48
|
+
expect(logger.output.warn).toEqual([]);
|
|
49
|
+
expect(logger.output.error).toEqual([
|
|
50
|
+
'waitForInitialization error: LaunchDarklyTimeoutError: waitForInitialization timed out after 1 seconds.',
|
|
51
|
+
]);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('waitForInitialization warns if no timeout is provided', async () => {
|
|
55
|
+
ldc.waitForInitialization();
|
|
56
|
+
|
|
57
|
+
expect(logger.output.warn).toEqual([
|
|
58
|
+
'The waitForInitialization function was called without a timeout specified. In a future version a default timeout will be applied.',
|
|
59
|
+
]);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('waitForInitialization warns if timeout is not a number', async () => {
|
|
63
|
+
ldc.waitForInitialization('10');
|
|
64
|
+
|
|
65
|
+
// You get two warnings in this case. Which should be fine as you have to go our of your way to get into this situation.
|
|
66
|
+
expect(logger.output.warn).toEqual([
|
|
67
|
+
'The waitForInitialization method was provided with a non-numeric timeout.',
|
|
68
|
+
'The waitForInitialization function was called without a timeout specified. In a future version a default timeout will be applied.',
|
|
69
|
+
]);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('waitForInitialization warns if timeout provided is too high', async () => {
|
|
73
|
+
ldc.waitForInitialization(10);
|
|
74
|
+
|
|
75
|
+
expect(logger.output.warn).toEqual([
|
|
76
|
+
'The waitForInitialization function was called with a timeout greater than 5 seconds. We recommend a timeout of 5 seconds or less.',
|
|
77
|
+
]);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('waitForInitialization does not timeout if the initialization promise resolves in the timeout', async () => {
|
|
81
|
+
mockGetInitializationPromise.mockImplementation(() => Promise.resolve('success'));
|
|
82
|
+
|
|
83
|
+
const p = ldc.waitForInitialization(5);
|
|
84
|
+
|
|
85
|
+
await expect(p).resolves.toEqual('success');
|
|
86
|
+
|
|
87
|
+
// No warnings in this configuration.
|
|
88
|
+
expect(logger.output.warn).toEqual([]);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
@@ -117,7 +117,10 @@ describe('DiagnosticsManager', () => {
|
|
|
117
117
|
const expectedStatsForPeriodicEvent1 = {
|
|
118
118
|
droppedEvents: 1,
|
|
119
119
|
eventsInLastBatch: 2,
|
|
120
|
-
streamInits: [
|
|
120
|
+
streamInits: [
|
|
121
|
+
{ timestamp: 1001, durationMillis: 100 },
|
|
122
|
+
{ timestamp: 1002, failed: true, durationMillis: 500 },
|
|
123
|
+
],
|
|
121
124
|
};
|
|
122
125
|
const expectedStatsForPeriodicEvent2 = {
|
|
123
126
|
droppedEvents: 0,
|
|
@@ -392,7 +395,10 @@ describe('DiagnosticsManager', () => {
|
|
|
392
395
|
dataSinceDate: storedStats.dataSinceDate,
|
|
393
396
|
droppedEvents: 2,
|
|
394
397
|
eventsInLastBatch: 3,
|
|
395
|
-
streamInits: [
|
|
398
|
+
streamInits: [
|
|
399
|
+
{ timestamp: 1000, durationMillis: 500 },
|
|
400
|
+
{ timestamp: 1001, durationMillis: 501 },
|
|
401
|
+
],
|
|
396
402
|
});
|
|
397
403
|
expect(firstEvent.creationDate).toBeGreaterThanOrEqual(timeBeforeStart);
|
|
398
404
|
});
|
package/src/errors.js
CHANGED
|
@@ -19,6 +19,7 @@ const LDInvalidEventKeyError = createCustomError('LaunchDarklyInvalidEventKeyErr
|
|
|
19
19
|
const LDInvalidArgumentError = createCustomError('LaunchDarklyInvalidArgumentError');
|
|
20
20
|
const LDFlagFetchError = createCustomError('LaunchDarklyFlagFetchError');
|
|
21
21
|
const LDInvalidDataError = createCustomError('LaunchDarklyInvalidDataError');
|
|
22
|
+
const LDTimeoutError = createCustomError('LaunchDarklyTimeoutError');
|
|
22
23
|
|
|
23
24
|
function isHttpErrorRecoverable(status) {
|
|
24
25
|
if (status >= 400 && status < 500) {
|
|
@@ -35,5 +36,6 @@ module.exports = {
|
|
|
35
36
|
LDInvalidArgumentError,
|
|
36
37
|
LDInvalidDataError,
|
|
37
38
|
LDFlagFetchError,
|
|
39
|
+
LDTimeoutError,
|
|
38
40
|
isHttpErrorRecoverable,
|
|
39
41
|
};
|
package/src/headers.js
CHANGED
|
@@ -17,8 +17,8 @@ function getLDHeaders(platform, options) {
|
|
|
17
17
|
if (tagKeys.length) {
|
|
18
18
|
h['x-launchdarkly-tags'] = tagKeys
|
|
19
19
|
.sort()
|
|
20
|
-
.map(
|
|
21
|
-
|
|
20
|
+
.map(key =>
|
|
21
|
+
Array.isArray(tags[key]) ? tags[key].sort().map(value => `${key}/${value}`) : [`${key}/${tags[key]}`]
|
|
22
22
|
)
|
|
23
23
|
.reduce((flattened, item) => flattened.concat(item), [])
|
|
24
24
|
.join(' ');
|
package/src/index.js
CHANGED
|
@@ -16,9 +16,11 @@ const errors = require('./errors');
|
|
|
16
16
|
const messages = require('./messages');
|
|
17
17
|
const { checkContext, getContextKeys } = require('./context');
|
|
18
18
|
const { InspectorTypes, InspectorManager } = require('./InspectorManager');
|
|
19
|
+
const timedPromise = require('./timedPromise');
|
|
19
20
|
|
|
20
21
|
const changeEvent = 'change';
|
|
21
22
|
const internalChangeEvent = 'internal-change';
|
|
23
|
+
const highTimeoutThreshold = 5;
|
|
22
24
|
|
|
23
25
|
// This is called by the per-platform initialize functions to create the base client object that we
|
|
24
26
|
// may also extend with additional behavior. It returns an object with these properties:
|
|
@@ -365,6 +367,9 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) {
|
|
|
365
367
|
emitter.maybeReportError(new errors.LDInvalidEventKeyError(messages.unknownCustomEventKey(key)));
|
|
366
368
|
return;
|
|
367
369
|
}
|
|
370
|
+
if (metricValue !== undefined && typeof metricValue !== 'number') {
|
|
371
|
+
logger.warn(messages.invalidMetricValue(typeof metricValue));
|
|
372
|
+
}
|
|
368
373
|
|
|
369
374
|
// The following logic was used only for the JS browser SDK (js-client-sdk) and
|
|
370
375
|
// is no longer needed as of version 2.9.13 of that SDK. The other client-side
|
|
@@ -457,8 +462,8 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) {
|
|
|
457
462
|
} else {
|
|
458
463
|
mods[data.key] = { current: newDetail };
|
|
459
464
|
}
|
|
460
|
-
handleFlagChanges(mods); // don't wait for this Promise to be resolved
|
|
461
465
|
notifyInspectionFlagChanged(data, newFlag);
|
|
466
|
+
handleFlagChanges(mods); // don't wait for this Promise to be resolved
|
|
462
467
|
} else {
|
|
463
468
|
logger.debug(messages.debugStreamPatchIgnored(data.key));
|
|
464
469
|
}
|
|
@@ -772,8 +777,42 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) {
|
|
|
772
777
|
return flags;
|
|
773
778
|
}
|
|
774
779
|
|
|
780
|
+
function waitForInitializationWithTimeout(timeout) {
|
|
781
|
+
if (timeout > highTimeoutThreshold) {
|
|
782
|
+
logger.warn(
|
|
783
|
+
'The waitForInitialization function was called with a timeout greater than ' +
|
|
784
|
+
`${highTimeoutThreshold} seconds. We recommend a timeout of ` +
|
|
785
|
+
`${highTimeoutThreshold} seconds or less.`
|
|
786
|
+
);
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
const initPromise = initializationStateTracker.getInitializationPromise();
|
|
790
|
+
const timeoutPromise = timedPromise(timeout, 'waitForInitialization');
|
|
791
|
+
|
|
792
|
+
return Promise.race([timeoutPromise, initPromise]).catch(e => {
|
|
793
|
+
if (e instanceof errors.LDTimeoutError) {
|
|
794
|
+
logger.error(`waitForInitialization error: ${e}`);
|
|
795
|
+
}
|
|
796
|
+
throw e;
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
function waitForInitialization(timeout = undefined) {
|
|
801
|
+
if (timeout !== undefined && timeout !== null) {
|
|
802
|
+
if (typeof timeout === 'number') {
|
|
803
|
+
return waitForInitializationWithTimeout(timeout);
|
|
804
|
+
}
|
|
805
|
+
logger.warn('The waitForInitialization method was provided with a non-numeric timeout.');
|
|
806
|
+
}
|
|
807
|
+
logger.warn(
|
|
808
|
+
'The waitForInitialization function was called without a timeout specified.' +
|
|
809
|
+
' In a future version a default timeout will be applied.'
|
|
810
|
+
);
|
|
811
|
+
return initializationStateTracker.getInitializationPromise();
|
|
812
|
+
}
|
|
813
|
+
|
|
775
814
|
const client = {
|
|
776
|
-
waitForInitialization
|
|
815
|
+
waitForInitialization,
|
|
777
816
|
waitUntilReady: () => initializationStateTracker.getReadyPromise(),
|
|
778
817
|
identify: identify,
|
|
779
818
|
getContext: getContext,
|
package/src/messages.js
CHANGED
|
@@ -15,7 +15,7 @@ const clientInitialized = function() {
|
|
|
15
15
|
};
|
|
16
16
|
|
|
17
17
|
const docLink =
|
|
18
|
-
' Please see https://docs.launchdarkly.com/sdk/client-side/javascript#
|
|
18
|
+
' Please see https://docs.launchdarkly.com/sdk/client-side/javascript#initialize-the-client for instructions on SDK initialization.';
|
|
19
19
|
|
|
20
20
|
const clientNotReady = function() {
|
|
21
21
|
return 'LaunchDarkly client is not ready';
|
|
@@ -190,6 +190,9 @@ const invalidTagValue = name => `Config option "${name}" must only contain lette
|
|
|
190
190
|
|
|
191
191
|
const tagValueTooLong = name => `Value of "${name}" was longer than 64 characters and was discarded.`;
|
|
192
192
|
|
|
193
|
+
const invalidMetricValue = badType =>
|
|
194
|
+
`The track function was called with a non-numeric "metricValue" (${badType}), only numeric metric values are supported.`;
|
|
195
|
+
|
|
193
196
|
module.exports = {
|
|
194
197
|
bootstrapInvalid,
|
|
195
198
|
bootstrapOldFormat,
|
|
@@ -219,6 +222,7 @@ module.exports = {
|
|
|
219
222
|
invalidData,
|
|
220
223
|
invalidInspector,
|
|
221
224
|
invalidKey,
|
|
225
|
+
invalidMetricValue,
|
|
222
226
|
invalidContext,
|
|
223
227
|
invalidTagValue,
|
|
224
228
|
localStorageUnavailable,
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const { LDTimeoutError } = require('./errors');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Returns a promise which errors after t seconds.
|
|
5
|
+
*
|
|
6
|
+
* @param t Timeout in seconds.
|
|
7
|
+
* @param taskName Name of task being timed for logging and error reporting.
|
|
8
|
+
*/
|
|
9
|
+
function timedPromise(t, taskName) {
|
|
10
|
+
return new Promise((_res, reject) => {
|
|
11
|
+
setTimeout(() => {
|
|
12
|
+
const e = `${taskName} timed out after ${t} seconds.`;
|
|
13
|
+
reject(new LDTimeoutError(e));
|
|
14
|
+
}, t * 1000);
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
module.exports = timedPromise;
|
package/test-types.ts
CHANGED
|
@@ -54,7 +54,7 @@ var allBaseOptions: ld.LDOptionsBase = {
|
|
|
54
54
|
var client: ld.LDClientBase = {} as ld.LDClientBase; // wouldn't do this in real life, it's just so the following statements will compile
|
|
55
55
|
|
|
56
56
|
client.waitUntilReady().then(() => {});
|
|
57
|
-
client.waitForInitialization().then(() => {});
|
|
57
|
+
client.waitForInitialization(5).then(() => {});
|
|
58
58
|
|
|
59
59
|
client.identify(user).then(() => {});
|
|
60
60
|
client.identify(user, undefined, () => {});
|