-
Notifications
You must be signed in to change notification settings - Fork 27
/
diagnosticEvents.js
267 lines (243 loc) · 8.74 KB
/
diagnosticEvents.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
const { v1: uuidv1 } = require('uuid');
// Note that in the diagnostic events spec, these IDs are to be generated with UUID v4. However,
// in JS we were already using v1 for unique context keys, so to avoid bringing in two packages we
// will use v1 here as well.
const { baseOptionDefs } = require('./configuration');
const messages = require('./messages');
const { appendUrlPath } = require('./utils');
function DiagnosticId(sdkKey) {
const ret = {
diagnosticId: uuidv1(),
};
if (sdkKey) {
ret.sdkKeySuffix = sdkKey.length > 6 ? sdkKey.substring(sdkKey.length - 6) : sdkKey;
}
return ret;
}
// A stateful object holding statistics that will go into diagnostic events.
function DiagnosticsAccumulator(startTime) {
let dataSinceDate, droppedEvents, eventsInLastBatch, streamInits;
function reset(time) {
dataSinceDate = time;
droppedEvents = 0;
eventsInLastBatch = 0;
streamInits = [];
}
reset(startTime);
return {
getProps: () => ({
dataSinceDate,
droppedEvents,
eventsInLastBatch,
streamInits,
// omit deduplicatedUsers for the JS SDKs because they don't deduplicate users
}),
setProps: props => {
dataSinceDate = props.dataSinceDate;
droppedEvents = props.droppedEvents || 0;
eventsInLastBatch = props.eventsInLastBatch || 0;
streamInits = props.streamInits || [];
},
incrementDroppedEvents: () => {
droppedEvents++;
},
setEventsInLastBatch: n => {
eventsInLastBatch = n;
},
recordStreamInit: (timestamp, failed, durationMillis) => {
const info = { timestamp, failed, durationMillis };
streamInits.push(info);
},
reset,
};
}
// An object that maintains information that will go into diagnostic events, and knows how to format
// those events. It is instantiated by the SDK client, and shared with the event processor.
//
// The JS-based SDKs have two modes for diagnostic events. By default, the behavior is basically the
// same as the server-side SDKs: a "diagnostic-init" event is sent on startup, and then "diagnostic"
// events with operating statistics are sent periodically. However, in a browser environment this is
// undesirable because the page may be reloaded frequently. In that case, setting the property
// "platform.diagnosticUseCombinedEvent" to true enables an alternate mode in which a combination of
// both kinds of event is sent at intervals, relative to the last time this was done (if any) which
// is cached in local storage.
function DiagnosticsManager(
platform,
persistentStorage,
accumulator,
eventSender,
environmentId,
config,
diagnosticId
) {
const combinedMode = !!platform.diagnosticUseCombinedEvent;
const localStorageKey = 'ld:' + environmentId + ':$diagnostics';
const diagnosticEventsUrl = appendUrlPath(config.eventsUrl, '/events/diagnostic/' + environmentId);
const periodicInterval = config.diagnosticRecordingInterval;
const acc = accumulator;
const initialEventSamplingInterval = 4; // used only in combined mode - see start()
let streamingEnabled = !!config.streaming;
let eventSentTime;
let periodicTimer;
const manager = {};
function makeInitProperties() {
return {
sdk: makeSdkData(),
configuration: makeConfigData(),
platform: platform.diagnosticPlatformData,
};
}
// Send a diagnostic event and do not wait for completion.
function sendDiagnosticEvent(event) {
config.logger && config.logger.debug(messages.debugPostingDiagnosticEvent(event));
eventSender
.sendEvents(event, diagnosticEventsUrl, true)
.then(() => undefined)
.catch(() => undefined);
}
function loadProperties(callback) {
if (!persistentStorage.isEnabled()) {
return callback(false); // false indicates that local storage is not available
}
persistentStorage
.get(localStorageKey)
.then(data => {
if (data) {
try {
const props = JSON.parse(data);
acc.setProps(props);
eventSentTime = props.dataSinceDate;
} catch (e) {
// disregard malformed cached data
}
}
callback(true);
})
.catch(() => {
callback(false);
});
}
function saveProperties() {
if (persistentStorage.isEnabled()) {
const props = { ...acc.getProps() };
persistentStorage.set(localStorageKey, JSON.stringify(props));
}
}
// Creates the initial event that is sent by the event processor when the SDK starts up. This will not
// be repeated during the lifetime of the SDK client. In combined mode, we don't send this.
function createInitEvent() {
return {
kind: 'diagnostic-init',
id: diagnosticId,
creationDate: acc.getProps().dataSinceDate,
...makeInitProperties(),
};
}
// Creates a periodic event containing time-dependent stats, and resets the state of the manager with
// regard to those stats. In combined mode (browser SDK) this also contains the configuration data.
function createPeriodicEventAndReset() {
const currentTime = new Date().getTime();
let ret = {
kind: combinedMode ? 'diagnostic-combined' : 'diagnostic',
id: diagnosticId,
creationDate: currentTime,
...acc.getProps(),
};
if (combinedMode) {
ret = { ...ret, ...makeInitProperties() };
}
acc.reset(currentTime);
return ret;
}
function sendPeriodicEvent() {
sendDiagnosticEvent(createPeriodicEventAndReset());
periodicTimer = setTimeout(sendPeriodicEvent, periodicInterval);
eventSentTime = new Date().getTime();
if (combinedMode) {
saveProperties();
}
}
function makeSdkData() {
const sdkData = { ...platform.diagnosticSdkData };
if (config.wrapperName) {
sdkData.wrapperName = config.wrapperName;
}
if (config.wrapperVersion) {
sdkData.wrapperVersion = config.wrapperVersion;
}
return sdkData;
}
function makeConfigData() {
const configData = {
customBaseURI: config.baseUrl !== baseOptionDefs.baseUrl.default,
customStreamURI: config.streamUrl !== baseOptionDefs.streamUrl.default,
customEventsURI: config.eventsUrl !== baseOptionDefs.eventsUrl.default,
eventsCapacity: config.eventCapacity,
eventsFlushIntervalMillis: config.flushInterval,
reconnectTimeMillis: config.streamReconnectDelay,
streamingDisabled: !streamingEnabled,
allAttributesPrivate: !!config.allAttributesPrivate,
diagnosticRecordingIntervalMillis: config.diagnosticRecordingInterval,
// The following extra properties are only provided by client-side JS SDKs:
usingSecureMode: !!config.hash,
bootstrapMode: !!config.bootstrap,
fetchGoalsDisabled: !config.fetchGoals,
sendEventsOnlyForVariation: !!config.sendEventsOnlyForVariation,
};
// Client-side JS SDKs do not have the following properties which other SDKs have:
// connectTimeoutMillis
// pollingIntervalMillis
// samplingInterval
// socketTimeoutMillis
// startWaitMillis
// userKeysCapacity
// userKeysFlushIntervalMillis
// usingProxy
// usingProxyAuthenticator
// usingRelayDaemon
return configData;
}
// Called when the SDK is starting up. Either send an init event immediately, or, in the alternate
// mode, check for cached local storage properties and send an event only if we haven't done so
// recently.
manager.start = () => {
if (combinedMode) {
loadProperties(localStorageAvailable => {
if (localStorageAvailable) {
const nextEventTime = (eventSentTime || 0) + periodicInterval;
const timeNow = new Date().getTime();
if (timeNow >= nextEventTime) {
sendPeriodicEvent();
} else {
periodicTimer = setTimeout(sendPeriodicEvent, nextEventTime - timeNow);
}
} else {
// We don't have the ability to cache anything in local storage, so we don't know if we
// recently sent an event before this page load, but we would still prefer not to send one
// on *every* page load. So, as a rough heuristic, we'll decide semi-randomly.
if (Math.floor(Math.random() * initialEventSamplingInterval) === 0) {
sendPeriodicEvent();
} else {
periodicTimer = setTimeout(sendPeriodicEvent, periodicInterval);
}
}
});
} else {
sendDiagnosticEvent(createInitEvent());
periodicTimer = setTimeout(sendPeriodicEvent, periodicInterval);
}
};
manager.stop = () => {
periodicTimer && clearTimeout(periodicTimer);
};
// Called when streaming mode is turned on or off dynamically.
manager.setStreaming = enabled => {
streamingEnabled = enabled;
};
return manager;
}
module.exports = {
DiagnosticId,
DiagnosticsAccumulator,
DiagnosticsManager,
};