launchdarkly-js-sdk-common 5.7.0-beta.2 → 5.7.0-beta.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/EventProcessor.js +2 -8
- package/src/EventSummarizer.js +0 -7
- package/src/MultiEventSummarizer.js +20 -35
- package/src/__tests__/EventProcessor-test.js +68 -86
- package/src/__tests__/EventSummarizer-test.js +0 -32
- package/src/__tests__/context-test.js +1 -330
- package/src/__tests__/stubPlatform.js +0 -9
- package/src/context.js +0 -37
package/package.json
CHANGED
package/src/EventProcessor.js
CHANGED
|
@@ -5,7 +5,6 @@ const errors = require('./errors');
|
|
|
5
5
|
const messages = require('./messages');
|
|
6
6
|
const utils = require('./utils');
|
|
7
7
|
const { getContextKeys } = require('./context');
|
|
8
|
-
const EventSummarizer = require('./EventSummarizer');
|
|
9
8
|
|
|
10
9
|
function EventProcessor(
|
|
11
10
|
platform,
|
|
@@ -19,12 +18,7 @@ function EventProcessor(
|
|
|
19
18
|
const eventSender = sender || EventSender(platform, environmentId, options);
|
|
20
19
|
const mainEventsUrl = utils.appendUrlPath(options.eventsUrl, '/events/bulk/' + environmentId);
|
|
21
20
|
const contextFilter = ContextFilter(options);
|
|
22
|
-
|
|
23
|
-
// Generally packages should be pinning a specific version of the common SDK, but this will handle them potentially
|
|
24
|
-
// being mis-matched.
|
|
25
|
-
const summarizer = platform.hasherFactory
|
|
26
|
-
? MultiEventSummarizer(contextFilter, () => platform.hasherFactory('sha256'))
|
|
27
|
-
: new EventSummarizer();
|
|
21
|
+
const summarizer = MultiEventSummarizer(contextFilter);
|
|
28
22
|
const samplingInterval = options.samplingInterval;
|
|
29
23
|
const eventCapacity = options.eventCapacity;
|
|
30
24
|
const flushInterval = options.flushInterval;
|
|
@@ -128,7 +122,7 @@ function EventProcessor(
|
|
|
128
122
|
return Promise.resolve();
|
|
129
123
|
}
|
|
130
124
|
const eventsToSend = queue;
|
|
131
|
-
const summaries =
|
|
125
|
+
const summaries = summarizer.getSummaries();
|
|
132
126
|
|
|
133
127
|
summaries.forEach(summary => {
|
|
134
128
|
if (Object.keys(summary.features).length) {
|
package/src/EventSummarizer.js
CHANGED
|
@@ -93,13 +93,6 @@ function EventSummarizer() {
|
|
|
93
93
|
};
|
|
94
94
|
};
|
|
95
95
|
|
|
96
|
-
es.getSummaries = () => {
|
|
97
|
-
const summary = es.getSummary();
|
|
98
|
-
const summaries = summary ? [summary] : [];
|
|
99
|
-
es.clearSummary();
|
|
100
|
-
return summaries;
|
|
101
|
-
};
|
|
102
|
-
|
|
103
96
|
es.clearSummary = () => {
|
|
104
97
|
startDate = 0;
|
|
105
98
|
endDate = 0;
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
const canonicalize = require('./canonicalize');
|
|
2
|
+
const EventSummarizer = require('./EventSummarizer');
|
|
3
|
+
|
|
3
4
|
/**
|
|
4
5
|
*
|
|
5
6
|
* @param {{filter: (context: any) => any}} contextFilter
|
|
6
7
|
* @param {() => {update: (value: string) => void, digest: (format: string) => Promise<string>}} hasherFactory
|
|
7
8
|
*/
|
|
8
|
-
function MultiEventSummarizer(contextFilter
|
|
9
|
+
function MultiEventSummarizer(contextFilter) {
|
|
9
10
|
let summarizers = {};
|
|
10
11
|
let contexts = {};
|
|
11
|
-
const pendingPromises = [];
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Summarize the given event.
|
|
@@ -18,51 +18,36 @@ function MultiEventSummarizer(contextFilter, hasherFactory) {
|
|
|
18
18
|
* }} event
|
|
19
19
|
*/
|
|
20
20
|
function summarizeEvent(event) {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const hash = await hashContext(event.context, hasherFactory());
|
|
26
|
-
if (!hash) {
|
|
27
|
-
return;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
let summarizer = summarizers[hash];
|
|
31
|
-
if (!summarizer) {
|
|
32
|
-
summarizers[hash] = EventSummarizer();
|
|
33
|
-
summarizer = summarizers[hash];
|
|
34
|
-
contexts[hash] = event.context;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
summarizer.summarizeEvent(event);
|
|
21
|
+
if (event.kind === 'feature') {
|
|
22
|
+
const key = canonicalize(event.context);
|
|
23
|
+
if (!key) {
|
|
24
|
+
return;
|
|
38
25
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
26
|
+
|
|
27
|
+
let summarizer = summarizers[key];
|
|
28
|
+
if (!summarizer) {
|
|
29
|
+
summarizers[key] = EventSummarizer();
|
|
30
|
+
summarizer = summarizers[key];
|
|
31
|
+
contexts[key] = event.context;
|
|
45
32
|
}
|
|
46
|
-
|
|
33
|
+
|
|
34
|
+
summarizer.summarizeEvent(event);
|
|
35
|
+
}
|
|
47
36
|
}
|
|
48
37
|
|
|
49
38
|
/**
|
|
50
39
|
* Get the summaries of the events that have been summarized.
|
|
51
40
|
* @returns {any[]}
|
|
52
41
|
*/
|
|
53
|
-
|
|
54
|
-
// Wait for any pending summarizations to complete
|
|
55
|
-
// Additional tasks queued while waiting will not be waited for.
|
|
56
|
-
await Promise.all([...pendingPromises]);
|
|
57
|
-
|
|
42
|
+
function getSummaries() {
|
|
58
43
|
const summarizersToFlush = summarizers;
|
|
59
44
|
const contextsForSummaries = contexts;
|
|
60
45
|
|
|
61
46
|
summarizers = {};
|
|
62
47
|
contexts = {};
|
|
63
|
-
return Object.entries(summarizersToFlush).map(([
|
|
48
|
+
return Object.entries(summarizersToFlush).map(([key, summarizer]) => {
|
|
64
49
|
const summary = summarizer.getSummary();
|
|
65
|
-
summary.context = contextFilter.filter(contextsForSummaries[
|
|
50
|
+
summary.context = contextFilter.filter(contextsForSummaries[key]);
|
|
66
51
|
return summary;
|
|
67
52
|
});
|
|
68
53
|
}
|
|
@@ -485,6 +485,73 @@ describe.each([
|
|
|
485
485
|
});
|
|
486
486
|
});
|
|
487
487
|
|
|
488
|
+
it('generates separate summary events for different contexts', async () => {
|
|
489
|
+
await withProcessorAndSender(defaultConfig, async (ep, mockEventSender) => {
|
|
490
|
+
const context1 = { key: 'user1', kind: 'user' };
|
|
491
|
+
const context2 = { key: 'user2', kind: 'user' };
|
|
492
|
+
|
|
493
|
+
// Create feature events for two different contexts
|
|
494
|
+
const event1 = {
|
|
495
|
+
kind: 'feature',
|
|
496
|
+
creationDate: 1000,
|
|
497
|
+
context: context1,
|
|
498
|
+
key: 'flag1',
|
|
499
|
+
version: 11,
|
|
500
|
+
variation: 1,
|
|
501
|
+
value: 'value1',
|
|
502
|
+
default: 'default1',
|
|
503
|
+
trackEvents: false,
|
|
504
|
+
};
|
|
505
|
+
|
|
506
|
+
const event2 = {
|
|
507
|
+
kind: 'feature',
|
|
508
|
+
creationDate: 1000,
|
|
509
|
+
context: context2,
|
|
510
|
+
key: 'flag2',
|
|
511
|
+
version: 22,
|
|
512
|
+
variation: 2,
|
|
513
|
+
value: 'value2',
|
|
514
|
+
default: 'default2',
|
|
515
|
+
trackEvents: false,
|
|
516
|
+
};
|
|
517
|
+
|
|
518
|
+
ep.enqueue(event1);
|
|
519
|
+
ep.enqueue(event2);
|
|
520
|
+
await ep.flush();
|
|
521
|
+
|
|
522
|
+
expect(mockEventSender.calls.length()).toEqual(1);
|
|
523
|
+
const output = (await mockEventSender.calls.take()).events;
|
|
524
|
+
|
|
525
|
+
// Should have two summary events, one for each context
|
|
526
|
+
expect(output.length).toEqual(2);
|
|
527
|
+
|
|
528
|
+
// Find the summary event for each context
|
|
529
|
+
const summary1 = output.find(e => e.context.key === 'user1');
|
|
530
|
+
const summary2 = output.find(e => e.context.key === 'user2');
|
|
531
|
+
|
|
532
|
+
// Verify each summary event has the correct context and flag data
|
|
533
|
+
expect(summary1).toBeDefined();
|
|
534
|
+
expect(summary1.context).toEqual(context1);
|
|
535
|
+
expect(summary1.features).toEqual({
|
|
536
|
+
flag1: {
|
|
537
|
+
contextKinds: ['user'],
|
|
538
|
+
default: 'default1',
|
|
539
|
+
counters: [{ version: 11, variation: 1, value: 'value1', count: 1 }],
|
|
540
|
+
},
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
expect(summary2).toBeDefined();
|
|
544
|
+
expect(summary2.context).toEqual(context2);
|
|
545
|
+
expect(summary2.features).toEqual({
|
|
546
|
+
flag2: {
|
|
547
|
+
contextKinds: ['user'],
|
|
548
|
+
default: 'default2',
|
|
549
|
+
counters: [{ version: 22, variation: 2, value: 'value2', count: 1 }],
|
|
550
|
+
},
|
|
551
|
+
});
|
|
552
|
+
});
|
|
553
|
+
});
|
|
554
|
+
|
|
488
555
|
describe('interaction with diagnostic events', () => {
|
|
489
556
|
it('sets eventsInLastBatch on flush', async () => {
|
|
490
557
|
const e0 = { kind: 'custom', creationDate: 1000, context: eventContext, key: 'key0' };
|
|
@@ -526,92 +593,7 @@ describe.each([
|
|
|
526
593
|
});
|
|
527
594
|
});
|
|
528
595
|
|
|
529
|
-
it('
|
|
530
|
-
const event = {
|
|
531
|
-
kind: 'feature',
|
|
532
|
-
creationDate: 1000,
|
|
533
|
-
context: eventContext,
|
|
534
|
-
key: 'flagkey',
|
|
535
|
-
version: 11,
|
|
536
|
-
variation: 1,
|
|
537
|
-
value: 'value',
|
|
538
|
-
default: 'default',
|
|
539
|
-
trackEvents: true,
|
|
540
|
-
};
|
|
541
|
-
|
|
542
|
-
const platformWithoutHasher = { ...platform };
|
|
543
|
-
delete platformWithoutHasher.hasherFactory;
|
|
544
|
-
|
|
545
|
-
const sender = MockEventSender();
|
|
546
|
-
const ep = EventProcessor(platformWithoutHasher, defaultConfig, envId, null, null, sender);
|
|
547
|
-
try {
|
|
548
|
-
ep.enqueue(event);
|
|
549
|
-
await ep.flush();
|
|
550
|
-
|
|
551
|
-
expect(sender.calls.length()).toEqual(1);
|
|
552
|
-
const output = (await sender.calls.take()).events;
|
|
553
|
-
expect(output.length).toEqual(2);
|
|
554
|
-
checkFeatureEvent(output[0], event, false, eventContext);
|
|
555
|
-
checkSummaryEvent(output[1]);
|
|
556
|
-
|
|
557
|
-
// Verify the summary event doesn't have a context field when there's no hasherFactory
|
|
558
|
-
const summaryEvent = output[1];
|
|
559
|
-
expect(summaryEvent.context).toBeUndefined();
|
|
560
|
-
expect(summaryEvent.features).toEqual({
|
|
561
|
-
flagkey: {
|
|
562
|
-
contextKinds: ['user'],
|
|
563
|
-
default: 'default',
|
|
564
|
-
counters: [{ version: 11, variation: 1, value: 'value', count: 1 }],
|
|
565
|
-
},
|
|
566
|
-
});
|
|
567
|
-
} finally {
|
|
568
|
-
ep.stop();
|
|
569
|
-
}
|
|
570
|
-
});
|
|
571
|
-
|
|
572
|
-
it('uses MultiEventSummarizer when platform has hasherFactory', async () => {
|
|
573
|
-
const event = {
|
|
574
|
-
kind: 'feature',
|
|
575
|
-
creationDate: 1000,
|
|
576
|
-
context: eventContext,
|
|
577
|
-
key: 'flagkey',
|
|
578
|
-
version: 11,
|
|
579
|
-
variation: 1,
|
|
580
|
-
value: 'value',
|
|
581
|
-
default: 'default',
|
|
582
|
-
trackEvents: true,
|
|
583
|
-
};
|
|
584
|
-
|
|
585
|
-
// Stub platform hash a hasher factory.
|
|
586
|
-
|
|
587
|
-
const sender = MockEventSender();
|
|
588
|
-
const ep = EventProcessor(platform, defaultConfig, envId, null, null, sender);
|
|
589
|
-
try {
|
|
590
|
-
ep.enqueue(event);
|
|
591
|
-
await ep.flush();
|
|
592
|
-
|
|
593
|
-
expect(sender.calls.length()).toEqual(1);
|
|
594
|
-
const output = (await sender.calls.take()).events;
|
|
595
|
-
expect(output.length).toEqual(2);
|
|
596
|
-
checkFeatureEvent(output[0], event, false, eventContext);
|
|
597
|
-
checkSummaryEvent(output[1]);
|
|
598
|
-
|
|
599
|
-
// Verify that when MultiEventSummarizer is used, context is included in the summary
|
|
600
|
-
const summaryEvent = output[1];
|
|
601
|
-
expect(summaryEvent.context).toEqual(eventContext);
|
|
602
|
-
expect(summaryEvent.features).toEqual({
|
|
603
|
-
flagkey: {
|
|
604
|
-
contextKinds: ['user'],
|
|
605
|
-
default: 'default',
|
|
606
|
-
counters: [{ version: 11, variation: 1, value: 'value', count: 1 }],
|
|
607
|
-
},
|
|
608
|
-
});
|
|
609
|
-
} finally {
|
|
610
|
-
ep.stop();
|
|
611
|
-
}
|
|
612
|
-
});
|
|
613
|
-
|
|
614
|
-
it('filters context in summary events when using MultiEventSummarizer', async () => {
|
|
596
|
+
it('filters context in summary events', async () => {
|
|
615
597
|
const event = {
|
|
616
598
|
kind: 'feature',
|
|
617
599
|
creationDate: 1000,
|
|
@@ -33,38 +33,6 @@ describe('EventSummarizer', () => {
|
|
|
33
33
|
expect(data.endDate).toEqual(2000);
|
|
34
34
|
});
|
|
35
35
|
|
|
36
|
-
it('returns summaries for feature events', () => {
|
|
37
|
-
// getSummaries returns the single summary from getSummary(), but wrapped in an array.
|
|
38
|
-
const es = EventSummarizer();
|
|
39
|
-
const event1 = { kind: 'feature', creationDate: 2000, key: 'key', user: user };
|
|
40
|
-
const event2 = { kind: 'feature', creationDate: 1000, key: 'key', user: user };
|
|
41
|
-
const event3 = { kind: 'feature', creationDate: 1500, key: 'key', user: user };
|
|
42
|
-
es.summarizeEvent(event1);
|
|
43
|
-
es.summarizeEvent(event2);
|
|
44
|
-
es.summarizeEvent(event3);
|
|
45
|
-
const summaries = es.getSummaries();
|
|
46
|
-
|
|
47
|
-
expect(summaries[0].startDate).toEqual(1000);
|
|
48
|
-
expect(summaries[0].endDate).toEqual(2000);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it('clears summaries when getSummaries is called', () => {
|
|
52
|
-
const es = EventSummarizer();
|
|
53
|
-
const event1 = { kind: 'feature', creationDate: 2000, key: 'key', user: user };
|
|
54
|
-
const event2 = { kind: 'feature', creationDate: 1000, key: 'key', user: user };
|
|
55
|
-
es.summarizeEvent(event1);
|
|
56
|
-
es.summarizeEvent(event2);
|
|
57
|
-
const summaries = es.getSummaries();
|
|
58
|
-
expect(summaries[0].startDate).toEqual(1000);
|
|
59
|
-
expect(summaries[0].endDate).toEqual(2000);
|
|
60
|
-
expect(es.getSummaries()).toEqual([]);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it('returns empty array if no summaries are available', () => {
|
|
64
|
-
const es = EventSummarizer();
|
|
65
|
-
expect(es.getSummaries()).toEqual([]);
|
|
66
|
-
});
|
|
67
|
-
|
|
68
36
|
function makeEvent(key, version, variation, value, defaultVal) {
|
|
69
37
|
return {
|
|
70
38
|
kind: 'feature',
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const { checkContext, getContextKeys, getContextKinds, getCanonicalKey
|
|
1
|
+
const { checkContext, getContextKeys, getContextKinds, getCanonicalKey } = require('../context');
|
|
2
2
|
|
|
3
3
|
describe.each([{ key: 'test' }, { kind: 'user', key: 'test' }, { kind: 'multi', user: { key: 'test' } }])(
|
|
4
4
|
'given a context which contains a single kind',
|
|
@@ -199,332 +199,3 @@ describe('getContextKeys', () => {
|
|
|
199
199
|
expect(keys).toEqual({});
|
|
200
200
|
});
|
|
201
201
|
});
|
|
202
|
-
|
|
203
|
-
function mockHasher() {
|
|
204
|
-
let state = '';
|
|
205
|
-
return {
|
|
206
|
-
update: input => {
|
|
207
|
-
state += input;
|
|
208
|
-
},
|
|
209
|
-
digest: () => state,
|
|
210
|
-
};
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
it('hashes two equal contexts the same', async () => {
|
|
214
|
-
const a = {
|
|
215
|
-
kind: 'multi',
|
|
216
|
-
org: {
|
|
217
|
-
key: 'testKey',
|
|
218
|
-
name: 'testName',
|
|
219
|
-
cat: 'calico',
|
|
220
|
-
dog: 'lab',
|
|
221
|
-
anonymous: true,
|
|
222
|
-
_meta: {
|
|
223
|
-
privateAttributes: ['/a/b/c', 'cat', 'custom/dog'],
|
|
224
|
-
},
|
|
225
|
-
},
|
|
226
|
-
customer: {
|
|
227
|
-
key: 'testKey',
|
|
228
|
-
name: 'testName',
|
|
229
|
-
bird: 'party parrot',
|
|
230
|
-
chicken: 'hen',
|
|
231
|
-
},
|
|
232
|
-
};
|
|
233
|
-
|
|
234
|
-
const b = {
|
|
235
|
-
kind: 'multi',
|
|
236
|
-
org: {
|
|
237
|
-
key: 'testKey',
|
|
238
|
-
name: 'testName',
|
|
239
|
-
cat: 'calico',
|
|
240
|
-
dog: 'lab',
|
|
241
|
-
anonymous: true,
|
|
242
|
-
_meta: {
|
|
243
|
-
privateAttributes: ['/a/b/c', 'cat', 'custom/dog'],
|
|
244
|
-
},
|
|
245
|
-
},
|
|
246
|
-
customer: {
|
|
247
|
-
key: 'testKey',
|
|
248
|
-
name: 'testName',
|
|
249
|
-
bird: 'party parrot',
|
|
250
|
-
chicken: 'hen',
|
|
251
|
-
},
|
|
252
|
-
};
|
|
253
|
-
expect(await hashContext(a, mockHasher())).toEqual(await hashContext(b, mockHasher()));
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
it('handles shared references without getting stuck', async () => {
|
|
257
|
-
const sharedObject = { value: 'shared' };
|
|
258
|
-
const context = {
|
|
259
|
-
kind: 'multi',
|
|
260
|
-
org: {
|
|
261
|
-
key: 'testKey',
|
|
262
|
-
shared: sharedObject,
|
|
263
|
-
},
|
|
264
|
-
user: {
|
|
265
|
-
key: 'testKey',
|
|
266
|
-
shared: sharedObject,
|
|
267
|
-
},
|
|
268
|
-
};
|
|
269
|
-
|
|
270
|
-
const hash = await hashContext(context, mockHasher());
|
|
271
|
-
expect(hash).toBeDefined();
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
it('returns undefined for contexts with cycles', async () => {
|
|
275
|
-
const cyclicObject = { value: 'cyclic' };
|
|
276
|
-
cyclicObject.self = cyclicObject;
|
|
277
|
-
|
|
278
|
-
const context = {
|
|
279
|
-
kind: 'user',
|
|
280
|
-
key: 'testKey',
|
|
281
|
-
cyclic: cyclicObject,
|
|
282
|
-
};
|
|
283
|
-
|
|
284
|
-
expect(await hashContext(context, mockHasher())).toBeUndefined();
|
|
285
|
-
});
|
|
286
|
-
|
|
287
|
-
it('handles nested objects correctly', async () => {
|
|
288
|
-
const context = {
|
|
289
|
-
kind: 'user',
|
|
290
|
-
key: 'testKey',
|
|
291
|
-
nested: {
|
|
292
|
-
level1: {
|
|
293
|
-
level2: {
|
|
294
|
-
value: 'deep',
|
|
295
|
-
},
|
|
296
|
-
},
|
|
297
|
-
},
|
|
298
|
-
};
|
|
299
|
-
|
|
300
|
-
const hash = await hashContext(context, mockHasher());
|
|
301
|
-
expect(hash).toBeDefined();
|
|
302
|
-
});
|
|
303
|
-
|
|
304
|
-
it('handles arrays correctly', async () => {
|
|
305
|
-
const context = {
|
|
306
|
-
kind: 'user',
|
|
307
|
-
key: 'testKey',
|
|
308
|
-
array: [1, 2, 3],
|
|
309
|
-
nestedArray: [
|
|
310
|
-
[1, 2],
|
|
311
|
-
[3, 4],
|
|
312
|
-
],
|
|
313
|
-
};
|
|
314
|
-
|
|
315
|
-
const hash = await hashContext(context, mockHasher());
|
|
316
|
-
expect(hash).toBeDefined();
|
|
317
|
-
});
|
|
318
|
-
|
|
319
|
-
it('handles primitive values correctly', async () => {
|
|
320
|
-
const context = {
|
|
321
|
-
kind: 'user',
|
|
322
|
-
key: 'testKey',
|
|
323
|
-
string: 'test',
|
|
324
|
-
number: 42,
|
|
325
|
-
boolean: true,
|
|
326
|
-
nullValue: null,
|
|
327
|
-
undefinedValue: undefined,
|
|
328
|
-
};
|
|
329
|
-
|
|
330
|
-
const hash = await hashContext(context, mockHasher());
|
|
331
|
-
expect(hash).toBeDefined();
|
|
332
|
-
});
|
|
333
|
-
|
|
334
|
-
it('includes private attributes in hash calculation', async () => {
|
|
335
|
-
const baseContext = {
|
|
336
|
-
kind: 'user',
|
|
337
|
-
key: 'testKey',
|
|
338
|
-
name: 'testName',
|
|
339
|
-
nested: {
|
|
340
|
-
value: 'testValue',
|
|
341
|
-
},
|
|
342
|
-
};
|
|
343
|
-
|
|
344
|
-
const contextWithPrivate = {
|
|
345
|
-
...baseContext,
|
|
346
|
-
_meta: {
|
|
347
|
-
privateAttributes: ['name', 'nested/value'],
|
|
348
|
-
},
|
|
349
|
-
};
|
|
350
|
-
|
|
351
|
-
const hashWithPrivate = await hashContext(contextWithPrivate, mockHasher());
|
|
352
|
-
const hashWithoutPrivate = await hashContext(baseContext, mockHasher());
|
|
353
|
-
|
|
354
|
-
// The hashes should be different because private attributes are included in the hash
|
|
355
|
-
expect(hashWithPrivate).not.toEqual(hashWithoutPrivate);
|
|
356
|
-
});
|
|
357
|
-
|
|
358
|
-
it('uses the keys of attributes in the hash', async () => {
|
|
359
|
-
const a = {
|
|
360
|
-
kind: 'user',
|
|
361
|
-
key: 'testKey',
|
|
362
|
-
a: 'b',
|
|
363
|
-
};
|
|
364
|
-
|
|
365
|
-
const b = {
|
|
366
|
-
kind: 'user',
|
|
367
|
-
key: 'testKey',
|
|
368
|
-
b: 'b',
|
|
369
|
-
};
|
|
370
|
-
|
|
371
|
-
const hashA = await hashContext(a, mockHasher());
|
|
372
|
-
const hashB = await hashContext(b, mockHasher());
|
|
373
|
-
expect(hashA).not.toBe(hashB);
|
|
374
|
-
});
|
|
375
|
-
|
|
376
|
-
it('uses the keys of nested objects inside the hash', async () => {
|
|
377
|
-
const a = {
|
|
378
|
-
kind: 'user',
|
|
379
|
-
key: 'testKey',
|
|
380
|
-
nested: {
|
|
381
|
-
level1: {
|
|
382
|
-
level2: {
|
|
383
|
-
value: 'deep',
|
|
384
|
-
},
|
|
385
|
-
},
|
|
386
|
-
},
|
|
387
|
-
};
|
|
388
|
-
|
|
389
|
-
const b = {
|
|
390
|
-
kind: 'user',
|
|
391
|
-
key: 'testKey',
|
|
392
|
-
nested: {
|
|
393
|
-
sub1: {
|
|
394
|
-
sub2: {
|
|
395
|
-
value: 'deep',
|
|
396
|
-
},
|
|
397
|
-
},
|
|
398
|
-
},
|
|
399
|
-
};
|
|
400
|
-
|
|
401
|
-
const hashA = await hashContext(a, mockHasher());
|
|
402
|
-
const hashB = await hashContext(b, mockHasher());
|
|
403
|
-
expect(hashA).not.toBe(hashB);
|
|
404
|
-
});
|
|
405
|
-
|
|
406
|
-
it('uses the values of nested array in calculations', async () => {
|
|
407
|
-
const a = {
|
|
408
|
-
kind: 'user',
|
|
409
|
-
key: 'testKey',
|
|
410
|
-
array: [1, 2, 3],
|
|
411
|
-
nestedArray: [
|
|
412
|
-
[1, 2],
|
|
413
|
-
[3, 4],
|
|
414
|
-
],
|
|
415
|
-
};
|
|
416
|
-
|
|
417
|
-
const b = {
|
|
418
|
-
kind: 'user',
|
|
419
|
-
key: 'testKey',
|
|
420
|
-
array: [1, 2, 3],
|
|
421
|
-
nestedArray: [
|
|
422
|
-
[2, 1],
|
|
423
|
-
[3, 4],
|
|
424
|
-
],
|
|
425
|
-
};
|
|
426
|
-
|
|
427
|
-
const hashA = await hashContext(a, mockHasher());
|
|
428
|
-
const hashB = await hashContext(b, mockHasher());
|
|
429
|
-
expect(hashA).not.toBe(hashB);
|
|
430
|
-
});
|
|
431
|
-
|
|
432
|
-
it('uses the values of nested objects inside the hash', async () => {
|
|
433
|
-
const a = {
|
|
434
|
-
kind: 'user',
|
|
435
|
-
key: 'testKey',
|
|
436
|
-
nested: {
|
|
437
|
-
level1: {
|
|
438
|
-
level2: {
|
|
439
|
-
value: 'deep',
|
|
440
|
-
},
|
|
441
|
-
},
|
|
442
|
-
},
|
|
443
|
-
};
|
|
444
|
-
|
|
445
|
-
const b = {
|
|
446
|
-
kind: 'user',
|
|
447
|
-
key: 'testKey',
|
|
448
|
-
nested: {
|
|
449
|
-
level1: {
|
|
450
|
-
level2: {
|
|
451
|
-
value: 'deeper',
|
|
452
|
-
},
|
|
453
|
-
},
|
|
454
|
-
},
|
|
455
|
-
};
|
|
456
|
-
|
|
457
|
-
const hashA = await hashContext(a, mockHasher());
|
|
458
|
-
const hashB = await hashContext(b, mockHasher());
|
|
459
|
-
expect(hashA).not.toBe(hashB);
|
|
460
|
-
});
|
|
461
|
-
|
|
462
|
-
it('hashes _meta in attributes', async () => {
|
|
463
|
-
const a = {
|
|
464
|
-
kind: 'user',
|
|
465
|
-
key: 'testKey',
|
|
466
|
-
nested: {
|
|
467
|
-
level1: {
|
|
468
|
-
level2: {
|
|
469
|
-
_meta: { test: 'a' },
|
|
470
|
-
},
|
|
471
|
-
},
|
|
472
|
-
},
|
|
473
|
-
};
|
|
474
|
-
|
|
475
|
-
const b = {
|
|
476
|
-
kind: 'user',
|
|
477
|
-
key: 'testKey',
|
|
478
|
-
nested: {
|
|
479
|
-
level1: {
|
|
480
|
-
level2: {
|
|
481
|
-
_meta: { test: 'b' },
|
|
482
|
-
},
|
|
483
|
-
},
|
|
484
|
-
},
|
|
485
|
-
};
|
|
486
|
-
|
|
487
|
-
const hashA = await hashContext(a, mockHasher());
|
|
488
|
-
const hashB = await hashContext(b, mockHasher());
|
|
489
|
-
expect(hashA).not.toBe(hashB);
|
|
490
|
-
});
|
|
491
|
-
|
|
492
|
-
it('produces the same value for the given context', async () => {
|
|
493
|
-
// This isn't so much a test as it is a detection of change.
|
|
494
|
-
// If this test failed, and you didn't expect it, then you probably need to make sure your
|
|
495
|
-
// change makes sense.
|
|
496
|
-
const complexContext = {
|
|
497
|
-
kind: 'multi',
|
|
498
|
-
org: {
|
|
499
|
-
key: 'testKey',
|
|
500
|
-
name: 'testName',
|
|
501
|
-
cat: 'calico',
|
|
502
|
-
dog: 'lab',
|
|
503
|
-
anonymous: true,
|
|
504
|
-
nestedArray: [
|
|
505
|
-
[1, 2],
|
|
506
|
-
[3, 4],
|
|
507
|
-
],
|
|
508
|
-
_meta: {
|
|
509
|
-
privateAttributes: ['/a/b/c', 'cat', 'custom/dog'],
|
|
510
|
-
},
|
|
511
|
-
},
|
|
512
|
-
customer: {
|
|
513
|
-
key: 'testKey',
|
|
514
|
-
name: 'testName',
|
|
515
|
-
bird: 'party parrot',
|
|
516
|
-
chicken: 'hen',
|
|
517
|
-
nested: {
|
|
518
|
-
level1: {
|
|
519
|
-
level2: {
|
|
520
|
-
value: 'deep',
|
|
521
|
-
_meta: { thisShouldBeInTheHash: true },
|
|
522
|
-
},
|
|
523
|
-
},
|
|
524
|
-
},
|
|
525
|
-
},
|
|
526
|
-
};
|
|
527
|
-
expect(await hashContext(complexContext, mockHasher())).toBe(
|
|
528
|
-
'{"customer":{"bird":"party parrot","chicken":"hen","key":"testKey","name":"testName","nested":{"level1":{"level2":{"_meta":{"thisShouldBeInTheHash":true},"value":"deep"}}}},"kind":"multi","org":{"_meta":{"privateAttributes":["/a/b/c","cat","custom/dog"]},"anonymous":true,"cat":"calico","dog":"lab","key":"testKey","name":"testName","nestedArray":[[1,2],[3,4]]}}'
|
|
529
|
-
);
|
|
530
|
-
});
|
|
@@ -45,15 +45,6 @@ export function defaults() {
|
|
|
45
45
|
diagnosticPlatformData: { name: 'stub-platform' },
|
|
46
46
|
getCurrentUrl: () => currentUrl,
|
|
47
47
|
isDoNotTrack: () => doNotTrack,
|
|
48
|
-
hasherFactory: (/*algorithm*/) => {
|
|
49
|
-
let content = '';
|
|
50
|
-
return {
|
|
51
|
-
update: value => {
|
|
52
|
-
content += value;
|
|
53
|
-
},
|
|
54
|
-
digest: (/*format*/) => content,
|
|
55
|
-
};
|
|
56
|
-
},
|
|
57
48
|
eventSourceFactory: (url, options) => {
|
|
58
49
|
const es = new EventSource(url);
|
|
59
50
|
es.options = options;
|
package/src/context.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
const { commonBasicLogger } = require('./loggers');
|
|
2
|
-
const canonicalize = require('./canonicalize');
|
|
3
2
|
|
|
4
3
|
/**
|
|
5
4
|
* Validate a context kind.
|
|
@@ -127,45 +126,9 @@ function getContextKeys(context, logger = commonBasicLogger()) {
|
|
|
127
126
|
return keys;
|
|
128
127
|
}
|
|
129
128
|
|
|
130
|
-
/**
|
|
131
|
-
* Hash the given context using the provided hasher.
|
|
132
|
-
* This implementation can produce different hashes for equivalent contexts.
|
|
133
|
-
*
|
|
134
|
-
* For example:
|
|
135
|
-
* A legacy user and a single-kind context of user kind that are equivalent, will hash differently.
|
|
136
|
-
* A multi-context with one kind, and the single context with that kind are equivalent, but will hash differently.
|
|
137
|
-
* Two equivalent contexts, with private attributes that are defined in different orders, will hash differently.
|
|
138
|
-
*
|
|
139
|
-
* @param {Object} context
|
|
140
|
-
* @param {{update: (value: string) => void, digest: (format: string) => Promise<string>}} hasher
|
|
141
|
-
* @returns {Promise<string | undefined>} The hash of the context, or undefined if the context is invalid.
|
|
142
|
-
*/
|
|
143
|
-
function hashContext(context, hasher) {
|
|
144
|
-
// In js-core we have legacy and non-legacy contexts hash the same. This implementation does not support that.
|
|
145
|
-
// Because this implementation directly uses the user-provided context and doesn't manipulate it.
|
|
146
|
-
// The js-core implementation is more conceptually correct, but it isn't a practical requirement.
|
|
147
|
-
|
|
148
|
-
// This implementation additionally doesn't produce the same hash for an equivalent multi-context with one kind, and
|
|
149
|
-
// the single context with that kind.
|
|
150
|
-
if (!checkContext(context)) {
|
|
151
|
-
return undefined;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
try {
|
|
155
|
-
const canonicalized = canonicalize(context);
|
|
156
|
-
|
|
157
|
-
hasher.update(canonicalized);
|
|
158
|
-
|
|
159
|
-
return hasher.digest('hex');
|
|
160
|
-
} catch {
|
|
161
|
-
return undefined;
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
129
|
module.exports = {
|
|
166
130
|
checkContext,
|
|
167
131
|
getContextKeys,
|
|
168
132
|
getContextKinds,
|
|
169
133
|
getCanonicalKey,
|
|
170
|
-
hashContext,
|
|
171
134
|
};
|