launchdarkly-js-sdk-common 3.4.0 → 4.0.2

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.
Files changed (68) hide show
  1. package/.circleci/config.yml +22 -0
  2. package/.eslintignore +4 -0
  3. package/.eslintrc.yaml +103 -0
  4. package/.github/ISSUE_TEMPLATE/bug_report.md +37 -0
  5. package/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
  6. package/.github/pull_request_template.md +21 -0
  7. package/.ldrelease/config.yml +24 -0
  8. package/.prettierignore +1 -0
  9. package/.prettierrc +5 -0
  10. package/CHANGELOG.md +24 -0
  11. package/CONTRIBUTING.md +45 -0
  12. package/babel.config.js +18 -0
  13. package/docs/typedoc.js +11 -0
  14. package/jest.config.js +12 -0
  15. package/package.json +5 -33
  16. package/scripts/better-audit.sh +76 -0
  17. package/src/EventEmitter.js +60 -0
  18. package/src/EventProcessor.js +175 -0
  19. package/src/EventSender.js +87 -0
  20. package/src/EventSummarizer.js +84 -0
  21. package/src/Identity.js +26 -0
  22. package/src/InitializationState.js +83 -0
  23. package/src/PersistentFlagStore.js +50 -0
  24. package/src/PersistentStorage.js +81 -0
  25. package/src/Requestor.js +111 -0
  26. package/src/Stream.js +154 -0
  27. package/src/UserFilter.js +75 -0
  28. package/src/UserValidator.js +56 -0
  29. package/src/__tests__/.eslintrc.yaml +6 -0
  30. package/src/__tests__/EventProcessor-test.js +559 -0
  31. package/src/__tests__/EventSender-test.js +252 -0
  32. package/src/__tests__/EventSource-mock.js +61 -0
  33. package/src/__tests__/EventSummarizer-test.js +103 -0
  34. package/src/__tests__/LDClient-events-test.js +757 -0
  35. package/src/__tests__/LDClient-localstorage-test.js +179 -0
  36. package/src/__tests__/LDClient-streaming-test.js +683 -0
  37. package/src/__tests__/LDClient-test.js +753 -0
  38. package/src/__tests__/PersistentFlagStore-test.js +111 -0
  39. package/src/__tests__/Requestor-test.js +362 -0
  40. package/src/__tests__/Stream-test.js +299 -0
  41. package/src/__tests__/UserFilter-test.js +93 -0
  42. package/src/__tests__/UserValidator-test.js +57 -0
  43. package/src/__tests__/configuration-test.js +217 -0
  44. package/src/__tests__/diagnosticEvents-test.js +449 -0
  45. package/src/__tests__/loggers-test.js +149 -0
  46. package/src/__tests__/mockHttp.js +122 -0
  47. package/src/__tests__/promiseCoalescer-test.js +128 -0
  48. package/src/__tests__/stubPlatform.js +148 -0
  49. package/src/__tests__/testUtils.js +77 -0
  50. package/src/__tests__/utils-test.js +148 -0
  51. package/src/configuration.js +151 -0
  52. package/src/diagnosticEvents.js +269 -0
  53. package/src/errors.js +37 -0
  54. package/src/index.js +769 -0
  55. package/src/jest.setup.js +1 -0
  56. package/src/loggers.js +93 -0
  57. package/src/messages.js +217 -0
  58. package/src/promiseCoalescer.js +52 -0
  59. package/src/utils.js +214 -0
  60. package/test-types.ts +94 -0
  61. package/tsconfig.json +13 -0
  62. package/typings.d.ts +98 -45
  63. package/dist/ldclient-common.cjs.js +0 -2
  64. package/dist/ldclient-common.cjs.js.map +0 -1
  65. package/dist/ldclient-common.es.js +0 -2
  66. package/dist/ldclient-common.es.js.map +0 -1
  67. package/dist/ldclient-common.min.js +0 -2
  68. package/dist/ldclient-common.min.js.map +0 -1
@@ -0,0 +1,449 @@
1
+ import { baseOptionDefs } from '../configuration';
2
+ import { DiagnosticId, DiagnosticsAccumulator, DiagnosticsManager } from '../diagnosticEvents';
3
+ import PersistentStorage from '../PersistentStorage';
4
+ import { sleepAsync } from 'launchdarkly-js-test-helpers';
5
+
6
+ import * as stubPlatform from './stubPlatform';
7
+ import { MockEventSender } from './testUtils';
8
+
9
+ // These tests cover the logic in diagnosticEvents.js. Some of the statistics in diagnostic events come from
10
+ // other SDK components; the tests for those components will verify that they generate the right values.
11
+
12
+ describe('DiagnosticId', () => {
13
+ it('creates unique IDs', () => {
14
+ const id1 = DiagnosticId('key');
15
+ const id2 = DiagnosticId('key');
16
+ expect(id1.diagnosticId).not.toEqual(id2.diagnosticId);
17
+ });
18
+
19
+ it('uses only last 6 characters of key', () => {
20
+ const id = DiagnosticId('0123456789abcdef');
21
+ expect(id.sdkKeySuffix).toEqual('abcdef');
22
+ });
23
+ });
24
+
25
+ describe('DiagnosticsAccumulator', () => {
26
+ it('sets initial properties', () => {
27
+ const acc = DiagnosticsAccumulator(1000);
28
+ expect(acc.getProps()).toEqual({
29
+ dataSinceDate: 1000,
30
+ droppedEvents: 0,
31
+ eventsInLastBatch: 0,
32
+ streamInits: [],
33
+ });
34
+ });
35
+
36
+ it('increments dropped events', () => {
37
+ const acc = DiagnosticsAccumulator(1000);
38
+ acc.incrementDroppedEvents();
39
+ acc.incrementDroppedEvents();
40
+ expect(acc.getProps().droppedEvents).toEqual(2);
41
+ });
42
+
43
+ it('sets event count', () => {
44
+ const acc = DiagnosticsAccumulator(1000);
45
+ acc.setEventsInLastBatch(99);
46
+ expect(acc.getProps().eventsInLastBatch).toEqual(99);
47
+ });
48
+
49
+ it('records successful stream init', () => {
50
+ const acc = DiagnosticsAccumulator(1000);
51
+ acc.recordStreamInit(1001, false, 500);
52
+ expect(acc.getProps().streamInits).toEqual([{ timestamp: 1001, failed: false, durationMillis: 500 }]);
53
+ });
54
+
55
+ it('records failed stream init', () => {
56
+ const acc = DiagnosticsAccumulator(1000);
57
+ acc.recordStreamInit(1001, true, 500);
58
+ expect(acc.getProps().streamInits).toEqual([{ timestamp: 1001, failed: true, durationMillis: 500 }]);
59
+ });
60
+
61
+ it('resets properties', () => {
62
+ const acc = DiagnosticsAccumulator(1000);
63
+ acc.incrementDroppedEvents();
64
+ acc.setEventsInLastBatch(99);
65
+ acc.recordStreamInit(1001, false, 500);
66
+ acc.reset(1002);
67
+ expect(acc.getProps()).toEqual({
68
+ dataSinceDate: 1002,
69
+ droppedEvents: 0,
70
+ eventsInLastBatch: 0,
71
+ streamInits: [],
72
+ });
73
+ });
74
+ });
75
+
76
+ describe('DiagnosticsManager', () => {
77
+ const diagnosticId = DiagnosticId('123456');
78
+ const envId = 'my-environment-id';
79
+ const defaultStartTime = 1000;
80
+ const defaultInterval = 100000;
81
+ const localStorageKey = 'ld:' + envId + ':$diagnostics';
82
+ const sdkData = {
83
+ name: 'js-test',
84
+ version: '0.0.1',
85
+ };
86
+ const platformData = {
87
+ name: 'Positron',
88
+ osArch: 'usrobots',
89
+ osName: 'Robbie',
90
+ osVersion: '1940',
91
+ };
92
+ const defaultConfig = {
93
+ baseUrl: baseOptionDefs.baseUrl.default,
94
+ streamUrl: baseOptionDefs.streamUrl.default,
95
+ eventsUrl: baseOptionDefs.eventsUrl.default,
96
+ eventCapacity: 50,
97
+ fetchGoals: true,
98
+ flushInterval: 1000,
99
+ streamReconnectDelay: 900,
100
+ diagnosticRecordingInterval: defaultInterval,
101
+ };
102
+ const defaultConfigInEvent = {
103
+ allAttributesPrivate: false,
104
+ allowFrequentDuplicateEvents: false,
105
+ autoAliasingOptOut: false,
106
+ bootstrapMode: false,
107
+ customBaseURI: false,
108
+ customEventsURI: false,
109
+ customStreamURI: false,
110
+ diagnosticRecordingIntervalMillis: defaultInterval,
111
+ eventsCapacity: defaultConfig.eventCapacity,
112
+ eventsFlushIntervalMillis: defaultConfig.flushInterval,
113
+ fetchGoalsDisabled: false,
114
+ inlineUsersInEvents: false,
115
+ reconnectTimeMillis: defaultConfig.streamReconnectDelay,
116
+ sendEventsOnlyForVariation: false,
117
+ streamingDisabled: true,
118
+ usingSecureMode: false,
119
+ };
120
+ const expectedStatsForPeriodicEvent1 = {
121
+ droppedEvents: 1,
122
+ eventsInLastBatch: 2,
123
+ streamInits: [{ timestamp: 1001, durationMillis: 100 }, { timestamp: 1002, failed: true, durationMillis: 500 }],
124
+ };
125
+ const expectedStatsForPeriodicEvent2 = {
126
+ droppedEvents: 0,
127
+ eventsInLastBatch: 1,
128
+ streamInits: [{ timestamp: 1003, durationMillis: 99 }],
129
+ };
130
+
131
+ async function withManager(extraConfig, overridePlatform, asyncCallback) {
132
+ const platform = overridePlatform || stubPlatform.defaults();
133
+ const storage = PersistentStorage(platform.localStorage, platform.testing.logger);
134
+ platform.diagnosticSdkData = sdkData;
135
+ platform.diagnosticPlatformData = platformData;
136
+ const config = { ...defaultConfig, ...extraConfig };
137
+ const acc = DiagnosticsAccumulator(defaultStartTime);
138
+ const sender = MockEventSender();
139
+ const m = DiagnosticsManager(platform, storage, acc, sender, envId, config, diagnosticId);
140
+ try {
141
+ return await asyncCallback(m, acc, sender);
142
+ } finally {
143
+ m.stop();
144
+ }
145
+ }
146
+
147
+ function setupStatsForPeriodicEvent1(acc) {
148
+ acc.incrementDroppedEvents();
149
+ acc.setEventsInLastBatch(2);
150
+ acc.recordStreamInit(1001, false, 100);
151
+ acc.recordStreamInit(1002, true, 500);
152
+ }
153
+
154
+ function setupStatsForPeriodicEvent2(acc) {
155
+ acc.setEventsInLastBatch(1);
156
+ acc.recordStreamInit(1003, false, 99);
157
+ }
158
+
159
+ async function getPostedEvent(sender, config) {
160
+ const posted = await sender.calls.take();
161
+ const baseUrl = { ...defaultConfig, ...config }.eventsUrl;
162
+ expect(posted.url).toEqual(baseUrl + '/events/diagnostic/' + envId);
163
+ return posted.events;
164
+ }
165
+
166
+ describe('in default mode', () => {
167
+ it('does not send init event before start()', async () => {
168
+ await withManager({}, null, async (manager, acc, sender) => {
169
+ expect(sender.calls.length()).toEqual(0);
170
+ });
171
+ });
172
+
173
+ it('sends init event on start() with default config', async () => {
174
+ await withManager({}, null, async (manager, acc, sender) => {
175
+ manager.start();
176
+ expect(sender.calls.length()).toEqual(1);
177
+ const initEvent = await getPostedEvent(sender);
178
+ expect(initEvent).toEqual({
179
+ kind: 'diagnostic-init',
180
+ creationDate: defaultStartTime,
181
+ id: diagnosticId,
182
+ sdk: sdkData,
183
+ platform: platformData,
184
+ configuration: defaultConfigInEvent,
185
+ });
186
+ });
187
+ });
188
+
189
+ it('sends init event on start() with custom config', async () => {
190
+ const configAndResultValues = [
191
+ [{ allAttributesPrivate: true }, { allAttributesPrivate: true }],
192
+ [{ allowFrequentDuplicateEvents: true }, { allowFrequentDuplicateEvents: true }],
193
+ [{ bootstrap: {} }, { bootstrapMode: true }],
194
+ [{ baseUrl: 'http://other' }, { customBaseURI: true }],
195
+ [{ eventsUrl: 'http://other' }, { customEventsURI: true }],
196
+ [{ streamUrl: 'http://other' }, { customStreamURI: true }],
197
+ [{ diagnosticRecordingInterval: 99999 }, { diagnosticRecordingIntervalMillis: 99999 }],
198
+ [{ eventCapacity: 222 }, { eventsCapacity: 222 }],
199
+ [{ flushInterval: 2222 }, { eventsFlushIntervalMillis: 2222 }],
200
+ [{ fetchGoals: false }, { fetchGoalsDisabled: true }],
201
+ [{ inlineUsersInEvents: true }, { inlineUsersInEvents: true }],
202
+ [{ streamReconnectDelay: 2222 }, { reconnectTimeMillis: 2222 }],
203
+ [{ sendEventsOnlyForVariation: true }, { sendEventsOnlyForVariation: true }],
204
+ [{ streaming: true }, { streamingDisabled: false }],
205
+ [{ hash: 'x' }, { usingSecureMode: true }],
206
+ [{ autoAliasingOptOut: true }, { autoAliasingOptOut: true }],
207
+ ];
208
+ for (const i in configAndResultValues) {
209
+ const configOverrides = configAndResultValues[i][0];
210
+ const expectedConfig = { ...defaultConfigInEvent, ...configAndResultValues[i][1] };
211
+ await withManager(configOverrides, null, async (manager, acc, sender) => {
212
+ manager.start();
213
+ expect(sender.calls.length()).toEqual(1);
214
+ const initEvent = await getPostedEvent(sender, configOverrides);
215
+ expect(initEvent).toEqual({
216
+ kind: 'diagnostic-init',
217
+ creationDate: defaultStartTime,
218
+ id: diagnosticId,
219
+ sdk: sdkData,
220
+ platform: platformData,
221
+ configuration: expectedConfig,
222
+ });
223
+ });
224
+ }
225
+ });
226
+
227
+ it('allows client to indicate that streaming is now enabled', async () => {
228
+ await withManager({}, null, async (manager, acc, sender) => {
229
+ manager.setStreaming(true);
230
+ manager.start();
231
+ expect(sender.calls.length()).toEqual(1);
232
+ const initEvent = await getPostedEvent(sender);
233
+ expect(initEvent).toEqual({
234
+ kind: 'diagnostic-init',
235
+ creationDate: defaultStartTime,
236
+ id: diagnosticId,
237
+ sdk: sdkData,
238
+ platform: platformData,
239
+ configuration: { ...defaultConfigInEvent, streamingDisabled: false },
240
+ });
241
+ });
242
+ });
243
+
244
+ it('sends periodic events', async () => {
245
+ const interval = 100;
246
+ // Note that since we haven't added any special instrumentation to DiagnosticsManager to let the test
247
+ // control the exact timing of the periodic events, this test is assuming that we can do a few simple
248
+ // steps before 100ms elapses.
249
+ await withManager({ diagnosticRecordingInterval: interval }, null, async (manager, acc, sender) => {
250
+ manager.start();
251
+ const initEvent = await getPostedEvent(sender);
252
+ expect(initEvent.kind).toEqual('diagnostic-init');
253
+
254
+ setupStatsForPeriodicEvent1(acc);
255
+
256
+ const periodic1 = await getPostedEvent(sender);
257
+ expect(periodic1).toMatchObject({
258
+ kind: 'diagnostic',
259
+ dataSinceDate: defaultStartTime,
260
+ id: diagnosticId,
261
+ ...expectedStatsForPeriodicEvent1,
262
+ });
263
+ expect(periodic1.creationDate).toBeGreaterThanOrEqual(defaultStartTime);
264
+
265
+ setupStatsForPeriodicEvent2(acc);
266
+
267
+ const periodic2 = await getPostedEvent(sender);
268
+ expect(periodic2).toMatchObject({
269
+ kind: 'diagnostic',
270
+ dataSinceDate: periodic1.creationDate,
271
+ id: diagnosticId,
272
+ ...expectedStatsForPeriodicEvent2,
273
+ });
274
+ });
275
+ });
276
+ });
277
+
278
+ describe('in combined (browser) mode', () => {
279
+ const interval = 100;
280
+ const expectedConfig = { ...defaultConfigInEvent, diagnosticRecordingIntervalMillis: interval };
281
+
282
+ it('does not send event before start()', async () => {
283
+ const overridePlatform = stubPlatform.defaults();
284
+ overridePlatform.diagnosticUseCombinedEvent = true;
285
+ await withManager({}, overridePlatform, async (manager, acc, sender) => {
286
+ expect(sender.calls.length()).toEqual(0);
287
+ });
288
+ });
289
+
290
+ it('if local storage has no data, sends event on start(), then sends periodic event', async () => {
291
+ const timeBeforeStart = new Date().getTime();
292
+ const overridePlatform = stubPlatform.defaults();
293
+ overridePlatform.diagnosticUseCombinedEvent = true;
294
+ await withManager({ diagnosticRecordingInterval: interval }, overridePlatform, async (manager, acc, sender) => {
295
+ manager.start();
296
+
297
+ const firstEvent = await getPostedEvent(sender);
298
+ expect(firstEvent).toMatchObject({
299
+ kind: 'diagnostic-combined',
300
+ id: diagnosticId,
301
+ dataSinceDate: defaultStartTime,
302
+ sdk: sdkData,
303
+ platform: platformData,
304
+ configuration: expectedConfig,
305
+ droppedEvents: 0,
306
+ eventsInLastBatch: 0,
307
+ streamInits: [],
308
+ });
309
+ expect(firstEvent.creationDate).toBeGreaterThanOrEqual(timeBeforeStart);
310
+
311
+ setupStatsForPeriodicEvent1(acc);
312
+
313
+ const periodic1 = await getPostedEvent(sender);
314
+ expect(periodic1).toMatchObject({
315
+ kind: 'diagnostic-combined',
316
+ id: diagnosticId,
317
+ sdk: sdkData,
318
+ platform: platformData,
319
+ configuration: expectedConfig,
320
+ ...expectedStatsForPeriodicEvent1,
321
+ });
322
+ expect(periodic1.dataSinceDate).toBeGreaterThan(firstEvent.dataSinceDate);
323
+ });
324
+ });
325
+
326
+ it('if local storage has non-recent data, sends cached event on start(), then sends periodic event', async () => {
327
+ const timeBeforeStart = new Date().getTime();
328
+ const storedStats = {
329
+ dataSinceDate: timeBeforeStart - interval - 1,
330
+ droppedEvents: 1,
331
+ eventsInLastBatch: 2,
332
+ streamInits: [{ timestamp: 1000, durationMillis: 500 }],
333
+ };
334
+ const overridePlatform = stubPlatform.defaults();
335
+ overridePlatform.diagnosticUseCombinedEvent = true;
336
+ overridePlatform.testing.setLocalStorageImmediately(localStorageKey, JSON.stringify(storedStats));
337
+ await withManager({ diagnosticRecordingInterval: interval }, overridePlatform, async (manager, acc, sender) => {
338
+ const timeBeforeStart = new Date().getTime();
339
+ manager.start();
340
+ await sleepAsync(10); // manager's localstorage logic is async, so allow it to catch up with us
341
+
342
+ expect(sender.calls.length()).toEqual(1);
343
+ const firstEvent = await getPostedEvent(sender);
344
+ expect(firstEvent).toMatchObject({
345
+ kind: 'diagnostic-combined',
346
+ id: diagnosticId,
347
+ sdk: sdkData,
348
+ platform: platformData,
349
+ configuration: { ...defaultConfigInEvent, diagnosticRecordingIntervalMillis: interval },
350
+ ...storedStats,
351
+ });
352
+ expect(firstEvent.creationDate).toBeGreaterThanOrEqual(timeBeforeStart);
353
+
354
+ setupStatsForPeriodicEvent1(acc);
355
+
356
+ const periodic1 = await getPostedEvent(sender);
357
+ expect(periodic1).toMatchObject({
358
+ kind: 'diagnostic-combined',
359
+ id: diagnosticId,
360
+ sdk: sdkData,
361
+ platform: platformData,
362
+ configuration: expectedConfig,
363
+ ...expectedStatsForPeriodicEvent1,
364
+ });
365
+ expect(periodic1.dataSinceDate).toBeGreaterThan(firstEvent.dataSinceDate);
366
+ });
367
+ });
368
+
369
+ it('defers event on start() if event was sent recently', async () => {
370
+ const timeBeforeStart = new Date().getTime();
371
+ const interval = 200;
372
+ const storedStats = {
373
+ dataSinceDate: timeBeforeStart - interval + 100,
374
+ droppedEvents: 1,
375
+ eventsInLastBatch: 2,
376
+ streamInits: [{ timestamp: 1000, durationMillis: 500 }],
377
+ };
378
+ const overridePlatform = stubPlatform.defaults();
379
+ overridePlatform.diagnosticUseCombinedEvent = true;
380
+ overridePlatform.testing.setLocalStorageImmediately(localStorageKey, JSON.stringify(storedStats));
381
+ await withManager({ diagnosticRecordingInterval: interval }, overridePlatform, async (manager, acc, sender) => {
382
+ const timeBeforeStart = new Date().getTime();
383
+ manager.start();
384
+ await sleepAsync(10); // manager's localstorage logic is async, so allow it to catch up with us
385
+ expect(sender.calls.length()).toEqual(0);
386
+
387
+ acc.incrementDroppedEvents();
388
+ acc.setEventsInLastBatch(3);
389
+ acc.recordStreamInit(1001, false, 501);
390
+
391
+ const firstEvent = await getPostedEvent(sender);
392
+ expect(firstEvent).toMatchObject({
393
+ kind: 'diagnostic-combined',
394
+ id: diagnosticId,
395
+ sdk: sdkData,
396
+ platform: platformData,
397
+ configuration: { ...defaultConfigInEvent, diagnosticRecordingIntervalMillis: interval },
398
+ dataSinceDate: storedStats.dataSinceDate,
399
+ droppedEvents: 2,
400
+ eventsInLastBatch: 3,
401
+ streamInits: [{ timestamp: 1000, durationMillis: 500 }, { timestamp: 1001, durationMillis: 501 }],
402
+ });
403
+ expect(firstEvent.creationDate).toBeGreaterThanOrEqual(timeBeforeStart);
404
+ });
405
+ });
406
+
407
+ it('continues sending periodic events', async () => {
408
+ // In the previous tests in this group, we always separately verified the first periodic event (after
409
+ // the initial event) because there could be a different code path for scheduling it depending on the
410
+ // initial conditions. But we can assume that the scheduling of the second event does not depend on the
411
+ // initial conditions - it will always be scheduled when the first one gets sent.
412
+ const interval = 100;
413
+ const expectedConfig = { ...defaultConfigInEvent, diagnosticRecordingIntervalMillis: interval };
414
+ const overridePlatform = stubPlatform.defaults();
415
+ overridePlatform.diagnosticUseCombinedEvent = true;
416
+ await withManager({ diagnosticRecordingInterval: interval }, overridePlatform, async (manager, acc, sender) => {
417
+ manager.start();
418
+
419
+ const firstEvent = await getPostedEvent(sender);
420
+ expect(firstEvent).toMatchObject({
421
+ kind: 'diagnostic-combined',
422
+ dataSinceDate: defaultStartTime,
423
+ });
424
+
425
+ setupStatsForPeriodicEvent1(acc);
426
+
427
+ const periodic1 = await getPostedEvent(sender);
428
+ expect(periodic1).toMatchObject({
429
+ kind: 'diagnostic-combined',
430
+ ...expectedStatsForPeriodicEvent1,
431
+ });
432
+ expect(periodic1.dataSinceDate).toBeGreaterThan(firstEvent.dataSinceDate);
433
+
434
+ setupStatsForPeriodicEvent2(acc);
435
+
436
+ const periodic2 = (await sender.calls.take()).events;
437
+ expect(periodic2).toMatchObject({
438
+ kind: 'diagnostic-combined',
439
+ id: diagnosticId,
440
+ sdk: sdkData,
441
+ platform: platformData,
442
+ configuration: expectedConfig,
443
+ ...expectedStatsForPeriodicEvent2,
444
+ });
445
+ expect(periodic2.dataSinceDate).toBeGreaterThan(periodic1.dataSinceDate);
446
+ });
447
+ });
448
+ });
449
+ });
@@ -0,0 +1,149 @@
1
+ const { format } = require('util');
2
+ const loggers = require('../loggers');
3
+
4
+ describe('commonBasicLogger', () => {
5
+ it('uses console methods by default', () => {
6
+ const realLog = console.log,
7
+ realInfo = console.info,
8
+ realWarn = console.warn,
9
+ realError = console.error;
10
+ const mockLog = jest.fn(),
11
+ mockInfo = jest.fn(),
12
+ mockWarn = jest.fn(),
13
+ mockError = jest.fn();
14
+ try {
15
+ console.log = mockLog;
16
+ console.info = mockInfo;
17
+ console.warn = mockWarn;
18
+ console.error = mockError;
19
+ const logger = loggers.commonBasicLogger({ level: 'debug' });
20
+ logger.debug('a');
21
+ logger.info('b');
22
+ logger.warn('c');
23
+ logger.error('d');
24
+ expect(mockLog).toHaveBeenCalledWith('[LaunchDarkly] a');
25
+ expect(mockInfo).toHaveBeenCalledWith('[LaunchDarkly] b');
26
+ expect(mockWarn).toHaveBeenCalledWith('[LaunchDarkly] c');
27
+ expect(mockError).toHaveBeenCalledWith('[LaunchDarkly] d');
28
+ } finally {
29
+ console.log = realLog;
30
+ console.info = realInfo;
31
+ console.warn = realWarn;
32
+ console.error = realError;
33
+ }
34
+ });
35
+
36
+ it('can write to an arbitrary function', () => {
37
+ const outputFn = jest.fn();
38
+ const logger = loggers.commonBasicLogger({ destination: outputFn });
39
+ logger.warn('hello');
40
+ expect(outputFn).toHaveBeenCalledWith('warn: [LaunchDarkly] hello');
41
+ });
42
+
43
+ it('throws an exception immediately if destination is not a function', () => {
44
+ expect(() => loggers.commonBasicLogger({ destination: 'Mars' })).toThrow();
45
+ });
46
+
47
+ it('does not use formatter if there is only one argument', () => {
48
+ const outputFn = jest.fn();
49
+ const logger = loggers.commonBasicLogger({ destination: outputFn }, format);
50
+ logger.warn('%d things');
51
+ expect(outputFn).toHaveBeenCalledWith('warn: [LaunchDarkly] %d things');
52
+ });
53
+
54
+ it('uses formatter if there are multiple arguments', () => {
55
+ const outputFn = jest.fn();
56
+ const logger = loggers.commonBasicLogger({ destination: outputFn }, format);
57
+ logger.warn('%d things', 3);
58
+ expect(outputFn).toHaveBeenCalledWith('warn: [LaunchDarkly] 3 things');
59
+ });
60
+
61
+ it('does not use formatter if there is none', () => {
62
+ const outputFn = jest.fn();
63
+ const logger = loggers.commonBasicLogger({ destination: outputFn }, null);
64
+ logger.warn('%d things', 3);
65
+ expect(outputFn).toHaveBeenCalledWith('warn: [LaunchDarkly] %d things');
66
+ });
67
+
68
+ describe('output filtering by level', () => {
69
+ const testLevel = (minLevel, enabledLevels) => {
70
+ it('level: ' + minLevel, () => {
71
+ const outputFn = jest.fn();
72
+ const config = { destination: outputFn };
73
+ if (minLevel) {
74
+ config.level = minLevel;
75
+ }
76
+ const logger = loggers.commonBasicLogger({ level: minLevel, destination: outputFn });
77
+ logger.debug('some debug output');
78
+ logger.info('some info output');
79
+ logger.warn('some warn output');
80
+ logger.error('some error output');
81
+ for (const [level, shouldBeEnabled] of Object.entries(enabledLevels)) {
82
+ const line = level + ': [LaunchDarkly] some ' + level + ' output';
83
+ if (shouldBeEnabled) {
84
+ expect(outputFn).toHaveBeenCalledWith(line);
85
+ } else {
86
+ expect(outputFn).not.toHaveBeenCalledWith(line);
87
+ }
88
+ }
89
+ });
90
+ };
91
+
92
+ testLevel('debug', { debug: true, info: true, warn: true, error: true });
93
+ testLevel('info', { debug: false, info: true, warn: true, error: true });
94
+ testLevel('warn', { debug: false, info: false, warn: true, error: true });
95
+ testLevel('error', { debug: false, info: false, warn: false, error: true });
96
+ testLevel('none', { debug: false, info: false, warn: false, error: false });
97
+
98
+ // default is info
99
+ testLevel(undefined, { debug: false, info: true, warn: true, error: true });
100
+ });
101
+
102
+ it('does not throw an error if console is undefined or null', () => {
103
+ const oldConsole = console;
104
+ try {
105
+ console = null; // eslint-disable-line no-global-assign
106
+ const logger = loggers.commonBasicLogger({ level: 'debug' });
107
+ logger.debug('x');
108
+ logger.info('x');
109
+ logger.warn('x');
110
+ logger.error('x');
111
+ console = undefined; // eslint-disable-line no-global-assign
112
+ logger.debug('x');
113
+ logger.info('x');
114
+ logger.warn('x');
115
+ logger.error('x');
116
+ } finally {
117
+ console = oldConsole; // eslint-disable-line no-global-assign
118
+ }
119
+ });
120
+ });
121
+
122
+ describe('validateLogger', () => {
123
+ function mockLogger() {
124
+ return {
125
+ error: jest.fn(),
126
+ warn: jest.fn(),
127
+ info: jest.fn(),
128
+ debug: jest.fn(),
129
+ };
130
+ }
131
+
132
+ const levels = ['error', 'warn', 'info', 'debug'];
133
+
134
+ it('throws an error if the logger does not conform to the LDLogger schema', () => {
135
+ // If the method does not exist
136
+ levels.forEach(method => {
137
+ const logger = mockLogger();
138
+ delete logger[method];
139
+ expect(() => loggers.validateLogger(logger)).toThrow(/Provided logger instance must support .* method/);
140
+ });
141
+
142
+ // If the method is not a function
143
+ levels.forEach(method => {
144
+ const logger = mockLogger();
145
+ logger[method] = 'invalid';
146
+ expect(() => loggers.validateLogger(logger)).toThrow(/Provided logger instance must support .* method/);
147
+ });
148
+ });
149
+ });