@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
package/event_factory.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
function isExperiment(flag, reason) {
|
|
2
|
+
if (reason) {
|
|
3
|
+
// If the reason says we're in an experiment, we are. Otherwise, apply
|
|
4
|
+
// the legacy rule exclusion logic.
|
|
5
|
+
if (reason.inExperiment) {
|
|
6
|
+
return true;
|
|
7
|
+
}
|
|
8
|
+
switch (reason.kind) {
|
|
9
|
+
case 'RULE_MATCH': {
|
|
10
|
+
const index = reason.ruleIndex;
|
|
11
|
+
if (index !== undefined) {
|
|
12
|
+
const rules = flag.rules || [];
|
|
13
|
+
return index >= 0 && index < rules.length && !!rules[index].trackEvents;
|
|
14
|
+
}
|
|
15
|
+
break;
|
|
16
|
+
}
|
|
17
|
+
case 'FALLTHROUGH':
|
|
18
|
+
return !!flag.trackEventsFallthrough;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function EventFactory(withReasons) {
|
|
25
|
+
const ef = {};
|
|
26
|
+
|
|
27
|
+
ef.newEvalEvent = (flag, context, detail, defaultVal, prereqOfFlag) => {
|
|
28
|
+
const addExperimentData = isExperiment(flag, detail.reason);
|
|
29
|
+
const e = {
|
|
30
|
+
kind: 'feature',
|
|
31
|
+
creationDate: new Date().getTime(),
|
|
32
|
+
key: flag.key,
|
|
33
|
+
context,
|
|
34
|
+
value: detail.value,
|
|
35
|
+
variation: detail.variationIndex,
|
|
36
|
+
default: defaultVal,
|
|
37
|
+
version: flag.version,
|
|
38
|
+
};
|
|
39
|
+
// the following properties are handled separately so we don't waste bandwidth on unused keys
|
|
40
|
+
if (addExperimentData || flag.trackEvents) {
|
|
41
|
+
e.trackEvents = true;
|
|
42
|
+
}
|
|
43
|
+
if (flag.debugEventsUntilDate) {
|
|
44
|
+
e.debugEventsUntilDate = flag.debugEventsUntilDate;
|
|
45
|
+
}
|
|
46
|
+
if (prereqOfFlag) {
|
|
47
|
+
e.prereqOf = prereqOfFlag.key;
|
|
48
|
+
}
|
|
49
|
+
if (addExperimentData || withReasons) {
|
|
50
|
+
e.reason = detail.reason;
|
|
51
|
+
}
|
|
52
|
+
return e;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
ef.newDefaultEvent = (flag, context, detail) => {
|
|
56
|
+
const e = {
|
|
57
|
+
kind: 'feature',
|
|
58
|
+
creationDate: new Date().getTime(),
|
|
59
|
+
key: flag.key,
|
|
60
|
+
context,
|
|
61
|
+
value: detail.value,
|
|
62
|
+
default: detail.value,
|
|
63
|
+
version: flag.version,
|
|
64
|
+
};
|
|
65
|
+
// the following properties are handled separately so we don't waste bandwidth on unused keys
|
|
66
|
+
if (flag.trackEvents) {
|
|
67
|
+
e.trackEvents = true;
|
|
68
|
+
}
|
|
69
|
+
if (flag.debugEventsUntilDate) {
|
|
70
|
+
e.debugEventsUntilDate = flag.debugEventsUntilDate;
|
|
71
|
+
}
|
|
72
|
+
if (withReasons) {
|
|
73
|
+
e.reason = detail.reason;
|
|
74
|
+
}
|
|
75
|
+
return e;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
ef.newUnknownFlagEvent = (key, context, detail) => {
|
|
79
|
+
const e = {
|
|
80
|
+
kind: 'feature',
|
|
81
|
+
creationDate: new Date().getTime(),
|
|
82
|
+
key: key,
|
|
83
|
+
context,
|
|
84
|
+
value: detail.value,
|
|
85
|
+
default: detail.value,
|
|
86
|
+
};
|
|
87
|
+
if (withReasons) {
|
|
88
|
+
e.reason = detail.reason;
|
|
89
|
+
}
|
|
90
|
+
return e;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
ef.newIdentifyEvent = context => ({
|
|
94
|
+
kind: 'identify',
|
|
95
|
+
creationDate: new Date().getTime(),
|
|
96
|
+
context,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
ef.newCustomEvent = (eventName, context, data, metricValue) => {
|
|
100
|
+
const e = {
|
|
101
|
+
kind: 'custom',
|
|
102
|
+
creationDate: new Date().getTime(),
|
|
103
|
+
key: eventName,
|
|
104
|
+
context,
|
|
105
|
+
};
|
|
106
|
+
if (data !== null && data !== undefined) {
|
|
107
|
+
e.data = data;
|
|
108
|
+
}
|
|
109
|
+
if (metricValue !== null && metricValue !== undefined) {
|
|
110
|
+
e.metricValue = metricValue;
|
|
111
|
+
}
|
|
112
|
+
return e;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
return ef;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
module.exports = {
|
|
119
|
+
EventFactory,
|
|
120
|
+
isExperiment,
|
|
121
|
+
};
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
const LRUCache = require('lru-cache');
|
|
2
|
+
const { v4: uuidv4 } = require('uuid');
|
|
3
|
+
|
|
4
|
+
const EventSummarizer = require('./event_summarizer');
|
|
5
|
+
const ContextFilter = require('./context_filter');
|
|
6
|
+
const errors = require('./errors');
|
|
7
|
+
const httpUtils = require('./utils/httpUtils');
|
|
8
|
+
const messages = require('./messages');
|
|
9
|
+
const wrapPromiseCallback = require('./utils/wrapPromiseCallback');
|
|
10
|
+
const { getCanonicalKey } = require('./context');
|
|
11
|
+
|
|
12
|
+
function EventProcessor(sdkKey, config, errorReporter, diagnosticsManager) {
|
|
13
|
+
const ep = {};
|
|
14
|
+
|
|
15
|
+
const contextFilter = ContextFilter(config),
|
|
16
|
+
summarizer = EventSummarizer(),
|
|
17
|
+
contextKeysCache = new LRUCache({ max: config.contextKeysCapacity }),
|
|
18
|
+
mainEventsUri = config.eventsUri + '/bulk',
|
|
19
|
+
diagnosticEventsUri = config.eventsUri + '/diagnostic';
|
|
20
|
+
|
|
21
|
+
let queue = [],
|
|
22
|
+
lastKnownPastTime = 0,
|
|
23
|
+
droppedEvents = 0,
|
|
24
|
+
deduplicatedUsers = 0,
|
|
25
|
+
exceededCapacity = false,
|
|
26
|
+
eventsInLastBatch = 0,
|
|
27
|
+
shutdown = false,
|
|
28
|
+
diagnosticsTimer;
|
|
29
|
+
|
|
30
|
+
function enqueue(event) {
|
|
31
|
+
if (queue.length < config.capacity) {
|
|
32
|
+
queue.push(event);
|
|
33
|
+
exceededCapacity = false;
|
|
34
|
+
} else {
|
|
35
|
+
if (!exceededCapacity) {
|
|
36
|
+
exceededCapacity = true;
|
|
37
|
+
config.logger.warn('Exceeded event queue capacity. Increase capacity to avoid dropping events.');
|
|
38
|
+
}
|
|
39
|
+
droppedEvents++;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function shouldDebugEvent(event) {
|
|
44
|
+
if (event.debugEventsUntilDate) {
|
|
45
|
+
if (event.debugEventsUntilDate > lastKnownPastTime && event.debugEventsUntilDate > new Date().getTime()) {
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function makeOutputEvent(event) {
|
|
53
|
+
switch (event.kind) {
|
|
54
|
+
case 'feature': {
|
|
55
|
+
const debug = !!event.debug;
|
|
56
|
+
const out = {
|
|
57
|
+
kind: debug ? 'debug' : 'feature',
|
|
58
|
+
creationDate: event.creationDate,
|
|
59
|
+
key: event.key,
|
|
60
|
+
value: event.value,
|
|
61
|
+
default: event.default,
|
|
62
|
+
prereqOf: event.prereqOf,
|
|
63
|
+
};
|
|
64
|
+
if (event.variation !== undefined && event.variation !== null) {
|
|
65
|
+
out.variation = event.variation;
|
|
66
|
+
}
|
|
67
|
+
if (event.version !== undefined && event.version !== null) {
|
|
68
|
+
out.version = event.version;
|
|
69
|
+
}
|
|
70
|
+
if (event.reason) {
|
|
71
|
+
out.reason = event.reason;
|
|
72
|
+
}
|
|
73
|
+
if (debug) {
|
|
74
|
+
out.context = processContext(event);
|
|
75
|
+
} else {
|
|
76
|
+
out.contextKeys = getContextKeys(event);
|
|
77
|
+
}
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
case 'identify':
|
|
81
|
+
return {
|
|
82
|
+
kind: 'identify',
|
|
83
|
+
creationDate: event.creationDate,
|
|
84
|
+
context: processContext(event),
|
|
85
|
+
};
|
|
86
|
+
case 'custom': {
|
|
87
|
+
const out = {
|
|
88
|
+
kind: 'custom',
|
|
89
|
+
creationDate: event.creationDate,
|
|
90
|
+
key: event.key,
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
out.contextKeys = getContextKeys(event);
|
|
94
|
+
|
|
95
|
+
if (event.data !== null && event.data !== undefined) {
|
|
96
|
+
out.data = event.data;
|
|
97
|
+
}
|
|
98
|
+
if (event.metricValue !== null && event.metricValue !== undefined) {
|
|
99
|
+
out.metricValue = event.metricValue;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return out;
|
|
103
|
+
}
|
|
104
|
+
default:
|
|
105
|
+
return event;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function processContext(event) {
|
|
110
|
+
return contextFilter.filter(event.context);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function getCacheKey(event) {
|
|
114
|
+
const context = event.context;
|
|
115
|
+
return getCanonicalKey(context);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function getContextKeys(event) {
|
|
119
|
+
const keys = {};
|
|
120
|
+
const context = event.context;
|
|
121
|
+
if (context !== undefined) {
|
|
122
|
+
if (context.kind === undefined && context.key) {
|
|
123
|
+
keys.user = String(context.key);
|
|
124
|
+
} else if (context.kind !== 'multi') {
|
|
125
|
+
keys[context.kind] = String(context.key);
|
|
126
|
+
} else if (context.kind === 'multi') {
|
|
127
|
+
Object.keys(context)
|
|
128
|
+
.filter(key => key !== 'kind')
|
|
129
|
+
.forEach(key => {
|
|
130
|
+
if (context[key] !== undefined && context[key].key !== undefined) {
|
|
131
|
+
keys[key] = context[key].key;
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
return keys;
|
|
136
|
+
}
|
|
137
|
+
return undefined;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
ep.sendEvent = event => {
|
|
141
|
+
let addIndexEvent = false,
|
|
142
|
+
addFullEvent = false,
|
|
143
|
+
addDebugEvent = false;
|
|
144
|
+
|
|
145
|
+
if (shutdown) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Always record the event in the summarizer.
|
|
150
|
+
summarizer.summarizeEvent(event);
|
|
151
|
+
|
|
152
|
+
// Decide whether to add the event to the payload. Feature events may be added twice, once for
|
|
153
|
+
// the event (if tracked) and once for debugging.
|
|
154
|
+
if (event.kind === 'feature') {
|
|
155
|
+
addFullEvent = event.trackEvents;
|
|
156
|
+
addDebugEvent = shouldDebugEvent(event);
|
|
157
|
+
} else {
|
|
158
|
+
addFullEvent = true;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// For each user we haven't seen before, we add an index event - unless this is already
|
|
162
|
+
// an identify event for that user.
|
|
163
|
+
if (event.context) {
|
|
164
|
+
const isIdentify = event.kind === 'identify';
|
|
165
|
+
if (contextKeysCache.get(getCacheKey(event))) {
|
|
166
|
+
if (!isIdentify) {
|
|
167
|
+
deduplicatedUsers++;
|
|
168
|
+
}
|
|
169
|
+
} else {
|
|
170
|
+
contextKeysCache.set(getCacheKey(event), true);
|
|
171
|
+
if (!isIdentify) {
|
|
172
|
+
addIndexEvent = true;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (addIndexEvent) {
|
|
178
|
+
enqueue({
|
|
179
|
+
kind: 'index',
|
|
180
|
+
creationDate: event.creationDate,
|
|
181
|
+
context: processContext(event),
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
if (addFullEvent) {
|
|
185
|
+
enqueue(makeOutputEvent(event));
|
|
186
|
+
}
|
|
187
|
+
if (addDebugEvent) {
|
|
188
|
+
const debugEvent = Object.assign({}, event, { debug: true });
|
|
189
|
+
enqueue(makeOutputEvent(debugEvent));
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
ep.flush = function (callback) {
|
|
194
|
+
return wrapPromiseCallback(
|
|
195
|
+
new Promise((resolve, reject) => {
|
|
196
|
+
if (shutdown) {
|
|
197
|
+
const err = new errors.LDInvalidSDKKeyError('Events cannot be posted because SDK key is invalid');
|
|
198
|
+
reject(err);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const worklist = queue;
|
|
203
|
+
queue = [];
|
|
204
|
+
const summary = summarizer.getSummary();
|
|
205
|
+
summarizer.clearSummary();
|
|
206
|
+
if (Object.keys(summary.features).length) {
|
|
207
|
+
summary.kind = 'summary';
|
|
208
|
+
worklist.push(summary);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (!worklist.length) {
|
|
212
|
+
resolve();
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
eventsInLastBatch = worklist.length;
|
|
217
|
+
config.logger.debug('Flushing %d events', worklist.length);
|
|
218
|
+
|
|
219
|
+
tryPostingEvents(worklist, mainEventsUri, uuidv4(), resolve, reject, true);
|
|
220
|
+
}),
|
|
221
|
+
callback
|
|
222
|
+
);
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
function tryPostingEvents(events, uri, payloadId, resolve, reject, canRetry) {
|
|
226
|
+
const retryOrReject = err => {
|
|
227
|
+
if (canRetry) {
|
|
228
|
+
config.logger && config.logger.warn('Will retry posting events after 1 second');
|
|
229
|
+
setTimeout(() => {
|
|
230
|
+
tryPostingEvents(events, uri, payloadId, resolve, reject, false);
|
|
231
|
+
}, 1000);
|
|
232
|
+
} else {
|
|
233
|
+
reject(err);
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const headers = Object.assign({ 'Content-Type': 'application/json' }, httpUtils.getDefaultHeaders(sdkKey, config));
|
|
238
|
+
if (payloadId) {
|
|
239
|
+
headers['X-LaunchDarkly-Payload-ID'] = payloadId;
|
|
240
|
+
headers['X-LaunchDarkly-Event-Schema'] = '4';
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const options = { method: 'POST', headers };
|
|
244
|
+
const body = JSON.stringify(events);
|
|
245
|
+
httpUtils.httpRequest(uri, options, body, config, (err, resp) => {
|
|
246
|
+
if (err) {
|
|
247
|
+
retryOrReject(err);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
if (resp.headers['date']) {
|
|
251
|
+
const date = Date.parse(resp.headers['date']);
|
|
252
|
+
if (date) {
|
|
253
|
+
lastKnownPastTime = date;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
if (resp.statusCode > 204) {
|
|
257
|
+
const err = new errors.LDUnexpectedResponseError(
|
|
258
|
+
messages.httpErrorMessage({ status: resp.statusCode }, 'event posting', 'some events were dropped')
|
|
259
|
+
);
|
|
260
|
+
errorReporter && errorReporter(err);
|
|
261
|
+
if (!errors.isHttpErrorRecoverable(resp.statusCode)) {
|
|
262
|
+
reject(err);
|
|
263
|
+
shutdown = true;
|
|
264
|
+
} else {
|
|
265
|
+
retryOrReject(err);
|
|
266
|
+
}
|
|
267
|
+
} else {
|
|
268
|
+
resolve();
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function postDiagnosticEvent(event) {
|
|
274
|
+
tryPostingEvents(
|
|
275
|
+
event,
|
|
276
|
+
diagnosticEventsUri,
|
|
277
|
+
null,
|
|
278
|
+
() => {},
|
|
279
|
+
() => {},
|
|
280
|
+
true
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const flushTimer = setInterval(() => {
|
|
285
|
+
ep.flush().then(
|
|
286
|
+
() => {},
|
|
287
|
+
() => {}
|
|
288
|
+
);
|
|
289
|
+
}, config.flushInterval * 1000);
|
|
290
|
+
|
|
291
|
+
const flushUsersTimer = setInterval(() => {
|
|
292
|
+
contextKeysCache.reset();
|
|
293
|
+
}, config.contextKeysFlushInterval * 1000);
|
|
294
|
+
|
|
295
|
+
ep.close = () => {
|
|
296
|
+
clearInterval(flushTimer);
|
|
297
|
+
clearInterval(flushUsersTimer);
|
|
298
|
+
diagnosticsTimer && clearInterval(diagnosticsTimer);
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
if (!config.diagnosticOptOut && diagnosticsManager) {
|
|
302
|
+
const initEvent = diagnosticsManager.createInitEvent();
|
|
303
|
+
postDiagnosticEvent(initEvent);
|
|
304
|
+
|
|
305
|
+
diagnosticsTimer = setInterval(() => {
|
|
306
|
+
const statsEvent = diagnosticsManager.createStatsEventAndReset(
|
|
307
|
+
droppedEvents,
|
|
308
|
+
deduplicatedUsers,
|
|
309
|
+
eventsInLastBatch
|
|
310
|
+
);
|
|
311
|
+
droppedEvents = 0;
|
|
312
|
+
deduplicatedUsers = 0;
|
|
313
|
+
postDiagnosticEvent(statsEvent);
|
|
314
|
+
}, config.diagnosticRecordingInterval * 1000);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return ep;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
module.exports = EventProcessor;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
const { getContextKinds } = require('./context');
|
|
2
|
+
|
|
3
|
+
function getKinds(event) {
|
|
4
|
+
if (event.context) {
|
|
5
|
+
return getContextKinds(event.context);
|
|
6
|
+
}
|
|
7
|
+
if (event.contextKeys) {
|
|
8
|
+
return Object.keys(event.contextKeys);
|
|
9
|
+
}
|
|
10
|
+
return [];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function EventSummarizer() {
|
|
14
|
+
const es = {};
|
|
15
|
+
|
|
16
|
+
let startDate = 0,
|
|
17
|
+
endDate = 0,
|
|
18
|
+
counters = {},
|
|
19
|
+
contextKinds = {};
|
|
20
|
+
|
|
21
|
+
es.summarizeEvent = event => {
|
|
22
|
+
if (event.kind === 'feature') {
|
|
23
|
+
const counterKey =
|
|
24
|
+
event.key +
|
|
25
|
+
':' +
|
|
26
|
+
(event.variation !== null && event.variation !== undefined ? event.variation : '') +
|
|
27
|
+
':' +
|
|
28
|
+
(event.version !== null && event.version !== undefined ? event.version : '');
|
|
29
|
+
const counterVal = counters[counterKey];
|
|
30
|
+
let kinds = contextKinds[event.key];
|
|
31
|
+
if (!kinds) {
|
|
32
|
+
kinds = new Set();
|
|
33
|
+
contextKinds[event.key] = kinds;
|
|
34
|
+
}
|
|
35
|
+
getKinds(event).forEach(kind => kinds.add(kind));
|
|
36
|
+
|
|
37
|
+
if (counterVal) {
|
|
38
|
+
counterVal.count = counterVal.count + 1;
|
|
39
|
+
} else {
|
|
40
|
+
counters[counterKey] = {
|
|
41
|
+
count: 1,
|
|
42
|
+
key: event.key,
|
|
43
|
+
version: event.version,
|
|
44
|
+
variation: event.variation,
|
|
45
|
+
value: event.value,
|
|
46
|
+
default: event.default,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
if (startDate === 0 || event.creationDate < startDate) {
|
|
50
|
+
startDate = event.creationDate;
|
|
51
|
+
}
|
|
52
|
+
if (event.creationDate > endDate) {
|
|
53
|
+
endDate = event.creationDate;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
es.getSummary = () => {
|
|
59
|
+
const flagsOut = {};
|
|
60
|
+
for (const c of Object.values(counters)) {
|
|
61
|
+
let flag = flagsOut[c.key];
|
|
62
|
+
if (!flag) {
|
|
63
|
+
flag = {
|
|
64
|
+
default: c.default,
|
|
65
|
+
counters: [],
|
|
66
|
+
contextKinds: [...contextKinds[c.key]],
|
|
67
|
+
};
|
|
68
|
+
flagsOut[c.key] = flag;
|
|
69
|
+
}
|
|
70
|
+
const counterOut = {
|
|
71
|
+
value: c.value,
|
|
72
|
+
count: c.count,
|
|
73
|
+
};
|
|
74
|
+
if (c.variation !== undefined && c.variation !== null) {
|
|
75
|
+
counterOut.variation = c.variation;
|
|
76
|
+
}
|
|
77
|
+
if (c.version !== undefined && c.version !== null) {
|
|
78
|
+
counterOut.version = c.version;
|
|
79
|
+
} else {
|
|
80
|
+
counterOut.unknown = true;
|
|
81
|
+
}
|
|
82
|
+
flag.counters.push(counterOut);
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
startDate: startDate,
|
|
86
|
+
endDate: endDate,
|
|
87
|
+
features: flagsOut,
|
|
88
|
+
};
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
es.clearSummary = () => {
|
|
92
|
+
startDate = 0;
|
|
93
|
+
endDate = 0;
|
|
94
|
+
counters = {};
|
|
95
|
+
contextKinds = {};
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
return es;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
module.exports = EventSummarizer;
|
package/feature_store.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// The default in-memory implementation of a feature store, which holds feature flags and
|
|
2
|
+
// other related data received from LaunchDarkly.
|
|
3
|
+
//
|
|
4
|
+
// Other implementations of the same interface can be used by passing them in the featureStore
|
|
5
|
+
// property of the client configuration (that's why the interface here is async, even though
|
|
6
|
+
// the in-memory store doesn't do anything asynchronous - because other implementations may
|
|
7
|
+
// need to be async). The interface is defined by LDFeatureStore in index.d.ts.
|
|
8
|
+
//
|
|
9
|
+
// Additional implementations should use CachingStoreWrapper if possible.
|
|
10
|
+
|
|
11
|
+
// Note that the contract for feature store methods does *not* require callbacks to be deferred
|
|
12
|
+
// with setImmediate, process.nextTick, etc. It is both allowed and desirable to call them
|
|
13
|
+
// directly whenever possible (i.e. if we don't actually have to do any I/O), since otherwise
|
|
14
|
+
// feature flag retrieval is a major performance bottleneck. These methods are for internal use
|
|
15
|
+
// by the SDK, and the SDK does not make any assumptions about whether a callback executes
|
|
16
|
+
// before or after the next statement.
|
|
17
|
+
|
|
18
|
+
function InMemoryFeatureStore() {
|
|
19
|
+
let allData = {};
|
|
20
|
+
let initCalled = false;
|
|
21
|
+
|
|
22
|
+
const store = {};
|
|
23
|
+
|
|
24
|
+
function callbackResult(cb, result) {
|
|
25
|
+
cb && cb(result);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
store.get = (kind, key, cb) => {
|
|
29
|
+
const items = allData[kind.namespace] || {};
|
|
30
|
+
if (Object.hasOwnProperty.call(items, key)) {
|
|
31
|
+
const item = items[key];
|
|
32
|
+
|
|
33
|
+
if (!item || item.deleted) {
|
|
34
|
+
callbackResult(cb, null);
|
|
35
|
+
} else {
|
|
36
|
+
callbackResult(cb, item);
|
|
37
|
+
}
|
|
38
|
+
} else {
|
|
39
|
+
callbackResult(cb, null);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
store.all = (kind, cb) => {
|
|
44
|
+
const results = {};
|
|
45
|
+
const items = allData[kind.namespace] || {};
|
|
46
|
+
|
|
47
|
+
for (const [key, item] of Object.entries(items)) {
|
|
48
|
+
if (item && !item.deleted) {
|
|
49
|
+
results[key] = item;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
callbackResult(cb, results);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
store.init = (newData, cb) => {
|
|
57
|
+
allData = newData;
|
|
58
|
+
initCalled = true;
|
|
59
|
+
callbackResult(cb);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
store.delete = (kind, key, version, cb) => {
|
|
63
|
+
let items = allData[kind.namespace];
|
|
64
|
+
if (!items) {
|
|
65
|
+
items = {};
|
|
66
|
+
allData[kind] = items;
|
|
67
|
+
}
|
|
68
|
+
const deletedItem = { version: version, deleted: true };
|
|
69
|
+
if (Object.hasOwnProperty.call(items, key)) {
|
|
70
|
+
const old = items[key];
|
|
71
|
+
if (!old || old.version < version) {
|
|
72
|
+
items[key] = deletedItem;
|
|
73
|
+
}
|
|
74
|
+
} else {
|
|
75
|
+
items[key] = deletedItem;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
callbackResult(cb);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
store.upsert = (kind, item, cb) => {
|
|
82
|
+
const key = item.key;
|
|
83
|
+
let items = allData[kind.namespace];
|
|
84
|
+
if (!items) {
|
|
85
|
+
items = {};
|
|
86
|
+
allData[kind.namespace] = items;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (Object.hasOwnProperty.call(items, key)) {
|
|
90
|
+
const old = items[key];
|
|
91
|
+
if (old && old.version < item.version) {
|
|
92
|
+
items[key] = clone(item);
|
|
93
|
+
}
|
|
94
|
+
} else {
|
|
95
|
+
items[key] = clone(item);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
callbackResult(cb);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
store.initialized = cb => {
|
|
102
|
+
callbackResult(cb, initCalled === true);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
store.close = () => {
|
|
106
|
+
// Close on the in-memory store is a no-op
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
store.description = 'memory';
|
|
110
|
+
|
|
111
|
+
return store;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Deep clone an object. Does not preserve any
|
|
115
|
+
// functions on the object
|
|
116
|
+
function clone(obj) {
|
|
117
|
+
return JSON.parse(JSON.stringify(obj));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
module.exports = InMemoryFeatureStore;
|