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.
@@ -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: [{ timestamp: 1001, durationMillis: 100 }, { timestamp: 1002, failed: true, durationMillis: 500 }],
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: [{ timestamp: 1000, durationMillis: 500 }, { timestamp: 1001, durationMillis: 501 }],
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([{ serverTime, status }]);
65
+ return Promise.resolve({ serverTime, status });
66
66
  },
67
67
  setServerTime: time => {
68
68
  serverTime = time;
@@ -1,4 +1,4 @@
1
- import { appendUrlPath, base64URLEncode, chunkEventsForUrl, getLDUserAgentString, wrapPromiseCallback } from '../utils';
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
- key => (Array.isArray(tags[key]) ? tags[key].sort().map(value => `${key}/${value}`) : [`${key}/${tags[key]}`])
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: () => initializationStateTracker.getInitializationPromise(),
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, () => {});