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.
- package/.circleci/config.yml +22 -0
- package/.eslintignore +4 -0
- package/.eslintrc.yaml +104 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +37 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
- package/.github/pull_request_template.md +21 -0
- package/.ldrelease/config.yml +24 -0
- package/.prettierignore +1 -0
- package/.prettierrc +5 -0
- package/CHANGELOG.md +4 -0
- package/CONTRIBUTING.md +45 -0
- package/babel.config.js +18 -0
- package/docs/typedoc.js +11 -0
- package/jest.config.js +15 -0
- package/package.json +3 -29
- package/scripts/better-audit.sh +76 -0
- package/src/EventEmitter.js +60 -0
- package/src/EventProcessor.js +175 -0
- package/src/EventSender.js +87 -0
- package/src/EventSummarizer.js +84 -0
- package/src/Identity.js +26 -0
- package/src/InitializationState.js +83 -0
- package/src/PersistentFlagStore.js +50 -0
- package/src/PersistentStorage.js +81 -0
- package/src/Requestor.js +111 -0
- package/src/Stream.js +154 -0
- package/src/UserFilter.js +75 -0
- package/src/UserValidator.js +56 -0
- package/src/__tests__/.eslintrc.yaml +7 -0
- package/src/__tests__/EventProcessor-test.js +559 -0
- package/src/__tests__/EventSender-test.js +252 -0
- package/src/__tests__/EventSource-mock.js +61 -0
- package/src/__tests__/EventSummarizer-test.js +103 -0
- package/src/__tests__/LDClient-events-test.js +757 -0
- package/src/__tests__/LDClient-localstorage-test.js +179 -0
- package/src/__tests__/LDClient-streaming-test.js +683 -0
- package/src/__tests__/LDClient-test.js +761 -0
- package/src/__tests__/PersistentFlagStore-test.js +111 -0
- package/src/__tests__/Requestor-test.js +362 -0
- package/src/__tests__/Stream-test.js +299 -0
- package/src/__tests__/UserFilter-test.js +93 -0
- package/src/__tests__/UserValidator-test.js +57 -0
- package/src/__tests__/configuration-test.js +217 -0
- package/src/__tests__/diagnosticEvents-test.js +449 -0
- package/src/__tests__/loggers-test.js +149 -0
- package/src/__tests__/mockHttp.js +122 -0
- package/src/__tests__/promiseCoalescer-test.js +128 -0
- package/src/__tests__/stubPlatform.js +148 -0
- package/src/__tests__/testUtils.js +77 -0
- package/src/__tests__/utils-test.js +148 -0
- package/src/configuration.js +151 -0
- package/src/diagnosticEvents.js +269 -0
- package/src/errors.js +37 -0
- package/src/index.js +772 -0
- package/src/jest.setup.js +1 -0
- package/src/loggers.js +93 -0
- package/src/messages.js +217 -0
- package/src/promiseCoalescer.js +52 -0
- package/src/utils.js +214 -0
- package/test-types.ts +96 -0
- package/tsconfig.json +13 -0
- package/dist/ldclient-common.cjs.js +0 -2
- package/dist/ldclient-common.cjs.js.map +0 -1
- package/dist/ldclient-common.es.js +0 -2
- package/dist/ldclient-common.es.js.map +0 -1
- package/dist/ldclient-common.min.js +0 -2
- package/dist/ldclient-common.min.js.map +0 -1
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import EventSender from '../EventSender';
|
|
2
|
+
import * as utils from '../utils';
|
|
3
|
+
|
|
4
|
+
import * as base64 from 'base64-js';
|
|
5
|
+
|
|
6
|
+
import { respond, networkError } from './mockHttp';
|
|
7
|
+
import * as stubPlatform from './stubPlatform';
|
|
8
|
+
|
|
9
|
+
// These tests verify that EventSender executes the expected HTTP requests to deliver events. Since
|
|
10
|
+
// the js-sdk-common package uses an abstraction of HTTP requests, these tests do not use HTTP but
|
|
11
|
+
// rather use a test implementation of our HTTP abstraction; the individual platform-specific SDKs
|
|
12
|
+
// are responsible for verifying that their own implementations of the same HTTP abstraction work
|
|
13
|
+
// correctly with real networking.
|
|
14
|
+
|
|
15
|
+
describe('EventSender', () => {
|
|
16
|
+
let platform;
|
|
17
|
+
const envId = 'env';
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
platform = stubPlatform.defaults();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
function fakeImageCreator() {
|
|
24
|
+
const ret = function(url) {
|
|
25
|
+
ret.urls.push(url);
|
|
26
|
+
};
|
|
27
|
+
ret.urls = [];
|
|
28
|
+
return ret;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function base64URLDecode(str) {
|
|
32
|
+
let s = str;
|
|
33
|
+
while (s.length % 4 !== 0) {
|
|
34
|
+
s = s + '=';
|
|
35
|
+
}
|
|
36
|
+
s = s.replace(/_/g, '/').replace(/-/g, '+');
|
|
37
|
+
const decodedBytes = base64.toByteArray(s);
|
|
38
|
+
const decodedStr = String.fromCharCode.apply(String, decodedBytes);
|
|
39
|
+
return decodeURIComponent(escape(decodedStr));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function decodeOutputFromUrl(url, baseUrl) {
|
|
43
|
+
const prefix = baseUrl + '/a/' + envId + '.gif?d=';
|
|
44
|
+
if (!url.startsWith(prefix)) {
|
|
45
|
+
throw 'URL "' + url + '" did not have expected prefix "' + prefix + '"';
|
|
46
|
+
}
|
|
47
|
+
return JSON.parse(base64URLDecode(url.substring(prefix.length)));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
describe('using image endpoint when CORS is not available', () => {
|
|
51
|
+
it('should encode events in a single chunk if they fit', async () => {
|
|
52
|
+
const server = platform.testing.http.newServer();
|
|
53
|
+
const imageCreator = fakeImageCreator();
|
|
54
|
+
const platformWithoutCors = { ...platform, httpAllowsPost: () => false, httpFallbackPing: imageCreator };
|
|
55
|
+
const sender = EventSender(platformWithoutCors, envId);
|
|
56
|
+
const event1 = { kind: 'identify', key: 'userKey1' };
|
|
57
|
+
const event2 = { kind: 'identify', key: 'userKey2' };
|
|
58
|
+
const events = [event1, event2];
|
|
59
|
+
|
|
60
|
+
await sender.sendEvents(events, server.url);
|
|
61
|
+
|
|
62
|
+
const urls = imageCreator.urls;
|
|
63
|
+
expect(urls.length).toEqual(1);
|
|
64
|
+
expect(decodeOutputFromUrl(urls[0], server.url)).toEqual(events);
|
|
65
|
+
|
|
66
|
+
expect(server.requests.length()).toEqual(0);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should send events in multiple chunks if necessary', async () => {
|
|
70
|
+
const server = platform.testing.http.newServer();
|
|
71
|
+
const imageCreator = fakeImageCreator();
|
|
72
|
+
const platformWithoutCors = { ...platform, httpAllowsPost: () => false, httpFallbackPing: imageCreator };
|
|
73
|
+
const sender = EventSender(platformWithoutCors, envId);
|
|
74
|
+
const events = [];
|
|
75
|
+
for (let i = 0; i < 80; i++) {
|
|
76
|
+
events.push({ kind: 'identify', key: 'thisIsALongUserKey' + i });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
await sender.sendEvents(events, server.url);
|
|
80
|
+
|
|
81
|
+
const urls = imageCreator.urls;
|
|
82
|
+
expect(urls.length).toEqual(3);
|
|
83
|
+
expect(decodeOutputFromUrl(urls[0], server.url)).toEqual(events.slice(0, 31));
|
|
84
|
+
expect(decodeOutputFromUrl(urls[1], server.url)).toEqual(events.slice(31, 61));
|
|
85
|
+
expect(decodeOutputFromUrl(urls[2], server.url)).toEqual(events.slice(61, 80));
|
|
86
|
+
|
|
87
|
+
expect(server.requests.length()).toEqual(0);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe('using POST when CORS is available', () => {
|
|
92
|
+
it('should send all events in request body', async () => {
|
|
93
|
+
const server = platform.testing.http.newServer();
|
|
94
|
+
server.byDefault(respond(202));
|
|
95
|
+
const sender = EventSender(platform, envId);
|
|
96
|
+
const events = [];
|
|
97
|
+
for (let i = 0; i < 80; i++) {
|
|
98
|
+
events.push({ kind: 'identify', key: 'thisIsALongUserKey' + i });
|
|
99
|
+
}
|
|
100
|
+
await sender.sendEvents(events, server.url + '/endpoint');
|
|
101
|
+
|
|
102
|
+
const r = await server.nextRequest();
|
|
103
|
+
expect(r.path).toEqual('/endpoint');
|
|
104
|
+
expect(r.method).toEqual('post');
|
|
105
|
+
expect(JSON.parse(r.body)).toEqual(events);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('should send custom user-agent header', async () => {
|
|
109
|
+
const options = { sendLDHeaders: true };
|
|
110
|
+
const server = platform.testing.http.newServer();
|
|
111
|
+
server.byDefault(respond(202));
|
|
112
|
+
const sender = EventSender(platform, envId, options);
|
|
113
|
+
const event = { kind: 'identify', key: 'userKey' };
|
|
114
|
+
await sender.sendEvents([event], server.url);
|
|
115
|
+
|
|
116
|
+
const r = await server.nextRequest();
|
|
117
|
+
expect(r.headers['user-agent']).toEqual(utils.getLDUserAgentString(platform));
|
|
118
|
+
expect(r.headers['x-launchdarkly-wrapper']).toBeUndefined();
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('should send unique payload IDs', async () => {
|
|
122
|
+
const options = { sendLDHeaders: true };
|
|
123
|
+
const server = platform.testing.http.newServer();
|
|
124
|
+
server.byDefault(respond(202));
|
|
125
|
+
const sender = EventSender(platform, envId, options);
|
|
126
|
+
const event = { kind: 'identify', key: 'userKey' };
|
|
127
|
+
await sender.sendEvents([event], server.url, false);
|
|
128
|
+
await sender.sendEvents([event], server.url, false); // deliberately repeated
|
|
129
|
+
|
|
130
|
+
const r0 = await server.nextRequest();
|
|
131
|
+
const r1 = await server.nextRequest();
|
|
132
|
+
const id0 = r0.headers['x-launchdarkly-payload-id'];
|
|
133
|
+
const id1 = r1.headers['x-launchdarkly-payload-id'];
|
|
134
|
+
expect(id0).toBeTruthy();
|
|
135
|
+
expect(id1).toBeTruthy();
|
|
136
|
+
expect(id0).not.toEqual(id1);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('should send wrapper info if present', async () => {
|
|
140
|
+
const options = { sendLDHeaders: true, wrapperName: 'FakeSDK' };
|
|
141
|
+
const server = platform.testing.http.newServer();
|
|
142
|
+
server.byDefault(respond(202));
|
|
143
|
+
const sender = EventSender(platform, envId, options);
|
|
144
|
+
const event = { kind: 'identify', key: 'userKey' };
|
|
145
|
+
await sender.sendEvents([event], server.url);
|
|
146
|
+
|
|
147
|
+
const r = await server.nextRequest();
|
|
148
|
+
expect(r.headers['user-agent']).toEqual(utils.getLDUserAgentString(platform));
|
|
149
|
+
expect(r.headers['x-launchdarkly-wrapper']).toEqual('FakeSDK');
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('should send transformed headers if requestHeaderTransform function is provided', async () => {
|
|
153
|
+
const headerTransform = input => {
|
|
154
|
+
const output = { ...input };
|
|
155
|
+
output['c'] = '30';
|
|
156
|
+
return output;
|
|
157
|
+
};
|
|
158
|
+
const options = { requestHeaderTransform: headerTransform };
|
|
159
|
+
const server = platform.testing.http.newServer();
|
|
160
|
+
server.byDefault(respond(202));
|
|
161
|
+
const sender = EventSender(platform, envId, options);
|
|
162
|
+
const event = { kind: 'identify', key: 'userKey' };
|
|
163
|
+
await sender.sendEvents([event], server.url);
|
|
164
|
+
|
|
165
|
+
const r = await server.nextRequest();
|
|
166
|
+
expect(r.headers['c']).toEqual('30');
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe('retry on recoverable HTTP error', () => {
|
|
170
|
+
const retryableStatuses = [400, 408, 429, 500, 503];
|
|
171
|
+
for (const i in retryableStatuses) {
|
|
172
|
+
const status = retryableStatuses[i];
|
|
173
|
+
it('status ' + status, async () => {
|
|
174
|
+
const server = platform.testing.http.newServer();
|
|
175
|
+
let n = 0;
|
|
176
|
+
server.byDefault((req, res) => {
|
|
177
|
+
n++;
|
|
178
|
+
respond(n >= 2 ? 200 : status)(req, res);
|
|
179
|
+
});
|
|
180
|
+
const sender = EventSender(platform, envId);
|
|
181
|
+
const event = { kind: 'false', key: 'userKey' };
|
|
182
|
+
await sender.sendEvents([event], server.url, false);
|
|
183
|
+
|
|
184
|
+
expect(server.requests.length()).toEqual(2);
|
|
185
|
+
const r0 = await server.nextRequest();
|
|
186
|
+
const r1 = await server.nextRequest();
|
|
187
|
+
expect(JSON.parse(r0.body)).toEqual([event]);
|
|
188
|
+
expect(JSON.parse(r1.body)).toEqual([event]);
|
|
189
|
+
const id0 = r0.headers['x-launchdarkly-payload-id'];
|
|
190
|
+
expect(id0).toBeTruthy();
|
|
191
|
+
expect(r1.headers['x-launchdarkly-payload-id']).toEqual(id0);
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('should not retry more than once', async () => {
|
|
197
|
+
const server = platform.testing.http.newServer();
|
|
198
|
+
let n = 0;
|
|
199
|
+
server.byDefault((req, res) => {
|
|
200
|
+
n++;
|
|
201
|
+
respond(n >= 3 ? 200 : 503)(req, res);
|
|
202
|
+
});
|
|
203
|
+
const sender = EventSender(platform, envId);
|
|
204
|
+
const event = { kind: 'false', key: 'userKey' };
|
|
205
|
+
await sender.sendEvents([event], server.url);
|
|
206
|
+
|
|
207
|
+
expect(server.requests.length()).toEqual(2);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('should not retry on error 401', async () => {
|
|
211
|
+
const server = platform.testing.http.newServer();
|
|
212
|
+
server.byDefault(respond(401));
|
|
213
|
+
const sender = EventSender(platform, envId);
|
|
214
|
+
const event = { kind: 'false', key: 'userKey' };
|
|
215
|
+
await sender.sendEvents([event], server.url);
|
|
216
|
+
|
|
217
|
+
expect(server.requests.length()).toEqual(1);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('should retry on I/O error', async () => {
|
|
221
|
+
const server = platform.testing.http.newServer();
|
|
222
|
+
let n = 0;
|
|
223
|
+
server.byDefault((req, res) => {
|
|
224
|
+
n++;
|
|
225
|
+
if (n >= 2) {
|
|
226
|
+
respond(200)(req, res);
|
|
227
|
+
} else {
|
|
228
|
+
networkError()(req, res);
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
const sender = EventSender(platform, envId);
|
|
232
|
+
const event = { kind: 'false', key: 'userKey' };
|
|
233
|
+
await sender.sendEvents([event], server.url);
|
|
234
|
+
|
|
235
|
+
expect(server.requests.length()).toEqual(2);
|
|
236
|
+
await server.nextRequest();
|
|
237
|
+
const r1 = await server.nextRequest();
|
|
238
|
+
expect(JSON.parse(r1.body)).toEqual([event]);
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
describe('When HTTP requests are not available at all', () => {
|
|
243
|
+
it('should silently discard events', async () => {
|
|
244
|
+
const server = platform.testing.http.newServer();
|
|
245
|
+
const sender = EventSender(stubPlatform.withoutHttp(), server.url, envId);
|
|
246
|
+
const event = { kind: 'false', key: 'userKey' };
|
|
247
|
+
await sender.sendEvents([event], server.url);
|
|
248
|
+
|
|
249
|
+
expect(server.requests.length()).toEqual(0);
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import EventEmitter from 'events';
|
|
2
|
+
|
|
3
|
+
export default function EventSource(url) {
|
|
4
|
+
this.__emitter = new EventEmitter();
|
|
5
|
+
|
|
6
|
+
this.onerror = undefined;
|
|
7
|
+
this.onopen = undefined;
|
|
8
|
+
this.onmessage = undefined;
|
|
9
|
+
this.readyState = EventSource.CONNECTING;
|
|
10
|
+
|
|
11
|
+
this.addEventListener = addEventListener;
|
|
12
|
+
this.removeEventListener = removeEventListener;
|
|
13
|
+
this.close = close;
|
|
14
|
+
|
|
15
|
+
this.mockUrl = url;
|
|
16
|
+
this.mockEmit = mockEmit;
|
|
17
|
+
this.mockError = mockError;
|
|
18
|
+
this.mockOpen = mockOpen;
|
|
19
|
+
this.mockMessage = mockMessage;
|
|
20
|
+
|
|
21
|
+
function addEventListener(eventName, callback) {
|
|
22
|
+
this.__emitter.on(eventName, callback);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function removeEventListener(eventName, callback) {
|
|
26
|
+
this.__emitter.off(eventName, callback);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function close() {
|
|
30
|
+
this.readyState = EventSource.CLOSED;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function mockEmit(eventName, param) {
|
|
34
|
+
if (this.readyState !== EventSource.CLOSED) {
|
|
35
|
+
this.__emitter.emit(eventName, param);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function mockError(error) {
|
|
40
|
+
if (this.readyState !== EventSource.CLOSED) {
|
|
41
|
+
this.onerror && this.onerror(error);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function mockOpen(error) {
|
|
46
|
+
if (this.readyState === EventSource.CONNECTING) {
|
|
47
|
+
this.readyState = EventSource.OPEN;
|
|
48
|
+
this.onopen && this.onopen(error);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function mockMessage(message) {
|
|
53
|
+
if (this.readyState === EventSource.OPEN) {
|
|
54
|
+
this.onmessage && this.onmessage(message);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
EventSource.CONNECTING = 0;
|
|
60
|
+
EventSource.OPEN = 1;
|
|
61
|
+
EventSource.CLOSED = 2;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import EventSummarizer from '../EventSummarizer';
|
|
2
|
+
|
|
3
|
+
// These tests cover only the logic for counting feature requests in summary data.
|
|
4
|
+
|
|
5
|
+
describe('EventSummarizer', () => {
|
|
6
|
+
const user = { key: 'key1' };
|
|
7
|
+
|
|
8
|
+
it('does nothing for identify event', () => {
|
|
9
|
+
const es = EventSummarizer();
|
|
10
|
+
const snapshot = es.getSummary();
|
|
11
|
+
es.summarizeEvent({ kind: 'identify', creationDate: 1000, user: user });
|
|
12
|
+
expect(es.getSummary()).toEqual(snapshot);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('does nothing for custom event', () => {
|
|
16
|
+
const es = EventSummarizer();
|
|
17
|
+
const snapshot = es.getSummary();
|
|
18
|
+
es.summarizeEvent({ kind: 'custom', creationDate: 1000, key: 'eventkey', user: user });
|
|
19
|
+
expect(es.getSummary()).toEqual(snapshot);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('sets start and end dates for feature events', () => {
|
|
23
|
+
const es = EventSummarizer();
|
|
24
|
+
const event1 = { kind: 'feature', creationDate: 2000, key: 'key', user: user };
|
|
25
|
+
const event2 = { kind: 'feature', creationDate: 1000, key: 'key', user: user };
|
|
26
|
+
const event3 = { kind: 'feature', creationDate: 1500, key: 'key', user: user };
|
|
27
|
+
es.summarizeEvent(event1);
|
|
28
|
+
es.summarizeEvent(event2);
|
|
29
|
+
es.summarizeEvent(event3);
|
|
30
|
+
const data = es.getSummary();
|
|
31
|
+
|
|
32
|
+
expect(data.startDate).toEqual(1000);
|
|
33
|
+
expect(data.endDate).toEqual(2000);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
function makeEvent(key, version, variation, value, defaultVal) {
|
|
37
|
+
return {
|
|
38
|
+
kind: 'feature',
|
|
39
|
+
creationDate: 1000,
|
|
40
|
+
key: key,
|
|
41
|
+
version: version,
|
|
42
|
+
user: user,
|
|
43
|
+
variation: variation,
|
|
44
|
+
value: value,
|
|
45
|
+
default: defaultVal,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
it('increments counters for feature events', () => {
|
|
50
|
+
const es = EventSummarizer();
|
|
51
|
+
const event1 = makeEvent('key1', 11, 1, 100, 111);
|
|
52
|
+
const event2 = makeEvent('key1', 11, 2, 200, 111);
|
|
53
|
+
const event3 = makeEvent('key2', 22, 1, 999, 222);
|
|
54
|
+
const event4 = makeEvent('key1', 11, 1, 100, 111);
|
|
55
|
+
const event5 = makeEvent('badkey', null, null, 333, 333);
|
|
56
|
+
es.summarizeEvent(event1);
|
|
57
|
+
es.summarizeEvent(event2);
|
|
58
|
+
es.summarizeEvent(event3);
|
|
59
|
+
es.summarizeEvent(event4);
|
|
60
|
+
es.summarizeEvent(event5);
|
|
61
|
+
const data = es.getSummary();
|
|
62
|
+
|
|
63
|
+
data.features.key1.counters.sort((a, b) => a.value - b.value);
|
|
64
|
+
const expectedFeatures = {
|
|
65
|
+
key1: {
|
|
66
|
+
default: 111,
|
|
67
|
+
counters: [
|
|
68
|
+
{ value: 100, variation: 1, version: 11, count: 2 },
|
|
69
|
+
{ value: 200, variation: 2, version: 11, count: 1 },
|
|
70
|
+
],
|
|
71
|
+
},
|
|
72
|
+
key2: {
|
|
73
|
+
default: 222,
|
|
74
|
+
counters: [{ value: 999, variation: 1, version: 22, count: 1 }],
|
|
75
|
+
},
|
|
76
|
+
badkey: {
|
|
77
|
+
default: 333,
|
|
78
|
+
counters: [{ value: 333, unknown: true, count: 1 }],
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
expect(data.features).toEqual(expectedFeatures);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('distinguishes between zero and null/undefined in feature variation', () => {
|
|
85
|
+
const es = EventSummarizer();
|
|
86
|
+
const event1 = makeEvent('key1', 11, 0, 100, 111);
|
|
87
|
+
const event2 = makeEvent('key1', 11, null, 111, 111);
|
|
88
|
+
const event3 = makeEvent('key1', 11, undefined, 111, 111);
|
|
89
|
+
es.summarizeEvent(event1);
|
|
90
|
+
es.summarizeEvent(event2);
|
|
91
|
+
es.summarizeEvent(event3);
|
|
92
|
+
const data = es.getSummary();
|
|
93
|
+
|
|
94
|
+
data.features.key1.counters.sort((a, b) => a.value - b.value);
|
|
95
|
+
const expectedFeatures = {
|
|
96
|
+
key1: {
|
|
97
|
+
default: 111,
|
|
98
|
+
counters: [{ variation: 0, value: 100, version: 11, count: 1 }, { value: 111, version: 11, count: 2 }],
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
expect(data.features).toEqual(expectedFeatures);
|
|
102
|
+
});
|
|
103
|
+
});
|