Skip to content

Commit 891e681

Browse files
committed
Merge branch 'rlamb/EMSR-326/per-context-summary-events' into rlamb/5.7.0-prerelease
2 parents 2a780d4 + 5ab516f commit 891e681

File tree

8 files changed

+120
-135
lines changed

8 files changed

+120
-135
lines changed

src/EventProcessor.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ function EventProcessor(
124124
const eventsToSend = queue;
125125
const summaries = await summarizer.getSummaries();
126126

127-
summaries.forEach((summary) => {
127+
summaries.forEach(summary => {
128128
if (Object.keys(summary.features).length) {
129129
eventsToSend.push(summary);
130130
}

src/MultiEventSummarizer.js

Lines changed: 63 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,76 +1,76 @@
11
import { hashContext } from './context';
22
import EventSummarizer from './EventSummarizer';
33
/**
4-
*
5-
* @param {{filter: (context: any) => any}} contextFilter
6-
* @param {() => {update: (value: string) => void, digest: (format: string) => Promise<string>}} hasherFactory
4+
*
5+
* @param {{filter: (context: any) => any}} contextFilter
6+
* @param {() => {update: (value: string) => void, digest: (format: string) => Promise<string>}} hasherFactory
77
*/
88
function MultiEventSummarizer(contextFilter, hasherFactory) {
9-
let summarizers = {};
10-
let contexts = {};
11-
const pendingPromises = [];
9+
let summarizers = {};
10+
let contexts = {};
11+
const pendingPromises = [];
1212

13-
/**
14-
* Summarize the given event.
15-
* @param {{
16-
* kind: string,
17-
* context?: any,
18-
* }} event
19-
*/
20-
function summarizeEvent(event) {
21-
// This will execute asynchronously, which means that a flush could happen before the event
22-
// is summarized. When that happens, then the event will just be in the next batch of summaries.
23-
const promise = (async () => {
24-
if(event.kind === 'feature') {
25-
const hash = await hashContext(event.context, hasherFactory());
26-
if(!hash) {
27-
return;
28-
}
13+
/**
14+
* Summarize the given event.
15+
* @param {{
16+
* kind: string,
17+
* context?: any,
18+
* }} event
19+
*/
20+
function summarizeEvent(event) {
21+
// This will execute asynchronously, which means that a flush could happen before the event
22+
// is summarized. When that happens, then the event will just be in the next batch of summaries.
23+
const promise = (async () => {
24+
if (event.kind === 'feature') {
25+
const hash = await hashContext(event.context, hasherFactory());
26+
if (!hash) {
27+
return;
28+
}
2929

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);
38-
}
39-
})();
40-
pendingPromises.push(promise);
41-
promise.finally(() => {
42-
const index = pendingPromises.indexOf(promise);
43-
if(index !== -1) {
44-
pendingPromises.splice(index, 1);
45-
}
46-
});
47-
}
30+
let summarizer = summarizers[hash];
31+
if (!summarizer) {
32+
summarizers[hash] = EventSummarizer();
33+
summarizer = summarizers[hash];
34+
contexts[hash] = event.context;
35+
}
4836

49-
/**
50-
* Get the summaries of the events that have been summarized.
51-
* @returns {any[]}
52-
*/
53-
async function getSummaries() {
54-
// Wait for any pending summarizations to complete
55-
// Additional tasks queued while waiting will not be waited for.
56-
await Promise.all([...pendingPromises]);
37+
summarizer.summarizeEvent(event);
38+
}
39+
})();
40+
pendingPromises.push(promise);
41+
promise.finally(() => {
42+
const index = pendingPromises.indexOf(promise);
43+
if (index !== -1) {
44+
pendingPromises.splice(index, 1);
45+
}
46+
});
47+
}
5748

58-
const summarizersToFlush = summarizers;
59-
const contextsForSummaries = contexts;
49+
/**
50+
* Get the summaries of the events that have been summarized.
51+
* @returns {any[]}
52+
*/
53+
async function getSummaries() {
54+
// Wait for any pending summarizations to complete
55+
// Additional tasks queued while waiting will not be waited for.
56+
await Promise.all([...pendingPromises]);
6057

61-
summarizers = {};
62-
contexts = {};
63-
return Object.entries(summarizersToFlush).map(([hash, summarizer]) => {
64-
const summary = summarizer.getSummary();
65-
summary.context = contextFilter.filter(contextsForSummaries[hash]);
66-
return summary;
67-
});
68-
}
58+
const summarizersToFlush = summarizers;
59+
const contextsForSummaries = contexts;
6960

70-
return {
71-
summarizeEvent,
72-
getSummaries
73-
};
61+
summarizers = {};
62+
contexts = {};
63+
return Object.entries(summarizersToFlush).map(([hash, summarizer]) => {
64+
const summary = summarizer.getSummary();
65+
summary.context = contextFilter.filter(contextsForSummaries[hash]);
66+
return summary;
67+
});
68+
}
69+
70+
return {
71+
summarizeEvent,
72+
getSummaries,
73+
};
7474
}
7575

76-
module.exports = MultiEventSummarizer;
76+
module.exports = MultiEventSummarizer;

src/__tests__/MultiEventSummarizer-test.js

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,12 @@ const MultiEventSummarizer = require('../MultiEventSummarizer');
22
const ContextFilter = require('../ContextFilter');
33

44
function mockHasher() {
5-
let state = "";
5+
let state = '';
66
return {
7-
update: (input) => {
7+
update: input => {
88
state += input;
99
},
10-
digest: () => {
11-
return state;
12-
},
10+
digest: () => state,
1311
};
1412
}
1513

@@ -35,7 +33,7 @@ describe('with mocked crypto and hasher', () => {
3533
summarizer = MultiEventSummarizer(contextFilter, mockHasher);
3634
});
3735

38-
test('creates new summarizer for new context hash', async () => {
36+
it('creates new summarizer for new context hash', async () => {
3937
const context = { kind: 'user', key: 'user1' };
4038
const event = { kind: 'feature', context };
4139

@@ -45,7 +43,7 @@ describe('with mocked crypto and hasher', () => {
4543
expect(summaries).toHaveLength(1);
4644
});
4745

48-
test('uses existing summarizer for same context hash', async () => {
46+
it('uses existing summarizer for same context hash', async () => {
4947
const context = { kind: 'user', key: 'user1' };
5048
const event1 = { kind: 'feature', context, value: 'value1' };
5149
const event2 = { kind: 'feature', context, value: 'value2' };
@@ -57,7 +55,7 @@ describe('with mocked crypto and hasher', () => {
5755
expect(summaries).toHaveLength(1);
5856
});
5957

60-
test('ignores non-feature events', async () => {
58+
it('ignores non-feature events', async () => {
6159
const context = { kind: 'user', key: 'user1' };
6260
const event = { kind: 'identify', context };
6361

@@ -67,7 +65,7 @@ describe('with mocked crypto and hasher', () => {
6765
expect(summaries).toHaveLength(0);
6866
});
6967

70-
test('handles multiple different contexts', async () => {
68+
it('handles multiple different contexts', async () => {
7169
const context1 = { kind: 'user', key: 'user1' };
7270
const context2 = { kind: 'user', key: 'user2' };
7371
const event1 = { kind: 'feature', context: context1 };
@@ -80,7 +78,7 @@ describe('with mocked crypto and hasher', () => {
8078
expect(summaries).toHaveLength(2);
8179
});
8280

83-
test('automatically clears summaries when summarized', async () => {
81+
it('automatically clears summaries when summarized', async () => {
8482
const context = { kind: 'user', key: 'user1' };
8583
const event = { kind: 'feature', context };
8684

@@ -92,7 +90,7 @@ describe('with mocked crypto and hasher', () => {
9290
expect(summariesB).toHaveLength(0);
9391
});
9492

95-
test('increments counters for feature events across multiple contexts', async () => {
93+
it('increments counters for feature events across multiple contexts', async () => {
9694
const context1 = { kind: 'user', key: 'user1' };
9795
const context2 = { kind: 'user', key: 'user2' };
9896

@@ -157,4 +155,4 @@ describe('with mocked crypto and hasher', () => {
157155
},
158156
});
159157
});
160-
});
158+
});

src/__tests__/canonicalize-test.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ const testInputDir = path.join(__dirname, 'testdata', 'input');
88
const testOutputDir = path.join(__dirname, 'testdata', 'output');
99
const testFiles = fs.readdirSync(testInputDir);
1010

11-
it.each(testFiles)('should correctly canonicalize %s', (filename) => {
11+
it.each(testFiles)('should correctly canonicalize %s', filename => {
1212
// Load the input and expected output files
1313
const inputPath = path.join(testInputDir, filename);
1414
const outputPath = path.join(testOutputDir, filename);
@@ -83,4 +83,4 @@ it('should throw an error for objects with cycles', () => {
8383
a.b = b;
8484

8585
expect(() => canonicalize(a)).toThrow('Cycle detected');
86-
});
86+
});

src/__tests__/context-test.js

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -201,14 +201,12 @@ describe('getContextKeys', () => {
201201
});
202202

203203
function mockHasher() {
204-
let state = "";
204+
let state = '';
205205
return {
206-
update: (input) => {
206+
update: input => {
207207
state += input;
208208
},
209-
digest: () => {
210-
return state;
211-
},
209+
digest: () => state,
212210
};
213211
}
214212

@@ -529,4 +527,4 @@ it('produces the same value for the given context', async () => {
529527
expect(await hashContext(complexContext, mockHasher())).toBe(
530528
'{"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]]}}'
531529
);
532-
});
530+
});

src/__tests__/stubPlatform.js

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,15 +45,13 @@ export function defaults() {
4545
diagnosticPlatformData: { name: 'stub-platform' },
4646
getCurrentUrl: () => currentUrl,
4747
isDoNotTrack: () => doNotTrack,
48-
hasherFactory: (algorithm) => {
48+
hasherFactory: (/*algorithm*/) => {
4949
let content = '';
5050
return {
51-
update: (value) => {
51+
update: value => {
5252
content += value;
5353
},
54-
digest: (format) => {
55-
return content;
56-
},
54+
digest: (/*format*/) => content,
5755
};
5856
},
5957
eventSourceFactory: (url, options) => {

src/canonicalize.js

Lines changed: 28 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -9,33 +9,33 @@
99
* @returns {string} The canonicalized JSON string.
1010
*/
1111
function canonicalize(object, visited = []) {
12-
// For JavaScript the default JSON serialization will produce canonicalized output for basic types.
13-
if (object === null || typeof object !== 'object') {
14-
return JSON.stringify(object);
15-
}
16-
17-
if (visited.includes(object)) {
18-
throw new Error('Cycle detected');
19-
}
20-
21-
if (Array.isArray(object)) {
22-
const values = object
23-
.map((item) => canonicalize(item, [...visited, object]))
24-
.map((item) => (item === undefined ? 'null' : item));
25-
return `[${values.join(',')}]`;
26-
}
27-
28-
const values = Object.keys(object)
29-
.sort()
30-
.map((key) => {
31-
const value = canonicalize(object[key], [...visited, object]);
32-
if (value !== undefined) {
33-
return `${JSON.stringify(key)}:${value}`;
34-
}
35-
return undefined;
36-
})
37-
.filter((item) => item !== undefined);
38-
return `{${values.join(',')}}`;
12+
// For JavaScript the default JSON serialization will produce canonicalized output for basic types.
13+
if (object === null || typeof object !== 'object') {
14+
return JSON.stringify(object);
3915
}
4016

41-
module.exports = canonicalize;
17+
if (visited.includes(object)) {
18+
throw new Error('Cycle detected');
19+
}
20+
21+
if (Array.isArray(object)) {
22+
const values = object
23+
.map(item => canonicalize(item, [...visited, object]))
24+
.map(item => (item === undefined ? 'null' : item));
25+
return `[${values.join(',')}]`;
26+
}
27+
28+
const values = Object.keys(object)
29+
.sort()
30+
.map(key => {
31+
const value = canonicalize(object[key], [...visited, object]);
32+
if (value !== undefined) {
33+
return `${JSON.stringify(key)}:${value}`;
34+
}
35+
return undefined;
36+
})
37+
.filter(item => item !== undefined);
38+
return `{${values.join(',')}}`;
39+
}
40+
41+
module.exports = canonicalize;

0 commit comments

Comments
 (0)