launchdarkly-js-sdk-common 5.0.3 → 5.2.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/CHANGELOG.md +15 -0
- package/CODEOWNERS +2 -0
- package/README.md +2 -2
- package/SECURITY.md +5 -0
- package/package.json +4 -3
- package/src/ContextFilter.js +17 -12
- package/src/EventProcessor.js +4 -2
- package/src/EventSender.js +6 -30
- package/src/__tests__/ContextFilter-test.js +33 -0
- package/src/__tests__/EventProcessor-test.js +8 -5
- package/src/__tests__/EventSender-test.js +14 -70
- package/src/__tests__/EventSummarizer-test.js +4 -1
- package/src/__tests__/LDClient-events-test.js +41 -29
- 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/__tests__/stubPlatform.js +0 -4
- package/src/__tests__/testUtils.js +1 -1
- package/src/__tests__/utils-test.js +1 -12
- package/src/errors.js +2 -0
- package/src/headers.js +2 -2
- package/src/index.js +40 -1
- package/src/messages.js +4 -0
- package/src/timedPromise.js +17 -0
- package/src/utils.js +0 -41
- package/test-types.ts +1 -1
- package/typings.d.ts +81 -71
|
@@ -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
|
});
|
|
@@ -13,8 +13,6 @@ import { MockHttpState } from './mockHttp';
|
|
|
13
13
|
// httpRequest?: (method, url, headers, body, sync) => requestProperties
|
|
14
14
|
// requestProperties.promise: Promise // resolves to { status, header: (name) => value, body } or rejects for a network error
|
|
15
15
|
// requestProperties.cancel?: () => void // provided if it's possible to cancel requests in this implementation
|
|
16
|
-
// httpAllowsPost: boolean // true if we can do cross-origin POST requests
|
|
17
|
-
// httpFallbackPing?: (url) => {} // method for doing an HTTP GET without awaiting the result (i.e. browser image mechanism)
|
|
18
16
|
// getCurrentUrl: () => string // returns null if we're not in a browser
|
|
19
17
|
// isDoNotTrack: () => boolean
|
|
20
18
|
// localStorage: {
|
|
@@ -45,8 +43,6 @@ export function defaults() {
|
|
|
45
43
|
httpRequest: mockHttpState.doRequest,
|
|
46
44
|
diagnosticSdkData: { name: 'stub-sdk' },
|
|
47
45
|
diagnosticPlatformData: { name: 'stub-platform' },
|
|
48
|
-
httpAllowsPost: () => true,
|
|
49
|
-
httpAllowsSync: () => true,
|
|
50
46
|
getCurrentUrl: () => currentUrl,
|
|
51
47
|
isDoNotTrack: () => doNotTrack,
|
|
52
48
|
eventSourceFactory: (url, options) => {
|
|
@@ -62,7 +62,7 @@ export function MockEventSender() {
|
|
|
62
62
|
calls,
|
|
63
63
|
sendEvents: (events, url) => {
|
|
64
64
|
calls.add({ events, url });
|
|
65
|
-
return Promise.resolve(
|
|
65
|
+
return Promise.resolve({ serverTime, status });
|
|
66
66
|
},
|
|
67
67
|
setServerTime: time => {
|
|
68
68
|
serverTime = time;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { appendUrlPath,
|
|
1
|
+
import { appendUrlPath, getLDUserAgentString, wrapPromiseCallback } from '../utils';
|
|
2
2
|
|
|
3
3
|
import * as stubPlatform from './stubPlatform';
|
|
4
4
|
|
|
@@ -63,15 +63,4 @@ describe('utils', () => {
|
|
|
63
63
|
expect(ua).toEqual('stubClient/7.8.9');
|
|
64
64
|
});
|
|
65
65
|
});
|
|
66
|
-
|
|
67
|
-
describe('chunkEventsForUrl', () => {
|
|
68
|
-
it('should properly chunk the list of events', () => {
|
|
69
|
-
const context = { key: 'foo', kind: 'user' };
|
|
70
|
-
const event = { kind: 'identify', key: context.key };
|
|
71
|
-
const eventLength = base64URLEncode(JSON.stringify(event)).length;
|
|
72
|
-
const events = [event, event, event, event, event];
|
|
73
|
-
const chunks = chunkEventsForUrl(eventLength * 2, events);
|
|
74
|
-
expect(chunks).toEqual([[event, event], [event, event], [event]]);
|
|
75
|
-
});
|
|
76
|
-
});
|
|
77
66
|
});
|
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
|
|
@@ -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
|
@@ -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/src/utils.js
CHANGED
|
@@ -112,46 +112,6 @@ function transformVersionedValuesToValues(flagsState) {
|
|
|
112
112
|
return ret;
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
-
/**
|
|
116
|
-
* Returns an array of event groups each of which can be safely URL-encoded
|
|
117
|
-
* without hitting the safe maximum URL length of certain browsers.
|
|
118
|
-
*
|
|
119
|
-
* @param {number} maxLength maximum URL length targeted
|
|
120
|
-
* @param {Array[Object}]} events queue of events to divide
|
|
121
|
-
* @returns Array[Array[Object]]
|
|
122
|
-
*/
|
|
123
|
-
function chunkEventsForUrl(maxLength, events) {
|
|
124
|
-
const allEvents = events.slice(0);
|
|
125
|
-
const allChunks = [];
|
|
126
|
-
let remainingSpace = maxLength;
|
|
127
|
-
let chunk;
|
|
128
|
-
|
|
129
|
-
while (allEvents.length > 0) {
|
|
130
|
-
chunk = [];
|
|
131
|
-
|
|
132
|
-
while (remainingSpace > 0) {
|
|
133
|
-
const event = allEvents.shift();
|
|
134
|
-
if (!event) {
|
|
135
|
-
break;
|
|
136
|
-
}
|
|
137
|
-
remainingSpace = remainingSpace - base64URLEncode(JSON.stringify(event)).length;
|
|
138
|
-
// If we are over the max size, put this one back on the queue
|
|
139
|
-
// to try in the next round, unless this event alone is larger
|
|
140
|
-
// than the limit, in which case, screw it, and try it anyway.
|
|
141
|
-
if (remainingSpace < 0 && chunk.length > 0) {
|
|
142
|
-
allEvents.unshift(event);
|
|
143
|
-
} else {
|
|
144
|
-
chunk.push(event);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
remainingSpace = maxLength;
|
|
149
|
-
allChunks.push(chunk);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
return allChunks;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
115
|
function getLDUserAgentString(platform) {
|
|
156
116
|
const version = platform.version || '?';
|
|
157
117
|
return platform.userAgent + '/' + version;
|
|
@@ -188,7 +148,6 @@ module.exports = {
|
|
|
188
148
|
appendUrlPath,
|
|
189
149
|
base64URLEncode,
|
|
190
150
|
btoa,
|
|
191
|
-
chunkEventsForUrl,
|
|
192
151
|
clone,
|
|
193
152
|
deepEquals,
|
|
194
153
|
extend,
|
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, () => {});
|