@depup/launchdarkly-node-server-sdk 7.0.4-depup.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.
Files changed (107) hide show
  1. package/.babelrc +16 -0
  2. package/.circleci/config.yml +89 -0
  3. package/.eslintignore +5 -0
  4. package/.eslintrc.yaml +114 -0
  5. package/.github/ISSUE_TEMPLATE/bug_report.md +37 -0
  6. package/.github/ISSUE_TEMPLATE/config.yml +5 -0
  7. package/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
  8. package/.github/pull_request_template.md +21 -0
  9. package/.github/workflows/stale.yml +8 -0
  10. package/.hound.yml +33 -0
  11. package/.ldrelease/config.yml +28 -0
  12. package/.prettierrc +6 -0
  13. package/CHANGELOG.md +603 -0
  14. package/CODEOWNERS +2 -0
  15. package/CONTRIBUTING.md +55 -0
  16. package/LICENSE.txt +13 -0
  17. package/README.md +36 -0
  18. package/SECURITY.md +5 -0
  19. package/attribute_reference.js +217 -0
  20. package/big_segments.js +117 -0
  21. package/caching_store_wrapper.js +240 -0
  22. package/changes.json +30 -0
  23. package/configuration.js +235 -0
  24. package/context.js +98 -0
  25. package/context_filter.js +137 -0
  26. package/contract-tests/README.md +7 -0
  27. package/contract-tests/index.js +109 -0
  28. package/contract-tests/log.js +23 -0
  29. package/contract-tests/package.json +15 -0
  30. package/contract-tests/sdkClientEntity.js +110 -0
  31. package/contract-tests/testharness-suppressions.txt +2 -0
  32. package/diagnostic_events.js +151 -0
  33. package/docs/typedoc.js +10 -0
  34. package/errors.js +26 -0
  35. package/evaluator.js +822 -0
  36. package/event_factory.js +121 -0
  37. package/event_processor.js +320 -0
  38. package/event_summarizer.js +101 -0
  39. package/feature_store.js +120 -0
  40. package/feature_store_event_wrapper.js +258 -0
  41. package/file_data_source.js +192 -0
  42. package/flags_state.js +46 -0
  43. package/index.d.ts +2426 -0
  44. package/index.js +452 -0
  45. package/integrations.js +7 -0
  46. package/interfaces.js +2 -0
  47. package/loggers.js +125 -0
  48. package/messages.js +31 -0
  49. package/operators.js +106 -0
  50. package/package.json +105 -0
  51. package/polling.js +70 -0
  52. package/requestor.js +62 -0
  53. package/scripts/better-audit.sh +76 -0
  54. package/sharedtest/big_segment_store_tests.js +86 -0
  55. package/sharedtest/feature_store_tests.js +177 -0
  56. package/sharedtest/persistent_feature_store_tests.js +183 -0
  57. package/sharedtest/store_tests.js +7 -0
  58. package/streaming.js +179 -0
  59. package/test/LDClient-big-segments-test.js +92 -0
  60. package/test/LDClient-end-to-end-test.js +218 -0
  61. package/test/LDClient-evaluation-all-flags-test.js +226 -0
  62. package/test/LDClient-evaluation-test.js +204 -0
  63. package/test/LDClient-events-test.js +502 -0
  64. package/test/LDClient-listeners-test.js +180 -0
  65. package/test/LDClient-test.js +96 -0
  66. package/test/LDClient-tls-test.js +110 -0
  67. package/test/attribute_reference-test.js +494 -0
  68. package/test/big_segments-test.js +182 -0
  69. package/test/caching_store_wrapper-test.js +434 -0
  70. package/test/configuration-test.js +249 -0
  71. package/test/context-test.js +93 -0
  72. package/test/context_filter-test.js +424 -0
  73. package/test/diagnostic_events-test.js +152 -0
  74. package/test/evaluator-big-segments-test.js +301 -0
  75. package/test/evaluator-bucketing-test.js +333 -0
  76. package/test/evaluator-clause-test.js +277 -0
  77. package/test/evaluator-flag-test.js +452 -0
  78. package/test/evaluator-pre-conditions-test.js +105 -0
  79. package/test/evaluator-rule-test.js +131 -0
  80. package/test/evaluator-segment-match-test.js +310 -0
  81. package/test/evaluator_helpers.js +106 -0
  82. package/test/event_processor-test.js +680 -0
  83. package/test/event_summarizer-test.js +146 -0
  84. package/test/feature_store-test.js +42 -0
  85. package/test/feature_store_event_wrapper-test.js +182 -0
  86. package/test/feature_store_test_base.js +60 -0
  87. package/test/file_data_source-test.js +255 -0
  88. package/test/loggers-test.js +126 -0
  89. package/test/operators-test.js +102 -0
  90. package/test/polling-test.js +158 -0
  91. package/test/requestor-test.js +60 -0
  92. package/test/store_tests_big_segments-test.js +61 -0
  93. package/test/streaming-test.js +323 -0
  94. package/test/stubs.js +107 -0
  95. package/test/test_data-test.js +341 -0
  96. package/test/update_queue-test.js +61 -0
  97. package/test-types.ts +210 -0
  98. package/test_data.js +323 -0
  99. package/tsconfig.json +14 -0
  100. package/update_queue.js +28 -0
  101. package/utils/__tests__/httpUtils-test.js +39 -0
  102. package/utils/__tests__/wrapPromiseCallback-test.js +33 -0
  103. package/utils/asyncUtils.js +32 -0
  104. package/utils/httpUtils.js +105 -0
  105. package/utils/stringifyAttrs.js +14 -0
  106. package/utils/wrapPromiseCallback.js +36 -0
  107. package/versioned_data_kind.js +34 -0
@@ -0,0 +1,680 @@
1
+ const { DiagnosticsManager, DiagnosticId } = require('../diagnostic_events');
2
+ const EventProcessor = require('../event_processor');
3
+ const { failOnTimeout, TestHttpHandlers, TestHttpServer, withCloseable } = require('launchdarkly-js-test-helpers');
4
+
5
+ describe('EventProcessor', () => {
6
+
7
+ const eventsUri = 'http://example.com';
8
+ const sdkKey = 'SDK_KEY';
9
+ const defaultConfig = {
10
+ eventsUri: eventsUri,
11
+ capacity: 100,
12
+ flushInterval: 30,
13
+ contextKeysCapacity: 1000,
14
+ contextKeysFlushInterval: 300,
15
+ diagnosticRecordingInterval: 900,
16
+ logger: {
17
+ debug: jest.fn(),
18
+ warn: jest.fn()
19
+ }
20
+ };
21
+ const user = { key: 'userKey', name: 'Red' };
22
+ const singleKindUser = { ...user, kind: 'user' };
23
+ const anonUser = { key: 'anon-user', name: 'Anon', anonymous: true };
24
+ const singleKindAnonUser = {key: 'anon-user', kind: 'user', name: 'Anon', anonymous: true };
25
+ const filteredUser = { key: 'userKey', kind: 'user', _meta: { redactedAttributes: ['/name'] } };
26
+ const numericUser = {
27
+ key: 1, ip: 3, country: 4, email: 5, firstName: 6, lastName: 7,
28
+ avatar: 8, name: 9, anonymous: false, custom: { age: 99 }
29
+ };
30
+ const stringifiedNumericUser = {
31
+ kind: 'user', key: '1', ip: '3', country: '4', email: '5', firstName: '6',
32
+ lastName: '7', avatar: '8', name: '9', age: 99, anonymous: false
33
+ };
34
+
35
+ function eventsServerTest(asyncCallback) {
36
+ return async () => withCloseable(TestHttpServer.start, async server => {
37
+ server.forMethodAndPath('post', '/bulk', TestHttpHandlers.respond(200));
38
+ server.forMethodAndPath('post', '/diagnostic', TestHttpHandlers.respond(200));
39
+ return await asyncCallback(server);
40
+ });
41
+ }
42
+
43
+ async function withEventProcessor(baseConfig, server, asyncCallback) {
44
+ const config = Object.assign({}, baseConfig, { eventsUri: server.url, diagnosticOptOut: true });
45
+ const ep = EventProcessor(sdkKey, config);
46
+ try {
47
+ return await asyncCallback(ep);
48
+ } finally {
49
+ ep.close();
50
+ }
51
+ }
52
+
53
+ async function withDiagnosticEventProcessor(baseConfig, server, asyncCallback) {
54
+ const config = Object.assign({}, baseConfig, { eventsUri: server.url });
55
+ const id = DiagnosticId(sdkKey);
56
+ const manager = DiagnosticsManager(config, id, new Date().getTime());
57
+ const ep = EventProcessor(sdkKey, config, null, manager);
58
+ try {
59
+ return await asyncCallback(ep, id, manager);
60
+ } finally {
61
+ ep.close();
62
+ }
63
+ }
64
+
65
+ function headersWithDate(timestamp) {
66
+ return { date: new Date(timestamp).toUTCString() };
67
+ }
68
+
69
+ function checkIndexEvent(e, source, context) {
70
+ expect(e.kind).toEqual('index');
71
+ expect(e.creationDate).toEqual(source.creationDate);
72
+ expect(e.context).toEqual(context);
73
+ }
74
+
75
+ function checkFeatureEvent(e, source, debug, contextKeys, inlineContext) {
76
+ expect(e.kind).toEqual(debug ? 'debug' : 'feature');
77
+ expect(e.creationDate).toEqual(source.creationDate);
78
+ expect(e.key).toEqual(source.key);
79
+ expect(e.version).toEqual(source.version);
80
+ expect(e.variation).toEqual(source.variation);
81
+ expect(e.value).toEqual(source.value);
82
+ expect(e.default).toEqual(source.default);
83
+ expect(e.reason).toEqual(source.reason);
84
+ if (inlineContext) {
85
+ expect(e.context).toEqual(inlineContext);
86
+ } else {
87
+ expect(e.contextKeys).toEqual(contextKeys);
88
+ }
89
+ }
90
+
91
+ function checkCustomEvent(e, source, contextKeys) {
92
+ expect(e.kind).toEqual('custom');
93
+ expect(e.creationDate).toEqual(source.creationDate);
94
+ expect(e.key).toEqual(source.key);
95
+ expect(e.data).toEqual(source.data);
96
+ expect(e.metricValue).toBe(source.metricValue);
97
+
98
+ expect(e.contextKeys).toEqual(contextKeys);
99
+ }
100
+
101
+ function checkSummaryEvent(e) {
102
+ expect(e.kind).toEqual('summary');
103
+ }
104
+
105
+ async function getJsonRequest(server) {
106
+ return JSON.parse((await server.nextRequest()).body);
107
+ }
108
+
109
+ it('queues identify event', eventsServerTest(async s => {
110
+ await withEventProcessor(defaultConfig, s, async ep => {
111
+ const e = { kind: 'identify', creationDate: 1000, context: user };
112
+ ep.sendEvent(e);
113
+ await ep.flush();
114
+
115
+ const output = await getJsonRequest(s);
116
+ expect(output).toEqual([{
117
+ kind: 'identify',
118
+ creationDate: 1000,
119
+ context: singleKindUser
120
+ }]);
121
+ });
122
+ }));
123
+
124
+ it('filters user in identify event', eventsServerTest(async s => {
125
+ const config = Object.assign({}, defaultConfig, { allAttributesPrivate: true });
126
+ await withEventProcessor(config, s, async ep => {
127
+ const e = { kind: 'identify', creationDate: 1000, context: user };
128
+ ep.sendEvent(e);
129
+ await ep.flush();
130
+
131
+ const output = await getJsonRequest(s);
132
+ expect(output).toEqual([{
133
+ kind: 'identify',
134
+ creationDate: 1000,
135
+ context: filteredUser
136
+ }]);
137
+ });
138
+ }));
139
+
140
+ it('stringifies user attributes in identify event', eventsServerTest(async s => {
141
+ await withEventProcessor(defaultConfig, s, async ep => {
142
+ const e = { kind: 'identify', creationDate: 1000, context: numericUser };
143
+ ep.sendEvent(e);
144
+ await ep.flush();
145
+
146
+ const output = await getJsonRequest(s);
147
+ expect(output).toEqual([{
148
+ kind: 'identify',
149
+ creationDate: 1000,
150
+ context: stringifiedNumericUser
151
+ }]);
152
+ });
153
+ }));
154
+
155
+ it('queues individual feature event with index event', eventsServerTest(async s => {
156
+ await withEventProcessor(defaultConfig, s, async ep => {
157
+ const e = {
158
+ kind: 'feature', creationDate: 1000, context: user, key: 'flagkey',
159
+ version: 11, variation: 1, value: 'value', trackEvents: true
160
+ };
161
+ ep.sendEvent(e);
162
+ await ep.flush();
163
+
164
+ const output = await getJsonRequest(s);
165
+ expect(output.length).toEqual(3);
166
+ checkIndexEvent(output[0], e, singleKindUser);
167
+ checkFeatureEvent(output[1], e, false, {user: 'userKey'});
168
+ checkSummaryEvent(output[2]);
169
+ });
170
+ }));
171
+
172
+ it('handles the version being 0', eventsServerTest(async s => {
173
+ await withEventProcessor(defaultConfig, s, async ep => {
174
+ const e = { kind: 'feature', creationDate: 1000, context: user, key: 'flagkey',
175
+ version: 0, variation: 1, value: 'value', trackEvents: true };
176
+ ep.sendEvent(e);
177
+ await ep.flush();
178
+
179
+ const output = await getJsonRequest(s);
180
+ expect(output.length).toEqual(3);
181
+ checkIndexEvent(output[0], e, singleKindUser);
182
+ checkFeatureEvent(output[1], e, false, {user: 'userKey'});
183
+ checkSummaryEvent(output[2]);
184
+ });
185
+ }));
186
+
187
+ it('queues individual feature event with index event for anonymous user', eventsServerTest(async s => {
188
+ await withEventProcessor(defaultConfig, s, async ep => {
189
+ const e = {
190
+ kind: 'feature', creationDate: 1000, context: anonUser, key: 'flagkey',
191
+ version: 11, variation: 1, value: 'value', trackEvents: true
192
+ };
193
+ ep.sendEvent(e);
194
+ await ep.flush();
195
+
196
+ const output = await getJsonRequest(s);
197
+ expect(output.length).toEqual(3);
198
+ checkIndexEvent(output[0], e, singleKindAnonUser);
199
+ checkFeatureEvent(output[1], e, false, {user: 'anon-user'});
200
+ checkSummaryEvent(output[2]);
201
+ });
202
+ }));
203
+
204
+ it('filters user in index event', eventsServerTest(async s => {
205
+ const config = Object.assign({}, defaultConfig, { allAttributesPrivate: true });
206
+ await withEventProcessor(config, s, async ep => {
207
+ const e = {
208
+ kind: 'feature', creationDate: 1000, context: user, key: 'flagkey',
209
+ version: 11, variation: 1, value: 'value', trackEvents: true
210
+ };
211
+ ep.sendEvent(e);
212
+ await ep.flush();
213
+
214
+ const output = await getJsonRequest(s);
215
+ expect(output.length).toEqual(3);
216
+ checkIndexEvent(output[0], e, filteredUser);
217
+ checkFeatureEvent(output[1], e, false, {user: 'userKey'});
218
+ checkSummaryEvent(output[2]);
219
+ });
220
+ }));
221
+
222
+ it('stringifies user attributes in index event', eventsServerTest(async s => {
223
+ await withEventProcessor(defaultConfig, s, async ep => {
224
+ const e = {
225
+ kind: 'feature', creationDate: 1000, context: numericUser, key: 'flagkey',
226
+ version: 11, variation: 1, value: 'value', trackEvents: true
227
+ };
228
+ ep.sendEvent(e);
229
+ await ep.flush();
230
+
231
+ const output = await getJsonRequest(s);
232
+ expect(output.length).toEqual(3);
233
+ checkIndexEvent(output[0], e, stringifiedNumericUser);
234
+ checkFeatureEvent(output[1], e, false, {user: '1'});
235
+ checkSummaryEvent(output[2]);
236
+ });
237
+ }));
238
+
239
+ it('can include reason in feature event', eventsServerTest(async s => {
240
+ await withEventProcessor(defaultConfig, s, async ep => {
241
+ const e = {
242
+ kind: 'feature', creationDate: 1000, context: user, key: 'flagkey',
243
+ version: 11, variation: 1, value: 'value', trackEvents: true,
244
+ reason: { kind: 'FALLTHROUGH' }
245
+ };
246
+ ep.sendEvent(e);
247
+ await ep.flush();
248
+
249
+ const output = await getJsonRequest(s);
250
+ expect(output.length).toEqual(3);
251
+ checkIndexEvent(output[0], e, singleKindUser);
252
+ checkFeatureEvent(output[1], e, false, {user: 'userKey'});
253
+ checkSummaryEvent(output[2]);
254
+ });
255
+ }));
256
+
257
+ it('sets event kind to debug if event is temporarily in debug mode', eventsServerTest(async s => {
258
+ await withEventProcessor(defaultConfig, s, async ep => {
259
+ var futureTime = new Date().getTime() + 1000000;
260
+ const e = {
261
+ kind: 'feature', creationDate: 1000, context: user, key: 'flagkey',
262
+ version: 11, variation: 1, value: 'value', trackEvents: false, debugEventsUntilDate: futureTime
263
+ };
264
+ ep.sendEvent(e);
265
+ await ep.flush();
266
+
267
+ const output = await getJsonRequest(s);
268
+ expect(output.length).toEqual(3);
269
+ checkIndexEvent(output[0], e, singleKindUser);
270
+ checkFeatureEvent(output[1], e, true, {user: 'userKey'}, singleKindUser);
271
+ checkSummaryEvent(output[2]);
272
+ });
273
+ }));
274
+
275
+ it('can both track and debug an event', eventsServerTest(async s => {
276
+ await withEventProcessor(defaultConfig, s, async ep => {
277
+ const futureTime = new Date().getTime() + 1000000;
278
+ const e = {
279
+ kind: 'feature', creationDate: 1000, context: user, key: 'flagkey',
280
+ version: 11, variation: 1, value: 'value', trackEvents: true, debugEventsUntilDate: futureTime
281
+ };
282
+ ep.sendEvent(e);
283
+ await ep.flush();
284
+
285
+ const output = await getJsonRequest(s);
286
+ expect(output.length).toEqual(4);
287
+ checkIndexEvent(output[0], e, singleKindUser);
288
+ checkFeatureEvent(output[1], e, false, {user: 'userKey'});
289
+ checkFeatureEvent(output[2], e, true, {user: 'userKey'}, singleKindUser);
290
+ checkSummaryEvent(output[3]);
291
+ });
292
+ }));
293
+
294
+ it('expires debug mode based on client time if client time is later than server time', eventsServerTest(async s => {
295
+ await withEventProcessor(defaultConfig, s, async ep => {
296
+ // Pick a server time that is somewhat behind the client time
297
+ const serverTime = new Date().getTime() - 20000;
298
+ s.forMethodAndPath('post', '/bulk', TestHttpHandlers.respond(200, headersWithDate(serverTime)));
299
+
300
+ // Send and flush an event we don't care about, just to set the last server time
301
+ ep.sendEvent({ kind: 'identify', context: { key: 'otherUser' } });
302
+ await ep.flush();
303
+ await s.nextRequest();
304
+
305
+ // Now send an event with debug mode on, with a "debug until" time that is further in
306
+ // the future than the server time, but in the past compared to the client.
307
+ const debugUntil = serverTime + 1000;
308
+ const e = {
309
+ kind: 'feature', creationDate: 1000, context: user, key: 'flagkey',
310
+ version: 11, variation: 1, value: 'value', trackEvents: false, debugEventsUntilDate: debugUntil
311
+ };
312
+ ep.sendEvent(e);
313
+ await ep.flush();
314
+
315
+ // Should get a summary event only, not a full feature event
316
+ const output = await getJsonRequest(s);
317
+ expect(output.length).toEqual(2);
318
+ checkIndexEvent(output[0], e, singleKindUser);
319
+ checkSummaryEvent(output[1]);
320
+ });
321
+ }));
322
+
323
+ it('expires debug mode based on server time if server time is later than client time', eventsServerTest(async s => {
324
+ await withEventProcessor(defaultConfig, s, async ep => {
325
+ // Pick a server time that is somewhat ahead of the client time
326
+ const serverTime = new Date().getTime() + 20000;
327
+ s.forMethodAndPath('post', '/bulk', TestHttpHandlers.respond(200, headersWithDate(serverTime)));
328
+
329
+ // Send and flush an event we don't care about, just to set the last server time
330
+ ep.sendEvent({ kind: 'identify', context: { key: 'otherUser' } });
331
+ await ep.flush();
332
+ await s.nextRequest();
333
+
334
+ // Now send an event with debug mode on, with a "debug until" time that is further in
335
+ // the future than the client time, but in the past compared to the server.
336
+ const debugUntil = serverTime - 1000;
337
+ const e = {
338
+ kind: 'feature', creationDate: 1000, context: user, key: 'flagkey',
339
+ version: 11, variation: 1, value: 'value', trackEvents: false, debugEventsUntilDate: debugUntil
340
+ };
341
+ ep.sendEvent(e);
342
+ await ep.flush();
343
+
344
+ // Should get a summary event only, not a full feature event
345
+ const output = await getJsonRequest(s);
346
+ expect(output.length).toEqual(2);
347
+ checkIndexEvent(output[0], e, singleKindUser);
348
+ checkSummaryEvent(output[1]);
349
+ });
350
+ }));
351
+
352
+ it('generates only one index event from two feature events for same user', eventsServerTest(async s => {
353
+ await withEventProcessor(defaultConfig, s, async ep => {
354
+ const e1 = {
355
+ kind: 'feature', creationDate: 1000, context: user, key: 'flagkey1',
356
+ version: 11, variation: 1, value: 'value', trackEvents: true
357
+ };
358
+ const e2 = {
359
+ kind: 'feature', creationDate: 1000, context: user, key: 'flagkey2',
360
+ version: 11, variation: 1, value: 'value', trackEvents: true
361
+ };
362
+ ep.sendEvent(e1);
363
+ ep.sendEvent(e2);
364
+ await ep.flush();
365
+
366
+ const output = await getJsonRequest(s);
367
+ expect(output.length).toEqual(4);
368
+ checkIndexEvent(output[0], e1, singleKindUser);
369
+ checkFeatureEvent(output[1], e1, false, {user: 'userKey'});
370
+ checkFeatureEvent(output[2], e2, false, {user: 'userKey'});
371
+ checkSummaryEvent(output[3]);
372
+ });
373
+ }));
374
+
375
+ it('summarizes nontracked events', eventsServerTest(async s => {
376
+ await withEventProcessor(defaultConfig, s, async ep => {
377
+ const e1 = {
378
+ kind: 'feature', creationDate: 1000, context: user, key: 'flagkey1',
379
+ version: 11, variation: 1, value: 'value1', default: 'default1', trackEvents: false
380
+ };
381
+ const e2 = {
382
+ kind: 'feature', creationDate: 2000, context: user, key: 'flagkey2',
383
+ version: 22, variation: 1, value: 'value2', default: 'default2', trackEvents: false
384
+ };
385
+ ep.sendEvent(e1);
386
+ ep.sendEvent(e2);
387
+ await ep.flush();
388
+
389
+ const output = await getJsonRequest(s);
390
+ expect(output.length).toEqual(2);
391
+ checkIndexEvent(output[0], e1, singleKindUser);
392
+ const se = output[1];
393
+ checkSummaryEvent(se);
394
+ expect(se.startDate).toEqual(1000);
395
+ expect(se.endDate).toEqual(2000);
396
+ expect(se.features).toEqual({
397
+ flagkey1: {
398
+ default: 'default1',
399
+ counters: [{ version: 11, variation: 1, value: 'value1', count: 1 }],
400
+ contextKinds: ['user']
401
+ },
402
+ flagkey2: {
403
+ default: 'default2',
404
+ counters: [{ version: 22, variation: 1, value: 'value2', count: 1 }],
405
+ contextKinds: ['user']
406
+ }
407
+ });
408
+ });
409
+ }));
410
+
411
+ it('queues custom event with user', eventsServerTest(async s => {
412
+ await withEventProcessor(defaultConfig, s, async ep => {
413
+ const e = {
414
+ kind: 'custom', creationDate: 1000, context: user, key: 'eventkey',
415
+ data: { thing: 'stuff' }
416
+ };
417
+ ep.sendEvent(e);
418
+ await ep.flush();
419
+
420
+ const output = await getJsonRequest(s);
421
+ expect(output.length).toEqual(2);
422
+ checkIndexEvent(output[0], e, singleKindUser);
423
+ checkCustomEvent(output[1], e, {user: 'userKey'});
424
+ });
425
+ }));
426
+
427
+ it('queues custom event with anonymous user', eventsServerTest(async s => {
428
+ await withEventProcessor(defaultConfig, s, async ep => {
429
+ const e = {
430
+ kind: 'custom', creationDate: 1000, context: anonUser, key: 'eventkey', data: { thing: 'stuff' }
431
+ };
432
+ ep.sendEvent(e);
433
+ await ep.flush();
434
+
435
+ const output = await getJsonRequest(s);
436
+ expect(output.length).toEqual(2);
437
+ checkIndexEvent(output[0], e, singleKindAnonUser);
438
+ checkCustomEvent(output[1], e, {user: 'anon-user'});
439
+ });
440
+ }));
441
+
442
+ it('can include metric value in custom event', eventsServerTest(async s => {
443
+ await withEventProcessor(defaultConfig, s, async ep => {
444
+ const e = {
445
+ kind: 'custom', creationDate: 1000, context: user, key: 'eventkey',
446
+ data: { thing: 'stuff' }, metricValue: 1.5
447
+ };
448
+ ep.sendEvent(e);
449
+ await ep.flush();
450
+
451
+ const output = await getJsonRequest(s);
452
+ expect(output.length).toEqual(2);
453
+ checkIndexEvent(output[0], e, singleKindUser);
454
+ checkCustomEvent(output[1], e, {user: 'userKey'});
455
+ });
456
+ }));
457
+
458
+ it('sends nothing if there are no events', eventsServerTest(async s => {
459
+ await withEventProcessor(defaultConfig, s, async ep => {
460
+ await ep.flush();
461
+ expect(s.requestCount()).toEqual(0);
462
+ });
463
+ }));
464
+
465
+ it('sends SDK key', eventsServerTest(async s => {
466
+ await withEventProcessor(defaultConfig, s, async ep => {
467
+ const e = { kind: 'identify', creationDate: 1000, context: user };
468
+ ep.sendEvent(e);
469
+ await ep.flush();
470
+
471
+ const request = await s.nextRequest();
472
+ expect(request.headers['authorization']).toEqual(sdkKey);
473
+ });
474
+ }));
475
+
476
+ it('sends unique payload IDs', eventsServerTest(async s => {
477
+ await withEventProcessor(defaultConfig, s, async ep => {
478
+ const e = { kind: 'identify', creationDate: 1000, context: user };
479
+ ep.sendEvent(e);
480
+ await ep.flush();
481
+ ep.sendEvent(e);
482
+ await ep.flush();
483
+
484
+ const req0 = await s.nextRequest();
485
+ const req1 = await s.nextRequest();
486
+ const id0 = req0.headers['x-launchdarkly-payload-id'];
487
+ const id1 = req1.headers['x-launchdarkly-payload-id'];
488
+ expect(id0).toBeTruthy();
489
+ expect(id1).toBeTruthy();
490
+ expect(id0).not.toEqual(id1);
491
+ });
492
+ }));
493
+
494
+ function verifyUnrecoverableHttpError(status) {
495
+ return eventsServerTest(async s => {
496
+ s.forMethodAndPath('post', '/bulk', TestHttpHandlers.respond(status));
497
+ await withEventProcessor(defaultConfig, s, async ep => {
498
+ const e = { kind: 'identify', creationDate: 1000, context: user };
499
+ ep.sendEvent(e);
500
+ await expect(ep.flush()).rejects.toThrow('error ' + status);
501
+
502
+ expect(s.requestCount()).toEqual(1);
503
+ await s.nextRequest();
504
+
505
+ ep.sendEvent(e);
506
+ await expect(ep.flush()).rejects.toThrow(/SDK key is invalid/);
507
+ expect(s.requestCount()).toEqual(1);
508
+ });
509
+ });
510
+ }
511
+
512
+ function verifyRecoverableHttpError(status) {
513
+ return eventsServerTest(async s => {
514
+ s.forMethodAndPath('post', '/bulk', TestHttpHandlers.respond(status));
515
+ await withEventProcessor(defaultConfig, s, async ep => {
516
+ var e = { kind: 'identify', creationDate: 1000, context: user };
517
+ ep.sendEvent(e);
518
+ await expect(ep.flush()).rejects.toThrow('error ' + status);
519
+
520
+ expect(s.requestCount()).toEqual(2);
521
+ const req0 = await s.nextRequest();
522
+ const req1 = await s.nextRequest();
523
+ expect(req0.body).toEqual(req1.body);
524
+ const id0 = req0.headers['x-launchdarkly-payload-id'];
525
+ expect(req1.headers['x-launchdarkly-payload-id']).toEqual(id0);
526
+
527
+ s.forMethodAndPath('post', '/bulk', TestHttpHandlers.respond(200));
528
+ ep.sendEvent(e);
529
+ await ep.flush();
530
+ expect(s.requestCount()).toEqual(3);
531
+ const req2 = await s.nextRequest();
532
+ expect(req2.headers['x-launchdarkly-payload-id']).not.toEqual(id0);
533
+ });
534
+ });
535
+ }
536
+
537
+ it('retries after a 400 error', verifyRecoverableHttpError(400));
538
+
539
+ it('stops sending events after a 401 error', verifyUnrecoverableHttpError(401));
540
+
541
+ it('stops sending events after a 403 error', verifyUnrecoverableHttpError(403));
542
+
543
+ it('retries after a 408 error', verifyRecoverableHttpError(408));
544
+
545
+ it('retries after a 429 error', verifyRecoverableHttpError(429));
546
+
547
+ it('retries after a 503 error', verifyRecoverableHttpError(503));
548
+
549
+ it('swallows errors from failed background flush', eventsServerTest(async s => {
550
+ // This test verifies that when a background flush fails, we don't emit an unhandled
551
+ // promise rejection. Jest will fail the test if we do that.
552
+
553
+ const config = Object.assign({}, defaultConfig, { flushInterval: 0.25 });
554
+ await withEventProcessor(config, s, async ep => {
555
+ s.forMethodAndPath('post', '/bulk', TestHttpHandlers.respond(500));
556
+
557
+ ep.sendEvent({ kind: 'identify', creationDate: 1000, context: user });
558
+
559
+ // unfortunately we must wait for both the flush interval and the 1-second retry interval
560
+ await failOnTimeout(s.nextRequest(), 500, 'timed out waiting for event payload');
561
+ await failOnTimeout(s.nextRequest(), 1500, 'timed out waiting for event payload');
562
+ });
563
+ }));
564
+
565
+ describe('diagnostic events', () => {
566
+ it('sends initial diagnostic event', eventsServerTest(async s => {
567
+ const startTime = new Date().getTime();
568
+ await withDiagnosticEventProcessor(defaultConfig, s, async (ep, id) => {
569
+ const req = await s.nextRequest();
570
+ expect(req.path).toEqual('/diagnostic');
571
+ const data = JSON.parse(req.body);
572
+ expect(data.kind).toEqual('diagnostic-init');
573
+ expect(data.id).toEqual(id);
574
+ expect(data.creationDate).toBeGreaterThanOrEqual(startTime);
575
+ expect(data.configuration).toMatchObject({ customEventsURI: true });
576
+ expect(data.sdk).toMatchObject({ name: 'node-server-sdk' });
577
+ expect(data.platform).toMatchObject({ name: 'Node' });
578
+ });
579
+ }));
580
+
581
+ it('sends periodic diagnostic event', eventsServerTest(async s => {
582
+ const startTime = new Date().getTime();
583
+ const config = Object.assign({}, defaultConfig, { diagnosticRecordingInterval: 0.1 });
584
+ await withDiagnosticEventProcessor(config, s, async (ep, id) => {
585
+ const req0 = await s.nextRequest();
586
+ expect(req0.path).toEqual('/diagnostic');
587
+
588
+ const req1 = await s.nextRequest();
589
+ expect(req1.path).toEqual('/diagnostic');
590
+ const data = JSON.parse(req1.body);
591
+ expect(data.kind).toEqual('diagnostic');
592
+ expect(data.id).toEqual(id);
593
+ expect(data.creationDate).toBeGreaterThanOrEqual(startTime);
594
+ expect(data.dataSinceDate).toBeGreaterThanOrEqual(startTime);
595
+ expect(data.droppedEvents).toEqual(0);
596
+ expect(data.deduplicatedUsers).toEqual(0);
597
+ expect(data.eventsInLastBatch).toEqual(0);
598
+ });
599
+ }));
600
+
601
+ it('counts events in queue from last flush and dropped events', eventsServerTest(async s => {
602
+ const startTime = new Date().getTime();
603
+ const config = Object.assign({}, defaultConfig, { diagnosticRecordingInterval: 0.1, capacity: 2 });
604
+ await withDiagnosticEventProcessor(config, s, async (ep, id) => {
605
+ const req0 = await s.nextRequest();
606
+ expect(req0.path).toEqual('/diagnostic');
607
+
608
+ ep.sendEvent({ kind: 'identify', creationDate: 1000, context: user });
609
+ ep.sendEvent({ kind: 'identify', creationDate: 1001, context: user });
610
+ ep.sendEvent({ kind: 'identify', creationDate: 1002, context: user });
611
+ await ep.flush();
612
+
613
+ // We can't be sure which will be posted first, the regular events or the diagnostic event
614
+ const requests = [];
615
+ const req1 = await s.nextRequest();
616
+ requests.push({ path: req1.path, data: JSON.parse(req1.body) });
617
+ const req2 = await s.nextRequest();
618
+ requests.push({ path: req2.path, data: JSON.parse(req2.body) });
619
+
620
+ expect(requests).toContainEqual({
621
+ path: '/bulk',
622
+ data: expect.arrayContaining([
623
+ expect.objectContaining({ kind: 'identify', creationDate: 1000 }),
624
+ expect.objectContaining({ kind: 'identify', creationDate: 1001 }),
625
+ ]),
626
+ });
627
+
628
+ expect(requests).toContainEqual({
629
+ path: '/diagnostic',
630
+ data: expect.objectContaining({
631
+ kind: 'diagnostic',
632
+ id: id,
633
+ droppedEvents: 1,
634
+ deduplicatedUsers: 0,
635
+ eventsInLastBatch: 2,
636
+ }),
637
+ });
638
+ });
639
+ }));
640
+
641
+ it('counts deduplicated users', eventsServerTest(async s => {
642
+ const startTime = new Date().getTime();
643
+ const config = Object.assign({}, defaultConfig, { diagnosticRecordingInterval: 0.1 });
644
+ await withDiagnosticEventProcessor(config, s, async (ep, id) => {
645
+ const req0 = await s.nextRequest();
646
+ expect(req0.path).toEqual('/diagnostic');
647
+
648
+ ep.sendEvent({ kind: 'track', key: 'eventkey1', creationDate: 1000, context: user });
649
+ ep.sendEvent({ kind: 'track', key: 'eventkey2', creationDate: 1001, context: user });
650
+ await ep.flush();
651
+
652
+ // We can't be sure which will be posted first, the regular events or the diagnostic event
653
+ const requests = [];
654
+ const req1 = await s.nextRequest();
655
+ requests.push({ path: req1.path, data: JSON.parse(req1.body) });
656
+ const req2 = await s.nextRequest();
657
+ requests.push({ path: req2.path, data: JSON.parse(req2.body) });
658
+
659
+ expect(requests).toContainEqual({
660
+ path: '/bulk',
661
+ data: expect.arrayContaining([
662
+ expect.objectContaining({ kind: 'track', creationDate: 1000 }),
663
+ expect.objectContaining({ kind: 'track', creationDate: 1001 }),
664
+ ]),
665
+ });
666
+
667
+ expect(requests).toContainEqual({
668
+ path: '/diagnostic',
669
+ data: expect.objectContaining({
670
+ kind: 'diagnostic',
671
+ id: id,
672
+ droppedEvents: 0,
673
+ deduplicatedUsers: 1,
674
+ eventsInLastBatch: 3, // 2 "track" + 1 "index"
675
+ }),
676
+ });
677
+ });
678
+ }));
679
+ });
680
+ });