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