launchdarkly-js-sdk-common 4.0.0 → 4.0.1

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 (67) hide show
  1. package/.circleci/config.yml +22 -0
  2. package/.eslintignore +4 -0
  3. package/.eslintrc.yaml +104 -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 +4 -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 +15 -0
  15. package/package.json +3 -29
  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 +7 -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 +761 -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 +772 -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 +96 -0
  61. package/tsconfig.json +13 -0
  62. package/dist/ldclient-common.cjs.js +0 -2
  63. package/dist/ldclient-common.cjs.js.map +0 -1
  64. package/dist/ldclient-common.es.js +0 -2
  65. package/dist/ldclient-common.es.js.map +0 -1
  66. package/dist/ldclient-common.min.js +0 -2
  67. package/dist/ldclient-common.min.js.map +0 -1
@@ -0,0 +1,757 @@
1
+ import * as messages from '../messages';
2
+
3
+ import { withCloseable, sleepAsync } from 'launchdarkly-js-test-helpers';
4
+
5
+ import { respond, respondJson } from './mockHttp';
6
+ import * as stubPlatform from './stubPlatform';
7
+ import { makeBootstrap, numericUser, stringifiedNumericUser } from './testUtils';
8
+
9
+ // These tests verify that the client generates the appropriate analytics events for various scenarios.
10
+ // We use a mock event processor component so that the events are not sent anywhere.
11
+ //
12
+ // We also use a mock HTTP service in a few tests-- not to simulate an event-recorder instance, since
13
+ // we're not testing event delivery here, but to simulate the polling service in cases where the test
14
+ // logic involves a flag request. In all other cases we just start the client with bootstrap data.
15
+
16
+ describe('LDClient events', () => {
17
+ const envName = 'UNKNOWN_ENVIRONMENT_ID';
18
+ const user = { key: 'user' };
19
+ const fakeUrl = 'http://fake';
20
+ let platform;
21
+
22
+ beforeEach(() => {
23
+ platform = stubPlatform.defaults();
24
+ platform.testing.setCurrentUrl(fakeUrl);
25
+ });
26
+
27
+ function stubEventProcessor() {
28
+ const ep = { events: [] };
29
+ ep.start = function() {};
30
+ ep.flush = function() {};
31
+ ep.stop = function() {};
32
+ ep.enqueue = function(e) {
33
+ ep.events.push(e);
34
+ };
35
+ return ep;
36
+ }
37
+
38
+ async function withServer(asyncCallback) {
39
+ const server = platform.testing.http.newServer();
40
+ server.byDefault(respondJson({}));
41
+ return await withCloseable(server, asyncCallback);
42
+ }
43
+
44
+ async function withClientAndEventProcessor(user, extraConfig, asyncCallback) {
45
+ const ep = stubEventProcessor();
46
+ const config = Object.assign({ baseUrl: 'shouldnt-use-this', bootstrap: {}, eventProcessor: ep }, extraConfig);
47
+ const client = platform.testing.makeClient(envName, user, config);
48
+ return await withCloseable(client, async () => await asyncCallback(client, ep));
49
+ }
50
+
51
+ function expectIdentifyEvent(e, user) {
52
+ expect(e.kind).toEqual('identify');
53
+ expect(e.user).toEqual(user);
54
+ }
55
+
56
+ function expectAliasEvent(e, user, previousUser) {
57
+ function userContextKind(user) {
58
+ return user.anonymous ? 'anonymousUser' : 'user';
59
+ }
60
+ expect(e.kind).toEqual('alias');
61
+ expect(e.key).toEqual(user.key);
62
+ expect(e.previousKey).toEqual(previousUser.key);
63
+ expect(e.contextKind).toEqual(userContextKind(user));
64
+ expect(e.previousContextKind).toEqual(userContextKind(previousUser));
65
+ }
66
+
67
+ function expectContextKindInEvent(e, user) {
68
+ if (user.anonymous) {
69
+ expect(e.contextKind).toEqual('anonymousUser');
70
+ } else {
71
+ expect(e.contextKind).toBe(undefined);
72
+ }
73
+ }
74
+
75
+ function expectFeatureEvent({
76
+ e,
77
+ key,
78
+ user,
79
+ value,
80
+ variation,
81
+ version,
82
+ defaultVal,
83
+ trackEvents,
84
+ debugEventsUntilDate,
85
+ }) {
86
+ expect(e.kind).toEqual('feature');
87
+ expect(e.key).toEqual(key);
88
+ expect(e.value).toEqual(value);
89
+ expect(e.variation).toEqual(variation);
90
+ expect(e.version).toEqual(version);
91
+ expect(e.default).toEqual(defaultVal);
92
+ expect(e.trackEvents).toEqual(trackEvents);
93
+ expect(e.debugEventsUntilDate).toEqual(debugEventsUntilDate);
94
+ expectContextKindInEvent(e, user);
95
+ }
96
+
97
+ it('sends an identify event at startup', async () => {
98
+ await withClientAndEventProcessor(user, {}, async (client, ep) => {
99
+ await client.waitForInitialization();
100
+
101
+ expect(ep.events.length).toEqual(1);
102
+ expectIdentifyEvent(ep.events[0], user);
103
+ });
104
+ });
105
+
106
+ it('stringifies user attributes in the identify event at startup', async () => {
107
+ // This just verifies that the event is being sent with the sanitized user, not the user that was passed in
108
+ await withClientAndEventProcessor(numericUser, {}, async (client, ep) => {
109
+ await client.waitForInitialization();
110
+
111
+ expect(ep.events.length).toEqual(1);
112
+ expectIdentifyEvent(ep.events[0], stringifiedNumericUser);
113
+ });
114
+ });
115
+
116
+ it('sends an identify event when identify() is called', async () => {
117
+ // need a server because it'll do a polling request when we call identify
118
+ await withServer(async server => {
119
+ await withClientAndEventProcessor(user, { baseUrl: server.url }, async (client, ep) => {
120
+ const user1 = { key: 'user1' };
121
+ await client.waitForInitialization();
122
+
123
+ expect(ep.events.length).toEqual(1);
124
+ await client.identify(user1);
125
+
126
+ expect(ep.events.length).toEqual(2);
127
+ expectIdentifyEvent(ep.events[1], user1);
128
+ });
129
+ });
130
+ });
131
+
132
+ it('sends an alias event when alias() is called', async () => {
133
+ await withClientAndEventProcessor(user, {}, async (client, ep) => {
134
+ const anon1 = { key: 'user1', anonymous: true };
135
+ const anon2 = { key: 'user2', anonymous: true };
136
+ const known1 = { key: 'user3' };
137
+ const known2 = { key: 'user4' };
138
+ await client.waitForInitialization();
139
+ expect(ep.events.length).toEqual(1);
140
+
141
+ await client.alias(anon2, anon1);
142
+ expectAliasEvent(ep.events[1], anon2, anon1);
143
+
144
+ await client.alias(known1, anon1);
145
+ expectAliasEvent(ep.events[2], known1, anon1);
146
+
147
+ await client.alias(known2, known1);
148
+ expectAliasEvent(ep.events[3], known2, known1);
149
+
150
+ await client.alias(anon1, known1);
151
+ expectAliasEvent(ep.events[4], anon1, known1);
152
+
153
+ expect(ep.events.length).toEqual(5);
154
+ });
155
+ });
156
+
157
+ it('sends an alias event when identify() is called for anon to known', async () => {
158
+ // need a server because it'll do a polling request when we call identify
159
+ await withServer(async server => {
160
+ const anonUser = { key: 'anon-user', anonymous: true };
161
+ const knownUser = { key: 'known-user' };
162
+ await withClientAndEventProcessor(anonUser, { baseUrl: server.url }, async (client, ep) => {
163
+ await client.waitForInitialization();
164
+
165
+ expect(ep.events.length).toEqual(1);
166
+ expectIdentifyEvent(ep.events[0], anonUser);
167
+
168
+ await client.identify(knownUser);
169
+ expect(ep.events.length).toEqual(3);
170
+ expectIdentifyEvent(ep.events[1], knownUser);
171
+ expectAliasEvent(ep.events[2], knownUser, anonUser);
172
+ });
173
+ });
174
+ });
175
+
176
+ it('does not send an alias event when identify() is called if auto-aliasing is disabled', async () => {
177
+ // need a server because it'll do a polling request when we call identify
178
+ await withServer(async server => {
179
+ const anonUser = { key: 'anon-user', anonymous: true };
180
+ const knownUser = { key: 'known-user' };
181
+ await withClientAndEventProcessor(
182
+ anonUser,
183
+ { baseUrl: server.url, autoAliasingOptOut: true },
184
+ async (client, ep) => {
185
+ await client.waitForInitialization();
186
+
187
+ expect(ep.events.length).toEqual(1);
188
+ expectIdentifyEvent(ep.events[0], anonUser);
189
+
190
+ await client.identify(knownUser);
191
+ expect(ep.events.length).toEqual(2); //no additional alias events
192
+ expectIdentifyEvent(ep.events[1], knownUser);
193
+ }
194
+ );
195
+ });
196
+ });
197
+
198
+ it('does not send an alias event when identify() is called for known to anon', async () => {
199
+ // need a server because it'll do a polling request when we call identify
200
+ await withServer(async server => {
201
+ const knownUser = { key: 'known-user' };
202
+ const anonUser = { key: 'anon-user', anonymous: true };
203
+ await withClientAndEventProcessor(knownUser, { baseUrl: server.url }, async (client, ep) => {
204
+ await client.waitForInitialization();
205
+
206
+ expect(ep.events.length).toEqual(1);
207
+ expectIdentifyEvent(ep.events[0], knownUser);
208
+
209
+ await client.identify(anonUser);
210
+ expect(ep.events.length).toEqual(2); //no additional alias events
211
+ expectIdentifyEvent(ep.events[1], anonUser);
212
+ });
213
+ });
214
+ });
215
+
216
+ it('does not send an alias event when identify() is called for anon to anon', async () => {
217
+ // need a server because it'll do a polling request when we call identify
218
+ await withServer(async server => {
219
+ const anonUser1 = { key: 'anon-user1', anonymous: true };
220
+ const anonUser2 = { key: 'anon-user2', anonymous: true };
221
+ await withClientAndEventProcessor(anonUser1, { baseUrl: server.url }, async (client, ep) => {
222
+ await client.waitForInitialization();
223
+
224
+ expect(ep.events.length).toEqual(1);
225
+ expectIdentifyEvent(ep.events[0], anonUser1);
226
+
227
+ await client.identify(anonUser2);
228
+ expect(ep.events.length).toEqual(2); //no additional alias events
229
+ expectIdentifyEvent(ep.events[1], anonUser2);
230
+ });
231
+ });
232
+ });
233
+
234
+ it('does not send an alias event when identify() is called for known to known', async () => {
235
+ // need a server because it'll do a polling request when we call identify
236
+ await withServer(async server => {
237
+ const knownUser1 = { key: 'known-user1' };
238
+ const knownUser2 = { key: 'known-user2' };
239
+ await withClientAndEventProcessor(knownUser1, { baseUrl: server.url }, async (client, ep) => {
240
+ await client.waitForInitialization();
241
+
242
+ expect(ep.events.length).toEqual(1);
243
+ expectIdentifyEvent(ep.events[0], knownUser1);
244
+
245
+ await client.identify(knownUser2);
246
+ expect(ep.events.length).toEqual(2); //no additional alias events
247
+ expectIdentifyEvent(ep.events[1], knownUser2);
248
+ });
249
+ });
250
+ });
251
+
252
+ it('stringifies user attributes in the identify event when identify() is called', async () => {
253
+ // This just verifies that the event is being sent with the sanitized user, not the user that was passed in
254
+ await withServer(async server => {
255
+ await withClientAndEventProcessor(user, { baseUrl: server.url }, async (client, ep) => {
256
+ await client.waitForInitialization();
257
+
258
+ expect(ep.events.length).toEqual(1);
259
+ await client.identify(numericUser);
260
+
261
+ expect(ep.events.length).toEqual(2);
262
+ expectIdentifyEvent(ep.events[1], stringifiedNumericUser);
263
+ });
264
+ });
265
+ });
266
+
267
+ it('does not send an identify event if doNotTrack is set', async () => {
268
+ platform.testing.setDoNotTrack(true);
269
+ await withServer(async server => {
270
+ await withClientAndEventProcessor(user, { baseUrl: server.url }, async (client, ep) => {
271
+ const user1 = { key: 'user1' };
272
+
273
+ await client.waitForInitialization();
274
+ await client.identify(user1);
275
+
276
+ expect(ep.events.length).toEqual(0);
277
+ });
278
+ });
279
+ });
280
+
281
+ it('sends a feature event for variation()', async () => {
282
+ const initData = makeBootstrap({ foo: { value: 'a', variation: 1, version: 2, flagVersion: 2000 } });
283
+ await withClientAndEventProcessor(user, { bootstrap: initData }, async (client, ep) => {
284
+ await client.waitForInitialization();
285
+
286
+ client.variation('foo', 'x');
287
+
288
+ expect(ep.events.length).toEqual(2);
289
+ expectIdentifyEvent(ep.events[0], user);
290
+ expectFeatureEvent({
291
+ e: ep.events[1],
292
+ key: 'foo',
293
+ user,
294
+ value: 'a',
295
+ variation: 1,
296
+ version: 2000,
297
+ defaultVal: 'x',
298
+ });
299
+ });
300
+ });
301
+
302
+ it('sends a feature event for variation() when user is anonymous', async () => {
303
+ const initData = makeBootstrap({ foo: { value: 'a', variation: 1, version: 2, flagVersion: 2000 } });
304
+ await withServer(async server => {
305
+ const anonUser = { key: 'anon-user', anonymous: true };
306
+ await withClientAndEventProcessor(anonUser, { baseUrl: server.url, bootstrap: initData }, async (client, ep) => {
307
+ await client.waitForInitialization();
308
+
309
+ client.variation('foo', 'x');
310
+
311
+ expect(ep.events.length).toEqual(2);
312
+ expectIdentifyEvent(ep.events[0], anonUser);
313
+ expectFeatureEvent({
314
+ e: ep.events[1],
315
+ key: 'foo',
316
+ user: anonUser,
317
+ value: 'a',
318
+ variation: 1,
319
+ version: 2000,
320
+ defaultVal: 'x',
321
+ });
322
+ });
323
+ });
324
+ });
325
+
326
+ it('sends a feature event with reason for variationDetail()', async () => {
327
+ const initData = makeBootstrap({
328
+ foo: { value: 'a', variation: 1, version: 2, flagVersion: 2000, reason: { kind: 'OFF' } },
329
+ });
330
+ await withClientAndEventProcessor(user, { bootstrap: initData }, async (client, ep) => {
331
+ await client.waitForInitialization();
332
+ client.variationDetail('foo', 'x');
333
+
334
+ expect(ep.events.length).toEqual(2);
335
+ expectIdentifyEvent(ep.events[0], user);
336
+ expectFeatureEvent({
337
+ e: ep.events[1],
338
+ key: 'foo',
339
+ user,
340
+ value: 'a',
341
+ variation: 1,
342
+ version: 2000,
343
+ defaultVal: 'x',
344
+ });
345
+ expect(ep.events[1].reason).toEqual({ kind: 'OFF' });
346
+ });
347
+ });
348
+
349
+ it('does not include reason in event for variation() even if reason is available', async () => {
350
+ const initData = makeBootstrap({
351
+ foo: { value: 'a', variation: 1, version: 2, flagVersion: 2000, reason: { kind: 'OFF' } },
352
+ });
353
+ await withClientAndEventProcessor(user, { bootstrap: initData }, async (client, ep) => {
354
+ await client.waitForInitialization();
355
+ client.variation('foo', 'x');
356
+
357
+ expect(ep.events.length).toEqual(2);
358
+ expectIdentifyEvent(ep.events[0], user);
359
+ expectFeatureEvent({
360
+ e: ep.events[1],
361
+ key: 'foo',
362
+ user,
363
+ value: 'a',
364
+ variation: 1,
365
+ version: 2000,
366
+ defaultVal: 'x',
367
+ });
368
+ expect(ep.events[1].reason).toBe(undefined);
369
+ });
370
+ });
371
+
372
+ it('sends a feature event with reason for variation() if trackReason is set', async () => {
373
+ const initData = makeBootstrap({
374
+ foo: { value: 'a', variation: 1, version: 2, flagVersion: 2000, reason: { kind: 'OFF' }, trackReason: true },
375
+ });
376
+ await withClientAndEventProcessor(user, { bootstrap: initData }, async (client, ep) => {
377
+ await client.waitForInitialization();
378
+ client.variation('foo', 'x');
379
+
380
+ expect(ep.events.length).toEqual(2);
381
+ expectIdentifyEvent(ep.events[0], user);
382
+ expectFeatureEvent({
383
+ e: ep.events[1],
384
+ key: 'foo',
385
+ user,
386
+ value: 'a',
387
+ variation: 1,
388
+ version: 2000,
389
+ defaultVal: 'x',
390
+ });
391
+ expect(ep.events[1].reason).toEqual({ kind: 'OFF' });
392
+ });
393
+ });
394
+
395
+ it('sends a feature event on receiving a new flag value', async () => {
396
+ const oldFlags = { foo: { value: 'a', variation: 1, version: 2, flagVersion: 2000 } };
397
+ const newFlags = { foo: { value: 'b', variation: 2, version: 3, flagVersion: 2001 } };
398
+ const initData = makeBootstrap(oldFlags);
399
+ await withServer(async server => {
400
+ server.byDefault(respondJson(newFlags));
401
+ await withClientAndEventProcessor(user, { baseUrl: server.url, bootstrap: initData }, async (client, ep) => {
402
+ await client.waitForInitialization();
403
+
404
+ const user1 = { key: 'user1' };
405
+ await client.identify(user1);
406
+
407
+ expect(ep.events.length).toEqual(3);
408
+ expectIdentifyEvent(ep.events[0], user);
409
+ expectIdentifyEvent(ep.events[1], user1);
410
+ expectFeatureEvent({ e: ep.events[2], key: 'foo', user: user1, value: 'b', variation: 2, version: 2001 });
411
+ });
412
+ });
413
+ });
414
+
415
+ it('does not send a feature event for a new flag value if sendEventsOnlyForVariation is set', async () => {
416
+ const oldFlags = { foo: { value: 'a', variation: 1, version: 2, flagVersion: 2000 } };
417
+ const newFlags = { foo: { value: 'b', variation: 2, version: 3, flagVersion: 2001 } };
418
+ const initData = makeBootstrap(oldFlags);
419
+ await withServer(async server => {
420
+ server.byDefault(respondJson(newFlags));
421
+ const extraConfig = { sendEventsOnlyForVariation: true, baseUrl: server.url, bootstrap: initData };
422
+ await withClientAndEventProcessor(user, extraConfig, async (client, ep) => {
423
+ await client.waitForInitialization();
424
+
425
+ const user1 = { key: 'user1' };
426
+ await client.identify(user1);
427
+
428
+ expect(ep.events.length).toEqual(2);
429
+ expectIdentifyEvent(ep.events[0], user);
430
+ expectIdentifyEvent(ep.events[1], user1);
431
+ });
432
+ });
433
+ });
434
+
435
+ it('does not send a feature event for a new flag value if there is a state provider', async () => {
436
+ const oldFlags = { foo: { value: 'a', variation: 1, version: 2, flagVersion: 2000 } };
437
+ const newFlags = { foo: { value: 'b', variation: 2, version: 3, flagVersion: 2001 } };
438
+ const sp = stubPlatform.mockStateProvider({ environment: envName, user: user, flags: oldFlags });
439
+ await withServer(async server => {
440
+ server.byDefault(respondJson(newFlags));
441
+ const extraConfig = { stateProvider: sp, baseUrl: server.url };
442
+ await withClientAndEventProcessor(user, extraConfig, async (client, ep) => {
443
+ await client.waitForInitialization();
444
+
445
+ sp.emit('update', { flags: newFlags });
446
+
447
+ expect(client.variation('foo')).toEqual('b');
448
+ expect(ep.events.length).toEqual(1);
449
+ });
450
+ });
451
+ });
452
+
453
+ it('sends feature events for allFlags()', async () => {
454
+ const initData = makeBootstrap({
455
+ foo: { value: 'a', variation: 1, version: 2 },
456
+ bar: { value: 'b', variation: 1, version: 3 },
457
+ });
458
+ await withClientAndEventProcessor(user, { bootstrap: initData }, async (client, ep) => {
459
+ await client.waitForInitialization();
460
+ client.allFlags();
461
+
462
+ expect(ep.events.length).toEqual(3);
463
+ expectIdentifyEvent(ep.events[0], user);
464
+ expectFeatureEvent({ e: ep.events[1], key: 'foo', user, value: 'a', variation: 1, version: 2, defaultVal: null });
465
+ expectFeatureEvent({ e: ep.events[2], key: 'bar', user, value: 'b', variation: 1, version: 3, defaultVal: null });
466
+ });
467
+ });
468
+
469
+ it('does not send feature events for allFlags() if sendEventsOnlyForVariation is set', async () => {
470
+ const initData = makeBootstrap({
471
+ foo: { value: 'a', variation: 1, version: 2 },
472
+ bar: { value: 'b', variation: 1, version: 3 },
473
+ });
474
+ const extraConfig = { sendEventsOnlyForVariation: true, bootstrap: initData };
475
+ await withClientAndEventProcessor(user, extraConfig, async (client, ep) => {
476
+ await client.waitForInitialization();
477
+ client.allFlags();
478
+
479
+ expect(ep.events.length).toEqual(1);
480
+ expectIdentifyEvent(ep.events[0], user);
481
+ });
482
+ });
483
+
484
+ it('uses "version" instead of "flagVersion" in event if "flagVersion" is absent', async () => {
485
+ const initData = makeBootstrap({ foo: { value: 'a', variation: 1, version: 2 } });
486
+ await withClientAndEventProcessor(user, { bootstrap: initData }, async (client, ep) => {
487
+ await client.waitForInitialization();
488
+ client.variation('foo', 'x');
489
+
490
+ expect(ep.events.length).toEqual(2);
491
+ expectIdentifyEvent(ep.events[0], user);
492
+ expectFeatureEvent({ e: ep.events[1], key: 'foo', user, value: 'a', variation: 1, version: 2, defaultVal: 'x' });
493
+ });
494
+ });
495
+
496
+ it('omits event version if flag does not exist', async () => {
497
+ await withClientAndEventProcessor(user, {}, async (client, ep) => {
498
+ await client.waitForInitialization();
499
+ client.variation('foo', 'x');
500
+
501
+ expect(ep.events.length).toEqual(2);
502
+ expectIdentifyEvent(ep.events[0], user);
503
+ expectFeatureEvent({ e: ep.events[1], key: 'foo', user, value: 'x', variation: null, defaultVal: 'x' });
504
+ });
505
+ });
506
+
507
+ it('can get metadata for events from bootstrap object', async () => {
508
+ const initData = makeBootstrap({
509
+ foo: {
510
+ value: 'bar',
511
+ variation: 1,
512
+ version: 2,
513
+ trackEvents: true,
514
+ debugEventsUntilDate: 1000,
515
+ },
516
+ });
517
+ await withClientAndEventProcessor(user, { bootstrap: initData }, async (client, ep) => {
518
+ await withCloseable(client, async () => {
519
+ await client.waitForInitialization();
520
+ client.variation('foo', 'x');
521
+
522
+ expect(ep.events.length).toEqual(2);
523
+ expectIdentifyEvent(ep.events[0], user);
524
+ expectFeatureEvent({
525
+ e: ep.events[1],
526
+ key: 'foo',
527
+ value: 'bar',
528
+ variation: 1,
529
+ version: 2,
530
+ defaultVal: 'x',
531
+ trackEvents: true,
532
+ debugEventsUntilDate: 1000,
533
+ user,
534
+ });
535
+ });
536
+ });
537
+ });
538
+
539
+ it('sends an event for track()', async () => {
540
+ await withClientAndEventProcessor(user, {}, async (client, ep) => {
541
+ await client.waitForInitialization();
542
+ client.track('eventkey');
543
+
544
+ expect(ep.events.length).toEqual(2);
545
+ expectIdentifyEvent(ep.events[0], user);
546
+ const trackEvent = ep.events[1];
547
+ expect(trackEvent.kind).toEqual('custom');
548
+ expect(trackEvent.key).toEqual('eventkey');
549
+ expect(trackEvent.user).toEqual(user);
550
+ expect(trackEvent.data).toEqual(undefined);
551
+ expect(trackEvent.url).toEqual(fakeUrl);
552
+ expectContextKindInEvent(trackEvent, user);
553
+ });
554
+ });
555
+
556
+ it('sends an event for track() when user is anonymous', async () => {
557
+ await withServer(async server => {
558
+ const anonUser = { key: 'anon-user', anonymous: true };
559
+ await withClientAndEventProcessor(anonUser, { baseUrl: server.url }, async (client, ep) => {
560
+ await client.waitForInitialization();
561
+ client.track('eventkey');
562
+
563
+ expect(ep.events.length).toEqual(2);
564
+ expectIdentifyEvent(ep.events[0], anonUser);
565
+ const trackEvent = ep.events[1];
566
+ expect(trackEvent.kind).toEqual('custom');
567
+ expect(trackEvent.key).toEqual('eventkey');
568
+ expect(trackEvent.user).toEqual(anonUser);
569
+ expect(trackEvent.data).toEqual(undefined);
570
+ expect(trackEvent.url).toEqual(fakeUrl);
571
+ expectContextKindInEvent(trackEvent, anonUser);
572
+ });
573
+ });
574
+ });
575
+
576
+ it('sends an event for track() with data', async () => {
577
+ await withClientAndEventProcessor(user, {}, async (client, ep) => {
578
+ const eventData = { thing: 'stuff' };
579
+ await client.waitForInitialization();
580
+ client.track('eventkey', eventData);
581
+
582
+ expect(ep.events.length).toEqual(2);
583
+ expectIdentifyEvent(ep.events[0], user);
584
+ const trackEvent = ep.events[1];
585
+ expect(trackEvent.kind).toEqual('custom');
586
+ expect(trackEvent.key).toEqual('eventkey');
587
+ expect(trackEvent.user).toEqual(user);
588
+ expect(trackEvent.data).toEqual(eventData);
589
+ expect(trackEvent.url).toEqual(fakeUrl);
590
+ expectContextKindInEvent(trackEvent, user);
591
+ });
592
+ });
593
+
594
+ it('sends an event for track() with metric value', async () => {
595
+ await withClientAndEventProcessor(user, {}, async (client, ep) => {
596
+ const eventData = { thing: 'stuff' };
597
+ const metricValue = 1.5;
598
+ await client.waitForInitialization();
599
+ client.track('eventkey', eventData, metricValue);
600
+
601
+ expect(ep.events.length).toEqual(2);
602
+ expectIdentifyEvent(ep.events[0], user);
603
+ const trackEvent = ep.events[1];
604
+ expect(trackEvent.kind).toEqual('custom');
605
+ expect(trackEvent.key).toEqual('eventkey');
606
+ expect(trackEvent.user).toEqual(user);
607
+ expect(trackEvent.data).toEqual(eventData);
608
+ expect(trackEvent.metricValue).toEqual(metricValue);
609
+ expect(trackEvent.url).toEqual(fakeUrl);
610
+ expectContextKindInEvent(trackEvent, user);
611
+ });
612
+ });
613
+
614
+ it('does not send an event for track() if doNotTrack is set', async () => {
615
+ platform.testing.setDoNotTrack(true);
616
+ await withClientAndEventProcessor(user, {}, async (client, ep) => {
617
+ const eventData = { thing: 'stuff' };
618
+ await client.waitForInitialization();
619
+ client.track('eventkey', eventData);
620
+ expect(ep.events.length).toEqual(0);
621
+ });
622
+ });
623
+
624
+ it('does not warn by default when tracking a custom event', async () => {
625
+ await withClientAndEventProcessor(user, {}, async client => {
626
+ await client.waitForInitialization();
627
+
628
+ client.track('known');
629
+ expect(platform.testing.logger.output.warn).toEqual([]);
630
+ });
631
+ });
632
+
633
+ it('emits an error when tracking a non-string custom event', async () => {
634
+ await withClientAndEventProcessor(user, {}, async client => {
635
+ await client.waitForInitialization();
636
+
637
+ const badCustomEventKeys = [123, [], {}, null, undefined];
638
+ badCustomEventKeys.forEach(key => {
639
+ platform.testing.logger.reset();
640
+ client.track(key);
641
+ expect(platform.testing.logger.output.error).toEqual([messages.unknownCustomEventKey(key)]);
642
+ });
643
+ });
644
+ });
645
+
646
+ it('should warn about missing user on first event', async () => {
647
+ await withClientAndEventProcessor(null, {}, async client => {
648
+ client.track('eventkey', null);
649
+ expect(platform.testing.logger.output.warn).toEqual([messages.eventWithoutUser()]);
650
+ });
651
+ });
652
+
653
+ it('allows stateProvider to take over sending an event', async () => {
654
+ const sp = stubPlatform.mockStateProvider({ environment: envName, user: user, flags: {} });
655
+ const divertedEvents = [];
656
+ sp.enqueueEvent = event => divertedEvents.push(event);
657
+
658
+ await withClientAndEventProcessor(user, { stateProvider: sp }, async (client, ep) => {
659
+ await client.waitForInitialization();
660
+
661
+ client.track('eventkey');
662
+ expect(ep.events.length).toEqual(0);
663
+ expect(divertedEvents.length).toEqual(1);
664
+ expect(divertedEvents[0].kind).toEqual('custom');
665
+ });
666
+ });
667
+
668
+ async function expectDiagnosticEventAndDiscardRegularEvent(server) {
669
+ const req0 = await server.nextRequest();
670
+ const req1 = await server.nextRequest();
671
+ const expectedPath = '/events/diagnostic/' + envName;
672
+ const otherPath = '/events/bulk/' + envName;
673
+ let initEventReq;
674
+ if (req0.path === expectedPath) {
675
+ expect(req1.path).toEqual(otherPath);
676
+ initEventReq = req0;
677
+ } else {
678
+ expect(req0.path).toEqual(otherPath);
679
+ expect(req1.path).toEqual(expectedPath);
680
+ initEventReq = req1;
681
+ }
682
+ return JSON.parse(initEventReq.body);
683
+ }
684
+
685
+ async function expectNoMoreRequests(server, timeout) {
686
+ await sleepAsync(timeout);
687
+ expect(server.requests.length()).toEqual(0);
688
+ }
689
+
690
+ it('sends diagnostic init event on startup', async () => {
691
+ const server = platform.testing.http.newServer();
692
+ server.byDefault(respond(202));
693
+ await withCloseable(server, async () => {
694
+ const config = {
695
+ baseUrl: 'shouldnt-use-this',
696
+ bootstrap: {},
697
+ eventsUrl: server.url,
698
+ };
699
+ const client = platform.testing.makeClient(envName, user, config);
700
+ await withCloseable(client, async () => {
701
+ await client.waitForInitialization();
702
+ await client.flush();
703
+ const data = await expectDiagnosticEventAndDiscardRegularEvent(server);
704
+ expect(data.kind).toEqual('diagnostic-init');
705
+ expect(data.platform).toEqual(platform.diagnosticPlatformData);
706
+ expect(data.sdk).toEqual(platform.diagnosticSdkData);
707
+ await expectNoMoreRequests(server, 50);
708
+ });
709
+ });
710
+ });
711
+
712
+ it('sends diagnostic combined event on startup', async () => {
713
+ const platform1 = stubPlatform.defaults();
714
+ platform1.diagnosticUseCombinedEvent = true;
715
+ const server = platform1.testing.http.newServer();
716
+ server.byDefault(respond(202));
717
+ await withCloseable(server, async () => {
718
+ const config = {
719
+ baseUrl: 'shouldnt-use-this',
720
+ bootstrap: {},
721
+ eventsUrl: server.url,
722
+ };
723
+ const client = platform1.testing.makeClient(envName, user, config);
724
+ await withCloseable(client, async () => {
725
+ await client.waitForInitialization();
726
+ await client.flush();
727
+ const data = await expectDiagnosticEventAndDiscardRegularEvent(server);
728
+ expect(data.kind).toEqual('diagnostic-combined');
729
+ expect(data.platform).toEqual(platform1.diagnosticPlatformData);
730
+ expect(data.sdk).toEqual(platform1.diagnosticSdkData);
731
+ await expectNoMoreRequests(server, 50);
732
+ });
733
+ });
734
+ });
735
+
736
+ it('does not send diagnostic init event when opted out', async () => {
737
+ const server = platform.testing.http.newServer();
738
+ server.byDefault(respond(202));
739
+ await withCloseable(server, async () => {
740
+ const config = {
741
+ baseUrl: 'shouldnt-use-this',
742
+ bootstrap: {},
743
+ eventsUrl: server.url,
744
+ diagnosticOptOut: true,
745
+ };
746
+ const client = platform.testing.makeClient(envName, user, config);
747
+ await withCloseable(client, async () => {
748
+ await client.waitForInitialization();
749
+ await client.flush();
750
+ expect(server.requests.length()).toEqual(1);
751
+ const req = await server.nextRequest();
752
+ expect(req.path).toEqual('/events/bulk/' + envName);
753
+ await expectNoMoreRequests(server, 50);
754
+ });
755
+ });
756
+ });
757
+ });