node-avo-inspector 2.0.0-beta.1 → 2.0.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/CHANGELOG.md +1 -1
- package/README.md +2 -0
- package/dist/AvoBatchQueue.d.ts +73 -0
- package/dist/AvoBatchQueue.js +239 -0
- package/dist/AvoDeduplicator.d.ts +15 -6
- package/dist/AvoDeduplicator.js +46 -24
- package/dist/AvoInspector.d.ts +136 -23
- package/dist/AvoInspector.js +777 -109
- package/dist/AvoInspectorVersion.d.ts +3 -0
- package/dist/AvoInspectorVersion.js +10 -0
- package/dist/AvoLog.d.ts +52 -0
- package/dist/AvoLog.js +246 -0
- package/dist/AvoNetworkCallsHandler.d.ts +55 -8
- package/dist/AvoNetworkCallsHandler.js +266 -92
- package/dist/AvoSchemaParser.d.ts +6 -0
- package/dist/AvoSchemaParser.js +138 -33
- package/dist/AvoStreamId.d.ts +2 -1
- package/dist/AvoStreamId.js +21 -2
- package/dist/eventSpec/AvoEventSpecCache.d.ts +28 -5
- package/dist/eventSpec/AvoEventSpecCache.js +66 -11
- package/dist/eventSpec/AvoEventSpecFetchTypes.d.ts +50 -18
- package/dist/eventSpec/AvoEventSpecFetcher.d.ts +29 -6
- package/dist/eventSpec/AvoEventSpecFetcher.js +279 -69
- package/dist/eventSpec/EventValidator.d.ts +16 -10
- package/dist/eventSpec/EventValidator.js +351 -38
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4 -1
- package/dist/utils.d.ts +29 -1
- package/dist/utils.js +86 -10
- package/package.json +2 -1
package/dist/AvoInspector.js
CHANGED
|
@@ -13,46 +13,237 @@ exports.AvoInspector = void 0;
|
|
|
13
13
|
const AvoInspectorEnv_1 = require("./AvoInspectorEnv");
|
|
14
14
|
const AvoSchemaParser_1 = require("./AvoSchemaParser");
|
|
15
15
|
const AvoNetworkCallsHandler_1 = require("./AvoNetworkCallsHandler");
|
|
16
|
+
const AvoBatchQueue_1 = require("./AvoBatchQueue");
|
|
16
17
|
const AvoDeduplicator_1 = require("./AvoDeduplicator");
|
|
17
18
|
const AvoStreamId_1 = require("./AvoStreamId");
|
|
18
19
|
const AvoEventSpecFetcher_1 = require("./eventSpec/AvoEventSpecFetcher");
|
|
19
20
|
const AvoEventSpecCache_1 = require("./eventSpec/AvoEventSpecCache");
|
|
20
21
|
const EventValidator_1 = require("./eventSpec/EventValidator");
|
|
22
|
+
const AvoInspectorVersion_1 = require("./AvoInspectorVersion");
|
|
23
|
+
const AvoLog_1 = require("./AvoLog");
|
|
21
24
|
const utils_1 = require("./utils");
|
|
22
|
-
const libVersion =
|
|
25
|
+
const libVersion = AvoInspectorVersion_1.VERSION;
|
|
26
|
+
const DEFAULT_BATCH_SIZE = 30;
|
|
27
|
+
const DEFAULT_BATCH_FLUSH_SECONDS = 30;
|
|
28
|
+
const DEFAULT_MAX_QUEUE_SIZE = 1000;
|
|
29
|
+
const DEFAULT_FLUSH_TIMEOUT_MS = 10000;
|
|
30
|
+
// The longest a track call can stay pending on its own work: a spec fetch (a socket wait
|
|
31
|
+
// and the fetch, 10 s each), then, in dev, its send (10 s). A send that also waits for a
|
|
32
|
+
// free slot can take longer; the call then resolves with its schema when the hold runs out.
|
|
33
|
+
const AWAITED_TRACK_HOLD_MS = 30000;
|
|
34
|
+
// A track call's promise that notices when someone awaits it. The SDK's sockets and
|
|
35
|
+
// timers do not hold the process, so a script awaiting a track that waits on a spec fetch
|
|
36
|
+
// or (in dev) on its send would otherwise look idle: "beforeExit" would fire mid-loop and
|
|
37
|
+
// the exit drain could end the process before the loop resumes. Once the promise is
|
|
38
|
+
// awaited (await, then, catch, finally, Promise.all all call then), a ref'd timer holds the
|
|
39
|
+
// process until it settles, at most AWAITED_TRACK_HOLD_MS; a fire-and-forget call with
|
|
40
|
+
// .catch() counts. A call whose promise nothing touches holds nothing. A call still pending
|
|
41
|
+
// when its hold runs out resolves with `onHoldExpired()`, so the script resumes; its event
|
|
42
|
+
// stays queued, and goes out with the exit drain or is reported as lost at exit.
|
|
43
|
+
class AwaitedTrackPromise extends Promise {
|
|
44
|
+
constructor() {
|
|
45
|
+
super(...arguments);
|
|
46
|
+
this.onFirstAwait = null;
|
|
47
|
+
}
|
|
48
|
+
static get [Symbol.species]() {
|
|
49
|
+
return Promise;
|
|
50
|
+
}
|
|
51
|
+
then(onFulfilled, onRejected) {
|
|
52
|
+
const onFirstAwait = this.onFirstAwait;
|
|
53
|
+
if (onFirstAwait !== null) {
|
|
54
|
+
this.onFirstAwait = null;
|
|
55
|
+
onFirstAwait();
|
|
56
|
+
}
|
|
57
|
+
return super.then(onFulfilled, onRejected);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const holdWhileAwaited = (work, onHoldExpired) => {
|
|
61
|
+
let settled = false;
|
|
62
|
+
let hold = null;
|
|
63
|
+
const done = () => {
|
|
64
|
+
settled = true;
|
|
65
|
+
if (hold !== null) {
|
|
66
|
+
clearTimeout(hold);
|
|
67
|
+
hold = null;
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
let release = () => { };
|
|
71
|
+
const tracked = new AwaitedTrackPromise((resolve, reject) => {
|
|
72
|
+
release = resolve;
|
|
73
|
+
work.then((value) => {
|
|
74
|
+
done();
|
|
75
|
+
resolve(value);
|
|
76
|
+
}, (reason) => {
|
|
77
|
+
done();
|
|
78
|
+
reject(reason);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
tracked.onFirstAwait = () => {
|
|
82
|
+
if (!settled) {
|
|
83
|
+
hold = setTimeout(() => {
|
|
84
|
+
hold = null;
|
|
85
|
+
settled = true;
|
|
86
|
+
release(onHoldExpired());
|
|
87
|
+
}, AWAITED_TRACK_HOLD_MS);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
return tracked;
|
|
91
|
+
};
|
|
92
|
+
// At an idle point with validations pending, how long the exit drain waits for them before
|
|
93
|
+
// sending anything (once per exit deadline).
|
|
94
|
+
const VALIDATION_GRACE_MS = 1000;
|
|
95
|
+
// Events waiting for a spec fetch at once, across every instance. The fetches share one
|
|
96
|
+
// 8-socket pool, so past this an event is sent without validation rather than queued.
|
|
97
|
+
const MAX_WAITING_VALIDATIONS = 1000;
|
|
98
|
+
// String() for a warning; a value with no string conversion (a null-prototype object)
|
|
99
|
+
// falls back to its tag, e.g. "[object Object]", which String() gives a plain object.
|
|
100
|
+
const valueToString = (value) => {
|
|
101
|
+
try {
|
|
102
|
+
return String(value);
|
|
103
|
+
}
|
|
104
|
+
catch (e) {
|
|
105
|
+
return Object.prototype.toString.call(value);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
const NO_API_KEY_MESSAGE = "[Avo Inspector] No API key provided. Inspector can't operate without API key.";
|
|
109
|
+
// The keys of InspectorEvent; any other key in a tracked event is warned about.
|
|
110
|
+
const KNOWN_EVENT_KEYS = ["eventName", "eventProperties", "streamId", "outputReference", "originHint", "originAppVersion"];
|
|
23
111
|
class AvoInspector {
|
|
24
|
-
|
|
25
|
-
this.
|
|
26
|
-
|
|
27
|
-
|
|
112
|
+
drainThisTurn() {
|
|
113
|
+
if (!this.drainScheduled) {
|
|
114
|
+
this.drainScheduled = true;
|
|
115
|
+
setImmediate(() => {
|
|
116
|
+
this.drainScheduled = false;
|
|
117
|
+
if (!this.destroyed) {
|
|
118
|
+
this.batchQueue.drain();
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// Mirrors `promise`, but resolves `onDestroy` instead if destroy() runs first.
|
|
124
|
+
untilDestroyed(promise, onDestroy) {
|
|
125
|
+
return new Promise((resolve, reject) => {
|
|
126
|
+
const cancel = () => resolve(onDestroy);
|
|
127
|
+
this.destroyWaiters.add(cancel);
|
|
128
|
+
promise.then((value) => {
|
|
129
|
+
this.destroyWaiters.delete(cancel);
|
|
130
|
+
resolve(value);
|
|
131
|
+
}, (reason) => {
|
|
132
|
+
this.destroyWaiters.delete(cancel);
|
|
133
|
+
reject(reason);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
// Mirrors a validation, but resolves null (send unvalidated) if giveUpValidations() runs first.
|
|
138
|
+
untilGivenUp(validation) {
|
|
139
|
+
return new Promise((resolve, reject) => {
|
|
140
|
+
const giveUp = () => resolve(null);
|
|
141
|
+
this.giveUpWaiters.add(giveUp);
|
|
142
|
+
validation.then((value) => {
|
|
143
|
+
this.giveUpWaiters.delete(giveUp);
|
|
144
|
+
resolve(value);
|
|
145
|
+
}, (reason) => {
|
|
146
|
+
this.giveUpWaiters.delete(giveUp);
|
|
147
|
+
reject(reason);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
/** The exit drain ran out of time for validations: their events are sent unvalidated. */
|
|
152
|
+
giveUpValidations() {
|
|
153
|
+
const waiters = Array.from(this.giveUpWaiters);
|
|
154
|
+
this.giveUpWaiters.clear();
|
|
155
|
+
if (waiters.length > 0 && AvoInspector.shouldLog) {
|
|
156
|
+
console.log("Avo Inspector: exiting; sending " + waiters.length + " event(s) without waiting for their event spec.");
|
|
28
157
|
}
|
|
158
|
+
waiters.forEach((giveUp) => giveUp());
|
|
159
|
+
}
|
|
160
|
+
get pendingCount() {
|
|
161
|
+
return this.pending.size;
|
|
162
|
+
}
|
|
163
|
+
trackPending(promise) {
|
|
164
|
+
this.pending.add(promise);
|
|
165
|
+
this.updateExitDrain();
|
|
29
166
|
const done = () => {
|
|
30
|
-
this.
|
|
31
|
-
|
|
32
|
-
clearInterval(this.keepAliveTimer);
|
|
33
|
-
this.keepAliveTimer = null;
|
|
34
|
-
}
|
|
167
|
+
this.pending.delete(promise);
|
|
168
|
+
this.updateExitDrain();
|
|
35
169
|
};
|
|
36
170
|
promise.then(done, done);
|
|
171
|
+
return promise;
|
|
172
|
+
}
|
|
173
|
+
static armExitDrain() {
|
|
174
|
+
if (!AvoInspector.exitDrainArmed) {
|
|
175
|
+
process.once("beforeExit", AvoInspector.drainOnExit);
|
|
176
|
+
AvoInspector.exitDrainArmed = true;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
static disarmExitDrain() {
|
|
180
|
+
if (AvoInspector.exitDrainArmed) {
|
|
181
|
+
process.removeListener("beforeExit", AvoInspector.drainOnExit);
|
|
182
|
+
AvoInspector.exitDrainArmed = false;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
updateExitDrain() {
|
|
186
|
+
const hasWork = !this.destroyed && (this.batchQueue.length > 0 || this.pending.size > 0);
|
|
187
|
+
if (hasWork) {
|
|
188
|
+
AvoInspector.instancesWithWork.add(this);
|
|
189
|
+
AvoInspector.armExitDrain();
|
|
190
|
+
if (!AvoInspector.exitReportArmed) {
|
|
191
|
+
process.on("exit", AvoInspector.reportOnExit);
|
|
192
|
+
AvoInspector.exitReportArmed = true;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
AvoInspector.instancesWithWork.delete(this);
|
|
197
|
+
if (AvoInspector.instancesWithWork.size === 0) {
|
|
198
|
+
AvoInspector.disarmExitDrain();
|
|
199
|
+
process.removeListener("exit", AvoInspector.reportOnExit);
|
|
200
|
+
AvoInspector.exitReportArmed = false;
|
|
201
|
+
AvoInspector.exitDeadline = null;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
37
204
|
}
|
|
205
|
+
/** Whether the SDK writes its diagnostic log lines (shared by every instance). */
|
|
38
206
|
static get shouldLog() {
|
|
39
207
|
return this._shouldLog;
|
|
40
208
|
}
|
|
41
209
|
static set shouldLog(enable) {
|
|
42
210
|
this._shouldLog = enable;
|
|
43
211
|
}
|
|
212
|
+
/**
|
|
213
|
+
* Creates an Inspector instance. A missing or unsupported `env` falls back to dev.
|
|
214
|
+
*
|
|
215
|
+
* @throws if `apiKey` or `version` is blank, or `apiKey` contains a character that
|
|
216
|
+
* cannot be sent in an HTTP header.
|
|
217
|
+
*/
|
|
44
218
|
constructor(options) {
|
|
45
219
|
this.eventSpecFetcher = null;
|
|
46
220
|
this.eventSpecCache = null;
|
|
47
221
|
this.eventValidator = null;
|
|
48
222
|
this.generatedAnonymousId = "";
|
|
49
|
-
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
this.
|
|
53
|
-
|
|
223
|
+
this.destroyed = false;
|
|
224
|
+
// Settles each track still waiting on a spec fetch or an immediate send when destroy()
|
|
225
|
+
// runs. Entries remove themselves once their wait settles, so none outlive their call.
|
|
226
|
+
this.destroyWaiters = new Set();
|
|
227
|
+
// In-flight work (spec fetches before enqueue, and batch sends) that flush() awaits.
|
|
228
|
+
// Nothing here keeps the process alive: callers flush() or await before exit.
|
|
229
|
+
this.pending = new Set();
|
|
230
|
+
// Pending entries that are event spec validations, keyed by their pending promise.
|
|
231
|
+
this.validations = new Map();
|
|
232
|
+
// One drain for every flush-marked validation that settles in the same event-loop turn.
|
|
233
|
+
this.drainScheduled = false;
|
|
234
|
+
// Frees this instance's places among them; destroy() runs whatever is left.
|
|
235
|
+
this.releaseWaiting = new Set();
|
|
236
|
+
// Validations not finished yet (their events are not queued), settled when they are.
|
|
237
|
+
this.validationOutcomes = new Set();
|
|
238
|
+
// Gives up each validation still pending (giveUpValidations).
|
|
239
|
+
this.giveUpWaiters = new Set();
|
|
54
240
|
// the constructor does aggressive null/undefined checking because same code paths will be accessible from JS
|
|
55
|
-
if (
|
|
241
|
+
if (options === null || typeof options !== "object") {
|
|
242
|
+
throw new Error(NO_API_KEY_MESSAGE);
|
|
243
|
+
}
|
|
244
|
+
// A non-string env is not "empty": it falls through to the unsupported-value check.
|
|
245
|
+
const env = options.env;
|
|
246
|
+
if (env === undefined || env === null || (typeof env === "string" && (0, utils_1.isValueEmpty)(env))) {
|
|
56
247
|
this.environment = AvoInspectorEnv_1.AvoInspectorEnv.Dev;
|
|
57
248
|
console.warn("[Avo Inspector] No environment provided. Defaulting to dev.");
|
|
58
249
|
}
|
|
@@ -63,13 +254,23 @@ class AvoInspector {
|
|
|
63
254
|
else {
|
|
64
255
|
this.environment = options.env;
|
|
65
256
|
}
|
|
66
|
-
if ((0, utils_1.isValueEmpty)(options.apiKey)) {
|
|
67
|
-
throw new Error(
|
|
257
|
+
if (typeof options.apiKey !== "string" || (0, utils_1.isValueEmpty)(options.apiKey)) {
|
|
258
|
+
throw new Error(NO_API_KEY_MESSAGE);
|
|
259
|
+
}
|
|
260
|
+
else if (/[\r\n\0]/.test(options.apiKey)) {
|
|
261
|
+
// The spec's exact message covers CR, LF and NUL.
|
|
262
|
+
throw new Error("[Avo Inspector] API key contains a control character. The API key is sent as a request header and cannot contain CR, LF, or NUL.");
|
|
263
|
+
}
|
|
264
|
+
else if ((0, utils_1.hasHeaderControlChar)(options.apiKey)) {
|
|
265
|
+
throw new Error("[Avo Inspector] apiKey must not contain control characters");
|
|
266
|
+
}
|
|
267
|
+
else if ((0, utils_1.hasNonLatin1Char)(options.apiKey)) {
|
|
268
|
+
throw new Error("[Avo Inspector] apiKey must only contain characters that can be sent in an HTTP header");
|
|
68
269
|
}
|
|
69
270
|
else {
|
|
70
271
|
this.apiKey = options.apiKey;
|
|
71
272
|
}
|
|
72
|
-
if ((0, utils_1.isValueEmpty)(options.version)) {
|
|
273
|
+
if (typeof options.version !== "string" || (0, utils_1.isValueEmpty)(options.version)) {
|
|
73
274
|
throw new Error("[Avo Inspector] No version provided. Many features of Inspector rely on versioning. Please provide comparable string version, i.e. integer or semantic.");
|
|
74
275
|
}
|
|
75
276
|
else {
|
|
@@ -94,28 +295,158 @@ class AvoInspector {
|
|
|
94
295
|
}
|
|
95
296
|
this.avoNetworkCallsHandler = new AvoNetworkCallsHandler_1.AvoNetworkCallsHandler(this.apiKey, this.environment.toString(), options.appName || "", this.version, libVersion, this.publicEncryptionKey);
|
|
96
297
|
this.avoDeduplicator = new AvoDeduplicator_1.AvoDeduplicator();
|
|
298
|
+
const batchOptions = AvoInspector.resolveBatchOptions(options);
|
|
299
|
+
// dev sends every event immediately, whatever was configured.
|
|
300
|
+
this.batchSize =
|
|
301
|
+
this.environment === AvoInspectorEnv_1.AvoInspectorEnv.Dev ? 1 : batchOptions.batchSize;
|
|
302
|
+
// Not clamped: the conformance suite (batch-4) requires FIFO overflow in this case.
|
|
303
|
+
if (this.batchSize > batchOptions.maxQueueSize) {
|
|
304
|
+
console.warn("[Avo Inspector] batchSize " + this.batchSize + " is larger than maxQueueSize " +
|
|
305
|
+
batchOptions.maxQueueSize + ", so a batch never fills: events are sent only by the " +
|
|
306
|
+
"scheduled flush or flush(), and the oldest are dropped once " +
|
|
307
|
+
batchOptions.maxQueueSize + " are buffered. Set batchSize to at most maxQueueSize.");
|
|
308
|
+
}
|
|
309
|
+
this.batchQueue = new AvoBatchQueue_1.AvoBatchQueue(Object.assign(Object.assign({}, batchOptions), { batchSize: this.batchSize }), (batch) => this.sendBatch(batch), "failed",
|
|
310
|
+
// A batch is in flight from the moment it is swapped out, even while it waits for a
|
|
311
|
+
// send slot, so flush() waits for it.
|
|
312
|
+
(outcome) => this.trackPending(outcome));
|
|
97
313
|
// Initialize event spec validation for dev/staging only
|
|
98
314
|
if (this.environment !== AvoInspectorEnv_1.AvoInspectorEnv.Prod) {
|
|
99
|
-
|
|
315
|
+
// Spec fetches follow the track endpoint to a mock server, so validation runs there too.
|
|
316
|
+
this.eventSpecFetcher = new AvoEventSpecFetcher_1.AvoEventSpecFetcher(this.apiKey, this.avoNetworkCallsHandler.mockEndpoint);
|
|
100
317
|
this.eventSpecCache = new AvoEventSpecCache_1.AvoEventSpecCache();
|
|
101
318
|
this.eventValidator = new EventValidator_1.EventValidator();
|
|
102
319
|
}
|
|
103
320
|
}
|
|
104
|
-
|
|
321
|
+
static resolveBatchOptions(options) {
|
|
322
|
+
const pick = (name, value, fallback, isValid) => {
|
|
323
|
+
if (value === undefined || value === null) {
|
|
324
|
+
return fallback;
|
|
325
|
+
}
|
|
326
|
+
if (typeof value === "number" && isValid(value)) {
|
|
327
|
+
return value;
|
|
328
|
+
}
|
|
329
|
+
console.warn(
|
|
330
|
+
// valueToString: concatenation throws for a symbol or a null-prototype object.
|
|
331
|
+
"[Avo Inspector] Invalid " + name + " " + valueToString(value) + ". Using default " + fallback + ".");
|
|
332
|
+
return fallback;
|
|
333
|
+
};
|
|
334
|
+
const isPositiveInteger = (v) => Number.isInteger(v) && v >= 1;
|
|
335
|
+
return {
|
|
336
|
+
batchSize: pick("batchSize", options.batchSize, DEFAULT_BATCH_SIZE, isPositiveInteger),
|
|
337
|
+
batchFlushSeconds: pick("batchFlushSeconds", options.batchFlushSeconds, DEFAULT_BATCH_FLUSH_SECONDS, (v) => Number.isFinite(v) && v > 0),
|
|
338
|
+
maxQueueSize: pick("maxQueueSize", options.maxQueueSize, DEFAULT_MAX_QUEUE_SIZE, isPositiveInteger),
|
|
339
|
+
disableBatchTimer: options.disableBatchTimer === true,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Resolves the gateway options for one event. outputReference / originHint are
|
|
344
|
+
* omitted when blank; appVersion is the per-event override, null for a
|
|
345
|
+
* source-scoped event without one, else the instance version.
|
|
346
|
+
*/
|
|
347
|
+
resolveGatewayFields(options) {
|
|
348
|
+
const opts = options !== null && typeof options === "object" ? options : {};
|
|
349
|
+
const outputReference = (0, utils_1.normalizeOption)(opts.outputReference);
|
|
350
|
+
const originHint = (0, utils_1.normalizeOption)(opts.originHint);
|
|
351
|
+
const originAppVersion = (0, utils_1.normalizeOption)(opts.originAppVersion);
|
|
352
|
+
const resolved = {
|
|
353
|
+
gatewayScoped: outputReference !== undefined ||
|
|
354
|
+
originHint !== undefined ||
|
|
355
|
+
originAppVersion !== undefined,
|
|
356
|
+
appVersion: originAppVersion !== undefined
|
|
357
|
+
? originAppVersion
|
|
358
|
+
: originHint !== undefined
|
|
359
|
+
? null
|
|
360
|
+
: this.version,
|
|
361
|
+
};
|
|
362
|
+
if (outputReference !== undefined) {
|
|
363
|
+
resolved.outputReference = outputReference;
|
|
364
|
+
}
|
|
365
|
+
if (originHint !== undefined) {
|
|
366
|
+
resolved.originHint = originHint;
|
|
367
|
+
}
|
|
368
|
+
return resolved;
|
|
369
|
+
}
|
|
370
|
+
// Warns (always on, rate-limited) when the event has keys other than InspectorEvent's,
|
|
371
|
+
// such as a typo; the known keys are still used and the event is still tracked.
|
|
372
|
+
// Never throws: listing a proxy's keys can.
|
|
373
|
+
static warnUnknownEventKeys(event) {
|
|
374
|
+
let unknown;
|
|
105
375
|
try {
|
|
376
|
+
unknown = Object.keys(event).filter((key) => KNOWN_EVENT_KEYS.indexOf(key) < 0);
|
|
377
|
+
}
|
|
378
|
+
catch (e) {
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
if (unknown.length > 0) {
|
|
382
|
+
AvoLog_1.AvoLog.unknownEventKeys(unknown, KNOWN_EVENT_KEYS);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Extracts the event schema and queues it for the Inspector API.
|
|
387
|
+
*
|
|
388
|
+
* Resolves with the schema once the event is queued. In dev (batch size 1) the send
|
|
389
|
+
* happens within the call and a non-200 response resolves `[]`; with batching the
|
|
390
|
+
* batch's HTTP outcome is not observable here. Buffered events are only delivered
|
|
391
|
+
* by a later size or time trigger, or by `flush()` — call `flush()` before exit.
|
|
392
|
+
*
|
|
393
|
+
* Anything but an object (such as a 1.x call `trackSchemaFromEvent(name, properties)`)
|
|
394
|
+
* sends nothing, prints an always-on error naming the fix, and resolves `[]`.
|
|
395
|
+
*/
|
|
396
|
+
trackSchemaFromEvent(event) {
|
|
397
|
+
if (event === null || typeof event !== "object" || Array.isArray(event)) {
|
|
398
|
+
AvoLog_1.AvoLog.notAnInspectorEvent();
|
|
399
|
+
return Promise.resolve([]);
|
|
400
|
+
}
|
|
401
|
+
AvoInspector.warnUnknownEventKeys(event);
|
|
402
|
+
const call = { schema: [] };
|
|
403
|
+
let work;
|
|
404
|
+
try {
|
|
405
|
+
// Each field is read once, here: a getter on the event runs only once.
|
|
406
|
+
const { eventName, eventProperties, streamId, outputReference, originHint, originAppVersion } = event;
|
|
407
|
+
work = this.track(eventName,
|
|
408
|
+
// Absent or null: no properties. Every step below handles both, as for a JS caller.
|
|
409
|
+
eventProperties, false, null, null, streamId, { outputReference, originHint, originAppVersion }, call);
|
|
410
|
+
}
|
|
411
|
+
catch (e) {
|
|
412
|
+
AvoLog_1.AvoLog.internal(e);
|
|
413
|
+
work = Promise.reject(AvoLog_1.INTERNAL_ERROR_MESSAGE);
|
|
414
|
+
}
|
|
415
|
+
return holdWhileAwaited(work, () => call.schema);
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Codegen entry point: like `trackSchemaFromEvent`, and additionally records the
|
|
419
|
+
* Avo function's `eventId` / `eventHash` so the event is sent with `avoFunction: true`.
|
|
420
|
+
*/
|
|
421
|
+
_avoFunctionTrackSchemaFromEvent(eventName, eventProperties, eventId, eventHash, streamId, options) {
|
|
422
|
+
const call = { schema: [] };
|
|
423
|
+
return holdWhileAwaited(this.track(eventName, eventProperties, true, eventId, eventHash, streamId, options, call), () => call.schema);
|
|
424
|
+
}
|
|
425
|
+
// Shared by the manual and Codegen entry points.
|
|
426
|
+
track(eventName, eventProperties, fromAvoFunction, eventId, eventHash, streamId, options, call) {
|
|
427
|
+
AvoInspector.lastTrackAt = (0, utils_1.monotonicNowMs)();
|
|
428
|
+
try {
|
|
429
|
+
if (this.destroyed) {
|
|
430
|
+
return Promise.resolve([]);
|
|
431
|
+
}
|
|
432
|
+
// A missing event name (null, undefined, non-string, empty or whitespace-only) is
|
|
433
|
+
// still reported to Inspector, under a placeholder name. A valid name is kept as is.
|
|
434
|
+
if (typeof eventName !== "string" || eventName.trim().length === 0) {
|
|
435
|
+
AvoLog_1.AvoLog.missingEventName();
|
|
436
|
+
eventName = AvoLog_1.MISSING_EVENT_NAME;
|
|
437
|
+
}
|
|
106
438
|
const avoStreamId = new AvoStreamId_1.AvoStreamId(streamId);
|
|
107
439
|
const anonymousId = avoStreamId.streamId || this.generatedAnonymousId;
|
|
108
|
-
|
|
440
|
+
const gatewayFields = this.resolveGatewayFields(options);
|
|
441
|
+
if (this.shouldRegisterEvent(eventName, eventProperties, fromAvoFunction, anonymousId, gatewayFields)) {
|
|
442
|
+
let eventSchema = this.extractSchema(eventProperties, false);
|
|
443
|
+
call.schema = eventSchema;
|
|
109
444
|
if (AvoInspector.shouldLog) {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
JSON.stringify(eventProperties));
|
|
445
|
+
// The schema (names and types), never the values: the logging flag is shared by
|
|
446
|
+
// every instance, so a prod instance can log once any dev instance turns it on.
|
|
447
|
+
console.log("Avo Inspector: Supplied event " + eventName + " with schema " + JSON.stringify(eventSchema));
|
|
114
448
|
}
|
|
115
|
-
|
|
116
|
-
return this.sendEventWithOptionalValidation(eventName, eventSchema, null, null, anonymousId, eventProperties).then(() => {
|
|
117
|
-
return eventSchema;
|
|
118
|
-
});
|
|
449
|
+
return this.sampleAndEnqueue(eventName, eventSchema, eventId, eventHash, anonymousId, eventProperties, gatewayFields);
|
|
119
450
|
}
|
|
120
451
|
else {
|
|
121
452
|
if (AvoInspector.shouldLog) {
|
|
@@ -125,102 +456,266 @@ class AvoInspector {
|
|
|
125
456
|
}
|
|
126
457
|
}
|
|
127
458
|
catch (e) {
|
|
128
|
-
|
|
129
|
-
return Promise.reject(
|
|
459
|
+
AvoLog_1.AvoLog.internal(e);
|
|
460
|
+
return Promise.reject(AvoLog_1.INTERNAL_ERROR_MESSAGE);
|
|
130
461
|
}
|
|
131
462
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
463
|
+
/**
|
|
464
|
+
* Codegen/manual deduplication applies only to plain calls. A call carrying gateway
|
|
465
|
+
* options is a distinct observation per gateway output and is always sent.
|
|
466
|
+
*/
|
|
467
|
+
shouldRegisterEvent(eventName, eventProperties, fromAvoFunction, anonymousId, gatewayFields) {
|
|
468
|
+
if (gatewayFields.gatewayScoped) {
|
|
469
|
+
return true;
|
|
470
|
+
}
|
|
471
|
+
return this.avoDeduplicator.shouldRegisterEvent(eventName, eventProperties, fromAvoFunction, anonymousId);
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* Samples the event, then queues it. When event spec validation is active the spec
|
|
475
|
+
* is fetched (or read from cache) first, so the event joins the queue once its
|
|
476
|
+
* validation results are merged in; flush() awaits that fetch before draining.
|
|
477
|
+
*/
|
|
478
|
+
sampleAndEnqueue(eventName, eventSchema, eventId, eventHash, anonymousId, rawEventProperties, gatewayFields) {
|
|
479
|
+
const samplingRate = this.avoNetworkCallsHandler.getSamplingRate();
|
|
480
|
+
// Stamped at the call: with validation active the event may join the queue later.
|
|
481
|
+
const createdAt = new Date().toISOString();
|
|
482
|
+
if (Math.random() > samplingRate) {
|
|
483
|
+
if (AvoInspector.shouldLog) {
|
|
484
|
+
console.log("Avo Inspector: event " + eventName + " dropped due to sampling rate.");
|
|
485
|
+
}
|
|
486
|
+
return Promise.resolve(eventSchema);
|
|
487
|
+
}
|
|
488
|
+
// The rate that governed this event's sampling decision, not the one at send time.
|
|
489
|
+
const stamp = { createdAt, samplingRate };
|
|
490
|
+
const buildBody = (validationResult) => {
|
|
491
|
+
// The body gets its own copy: the call resolves with eventSchema, which the caller
|
|
492
|
+
// may change while the event still waits in the batch.
|
|
493
|
+
const bodySchema = JSON.parse(JSON.stringify(eventSchema));
|
|
494
|
+
let body;
|
|
495
|
+
if (validationResult) {
|
|
136
496
|
if (AvoInspector.shouldLog) {
|
|
137
|
-
console.log("Avo Inspector:
|
|
138
|
-
eventName +
|
|
139
|
-
" with params \n" +
|
|
140
|
-
JSON.stringify(eventProperties));
|
|
497
|
+
console.log("Avo Inspector: Sending validated event " + eventName);
|
|
141
498
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
return eventSchema;
|
|
145
|
-
});
|
|
499
|
+
const eventProps = this.avoNetworkCallsHandler.buildEventProperties(bodySchema, rawEventProperties);
|
|
500
|
+
body = this.avoNetworkCallsHandler.bodyForValidatedEventSchemaCall(anonymousId, eventName, eventProps, eventId, eventHash, validationResult.metadata, validationResult.propertyResults, gatewayFields, stamp);
|
|
146
501
|
}
|
|
147
502
|
else {
|
|
148
|
-
|
|
149
|
-
|
|
503
|
+
body = this.avoNetworkCallsHandler.bodyForEventSchemaCall(anonymousId, eventName, bodySchema, eventId, eventHash, rawEventProperties, gatewayFields, stamp);
|
|
504
|
+
}
|
|
505
|
+
return body;
|
|
506
|
+
};
|
|
507
|
+
if (!this.isValidationActive()) {
|
|
508
|
+
return this.enqueue(buildBody(null), eventSchema).result;
|
|
509
|
+
}
|
|
510
|
+
// A flush() that starts while this validation is in progress sets flushRequested:
|
|
511
|
+
// the event then goes out once it is queued, in the drain shared by every validation
|
|
512
|
+
// that settles in the same event-loop turn, and flush() waits for that send.
|
|
513
|
+
const validation = { flushRequested: false };
|
|
514
|
+
const outcome = this.untilDestroyed(this.untilGivenUp(this.fetchAndValidate(eventName, eventSchema, anonymousId, rawEventProperties, gatewayFields)), null)
|
|
515
|
+
.catch((err) => {
|
|
516
|
+
if (AvoInspector.shouldLog) {
|
|
517
|
+
// Only the error's type: validation reads property values, which can end up in it.
|
|
518
|
+
console.warn("Avo Inspector: Event spec validation failed for event: " + eventName + ". Sending without validation. (" + AvoLog_1.AvoLog.errorType(err) + ")");
|
|
519
|
+
}
|
|
520
|
+
return null;
|
|
521
|
+
})
|
|
522
|
+
.then((validationResult) => {
|
|
523
|
+
if (this.destroyed) {
|
|
524
|
+
return { result: Promise.resolve([]), send: null };
|
|
525
|
+
}
|
|
526
|
+
try {
|
|
527
|
+
const enqueued = this.enqueue(buildBody(validationResult), eventSchema);
|
|
528
|
+
let send = enqueued.send;
|
|
529
|
+
if (send === null && validation.flushRequested) {
|
|
530
|
+
// flush() waits for the batch that carries this event. Another drain (a size
|
|
531
|
+
// trigger or the timer) may swap it out before the scheduled one runs, so the
|
|
532
|
+
// wait follows the buffered batch, not this particular drain.
|
|
533
|
+
send = this.batchQueue.bufferedBatchOutcome();
|
|
534
|
+
this.drainThisTurn();
|
|
150
535
|
}
|
|
151
|
-
return
|
|
536
|
+
return { result: enqueued.result, send };
|
|
152
537
|
}
|
|
538
|
+
catch (err) {
|
|
539
|
+
// Same outcome as a synchronous internal error on the unvalidated path (SPEC §4.2
|
|
540
|
+
// step 5): logged, and the call rejects with the internal error message.
|
|
541
|
+
AvoLog_1.AvoLog.internal(err);
|
|
542
|
+
return { result: null, send: null };
|
|
543
|
+
}
|
|
544
|
+
});
|
|
545
|
+
this.validationOutcomes.add(outcome);
|
|
546
|
+
const validated = () => {
|
|
547
|
+
this.validationOutcomes.delete(outcome);
|
|
548
|
+
};
|
|
549
|
+
outcome.then(validated, validated);
|
|
550
|
+
// In flight until the event is queued and, if that triggered a send, until it settles.
|
|
551
|
+
const inFlight = outcome.then(({ send }) => (send ? send.then(() => undefined) : undefined));
|
|
552
|
+
this.validations.set(inFlight, validation);
|
|
553
|
+
const forget = () => {
|
|
554
|
+
this.validations.delete(inFlight);
|
|
555
|
+
};
|
|
556
|
+
inFlight.then(forget, forget);
|
|
557
|
+
this.trackPending(inFlight);
|
|
558
|
+
return outcome.then(({ result }) => result || Promise.reject(AvoLog_1.INTERNAL_ERROR_MESSAGE));
|
|
559
|
+
}
|
|
560
|
+
enqueue(body, eventSchema) {
|
|
561
|
+
const send = this.batchQueue.enqueue(body);
|
|
562
|
+
this.updateExitDrain();
|
|
563
|
+
if (this.batchSize === 1 && send !== null) {
|
|
564
|
+
// Immediate send: the HTTP outcome is observable per call. An event whose send is
|
|
565
|
+
// abandoned by destroy() is not delivered, so it resolves [] like a non-200.
|
|
566
|
+
return {
|
|
567
|
+
result: this.untilDestroyed(send.then((outcome) => (outcome === "non200" || this.destroyed ? [] : eventSchema)), []),
|
|
568
|
+
send,
|
|
569
|
+
};
|
|
153
570
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
571
|
+
if (this.batchQueue.waitingLength >= AvoBatchQueue_1.BACKPRESSURE_WAITING_EVENTS) {
|
|
572
|
+
// Backpressure: the event is queued, but the call resolves only once a send slot
|
|
573
|
+
// frees up enough room, so an awaited loop cannot outrun the sends and overflow the
|
|
574
|
+
// backlog. A call that is not awaited is unaffected. destroy() resolves it [].
|
|
575
|
+
return {
|
|
576
|
+
result: this.untilDestroyed(this.batchQueue.whenBelowBackpressure().then(() => (this.destroyed ? [] : eventSchema)), []),
|
|
577
|
+
send,
|
|
578
|
+
};
|
|
157
579
|
}
|
|
580
|
+
return { result: Promise.resolve(eventSchema), send };
|
|
581
|
+
}
|
|
582
|
+
sendBatch(batch) {
|
|
583
|
+
if (this.destroyed) {
|
|
584
|
+
return Promise.resolve("failed");
|
|
585
|
+
}
|
|
586
|
+
const send = this.avoNetworkCallsHandler.callInspectorWithBatchBody(batch).then((status) => {
|
|
587
|
+
if (typeof status === "number" && status !== 200) {
|
|
588
|
+
// Not logged for a send abandoned by destroy().
|
|
589
|
+
if (!this.destroyed) {
|
|
590
|
+
AvoLog_1.AvoLog.rejected(status);
|
|
591
|
+
}
|
|
592
|
+
return "non200";
|
|
593
|
+
}
|
|
594
|
+
if (AvoInspector.shouldLog) {
|
|
595
|
+
batch.forEach((event) => {
|
|
596
|
+
console.log("Avo Inspector: Saved event " + event.eventName + " with schema " + (0, utils_1.formatSchema)(event.eventProperties));
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
return "ok";
|
|
600
|
+
}, (err) => {
|
|
601
|
+
// At-most-once: a failed batch is dropped, never re-queued or retried.
|
|
602
|
+
if (!this.destroyed) {
|
|
603
|
+
AvoLog_1.AvoLog.failed(err);
|
|
604
|
+
}
|
|
605
|
+
return "failed";
|
|
606
|
+
});
|
|
607
|
+
return send;
|
|
608
|
+
}
|
|
609
|
+
isValidationActive() {
|
|
610
|
+
return this.eventSpecFetcher !== null;
|
|
158
611
|
}
|
|
159
612
|
/**
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
* so short-lived processes (CLIs) don't exit before the send finishes — even
|
|
166
|
-
* when the caller doesn't await the returned promise.
|
|
613
|
+
* Sends every buffered event, then waits until all in-flight sends (and spec fetches
|
|
614
|
+
* that will enqueue) have completed or `timeoutMs` (default 10000) has elapsed.
|
|
615
|
+
* Resolves `true` if, when it resolves, this instance has nothing buffered, waiting or in
|
|
616
|
+
* flight (always after destroy()), and `false` if the timeout won. Never rejects. Call it
|
|
617
|
+
* before process exit or before a serverless handler returns.
|
|
167
618
|
*/
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
619
|
+
flush() {
|
|
620
|
+
return __awaiter(this, arguments, void 0, function* (timeoutMs = DEFAULT_FLUSH_TIMEOUT_MS) {
|
|
621
|
+
const budget = typeof timeoutMs === "number" && Number.isFinite(timeoutMs) && timeoutMs >= 0
|
|
622
|
+
? Math.min(timeoutMs, AvoBatchQueue_1.MAX_TIMER_MS)
|
|
623
|
+
: DEFAULT_FLUSH_TIMEOUT_MS;
|
|
624
|
+
// While an explicit flush runs, the exit drain holds the process until its deadline too.
|
|
625
|
+
const token = {};
|
|
626
|
+
AvoInspector.explicitFlushDeadlines.set(token, (0, utils_1.monotonicNowMs)() + budget);
|
|
627
|
+
try {
|
|
628
|
+
yield this.flushWithin(budget);
|
|
629
|
+
}
|
|
630
|
+
finally {
|
|
631
|
+
AvoInspector.explicitFlushDeadlines.delete(token);
|
|
632
|
+
}
|
|
633
|
+
return this.hasDrained();
|
|
634
|
+
});
|
|
172
635
|
}
|
|
173
|
-
|
|
636
|
+
// Nothing buffered, waiting for a send slot, or in flight (spec validations included).
|
|
637
|
+
hasDrained() {
|
|
638
|
+
return (this.destroyed ||
|
|
639
|
+
(this.pending.size === 0 && this.batchQueue.length === 0 && this.batchQueue.waitingLength === 0));
|
|
640
|
+
}
|
|
641
|
+
// The body of flush(): also used by the exit drain, whose own deadline is not an explicit one.
|
|
642
|
+
// Resolves whether everything it waited for settled within the budget (false if the
|
|
643
|
+
// budget ran out first).
|
|
644
|
+
flushWithin(budget) {
|
|
174
645
|
return __awaiter(this, void 0, void 0, function* () {
|
|
175
|
-
const validationResult = yield this.fetchAndValidate(eventName, eventSchema, anonymousId, rawEventProperties, eventId);
|
|
176
646
|
try {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
if (AvoInspector.shouldLog) {
|
|
180
|
-
console.log("Avo Inspector: Sending validated event " + eventName);
|
|
181
|
-
}
|
|
182
|
-
const eventProps = this.avoNetworkCallsHandler.buildEventProperties(eventSchema, rawEventProperties);
|
|
183
|
-
body = this.avoNetworkCallsHandler.bodyForValidatedEventSchemaCall(anonymousId, eventName, eventProps, eventId, eventHash, validationResult.metadata, validationResult.propertyResults);
|
|
647
|
+
if (this.destroyed) {
|
|
648
|
+
return true;
|
|
184
649
|
}
|
|
185
|
-
|
|
186
|
-
|
|
650
|
+
// Only work started before this call, plus the batch it drains, is awaited.
|
|
651
|
+
const waitFor = Array.from(this.pending);
|
|
652
|
+
waitFor.forEach((promise) => {
|
|
653
|
+
const validation = this.validations.get(promise);
|
|
654
|
+
if (validation) {
|
|
655
|
+
validation.flushRequested = true;
|
|
656
|
+
}
|
|
657
|
+
});
|
|
658
|
+
const drained = this.batchQueue.drain();
|
|
659
|
+
if (drained !== null) {
|
|
660
|
+
waitFor.push(drained);
|
|
187
661
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
const schemaString = eventSchema.map(p => '\t"' + p.propertyName + '": "' + p.propertyType + '"').join(";\n");
|
|
191
|
-
console.log("Avo Inspector: Saved event " + eventName + " with schema {\n" + schemaString + "\n}");
|
|
662
|
+
if (waitFor.length === 0) {
|
|
663
|
+
return true;
|
|
192
664
|
}
|
|
665
|
+
let timer;
|
|
666
|
+
const completed = yield Promise.race([
|
|
667
|
+
Promise.allSettled(waitFor).then(() => true),
|
|
668
|
+
new Promise((resolve) => {
|
|
669
|
+
timer = setTimeout(() => resolve(false), budget);
|
|
670
|
+
timer.unref();
|
|
671
|
+
}),
|
|
672
|
+
]);
|
|
673
|
+
clearTimeout(timer);
|
|
674
|
+
return completed;
|
|
193
675
|
}
|
|
194
|
-
catch (
|
|
195
|
-
|
|
676
|
+
catch (e) {
|
|
677
|
+
// flush() is a completion guarantee and never rejects.
|
|
678
|
+
return false;
|
|
679
|
+
}
|
|
680
|
+
finally {
|
|
681
|
+
// Counts whose window has expired are printed now; a count still inside its window
|
|
682
|
+
// stays pending, so an app that flushes after every event keeps the 10 s limit.
|
|
683
|
+
AvoLog_1.AvoLog.flushPending(true);
|
|
196
684
|
}
|
|
197
685
|
});
|
|
198
686
|
}
|
|
687
|
+
/** @internal Test-only hook for the conformance harness; not part of the public API. */
|
|
688
|
+
_setSamplingRateForTesting(samplingRate) {
|
|
689
|
+
this.avoNetworkCallsHandler._setSamplingRateForTesting(samplingRate);
|
|
690
|
+
}
|
|
691
|
+
/** Turns the SDK's diagnostic logging on or off for every instance. */
|
|
199
692
|
enableLogging(enable) {
|
|
200
693
|
AvoInspector._shouldLog = enable;
|
|
201
694
|
}
|
|
695
|
+
/** Returns the schema of `eventProperties` without tracking or sending anything. */
|
|
202
696
|
extractSchema(eventProperties, shouldLogIfEnabled = true) {
|
|
203
697
|
try {
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
698
|
+
// The scan compares against every recent Codegen entry, so it runs only when its
|
|
699
|
+
// warning can print (never on the tracking path, which passes false).
|
|
700
|
+
if (shouldLogIfEnabled &&
|
|
701
|
+
AvoInspector.shouldLog &&
|
|
702
|
+
this.avoDeduplicator.hasSeenEventParams(eventProperties, true)) {
|
|
703
|
+
console.warn("Avo Inspector: WARNING! You are trying to extract schema shape that was just reported by your Codegen. " +
|
|
704
|
+
"This is an indicator of duplicate inspector reporting. " +
|
|
705
|
+
"Please reach out to support@avo.app for advice if you are not sure how to handle this.");
|
|
210
706
|
}
|
|
211
707
|
if (AvoInspector.shouldLog) {
|
|
212
|
-
|
|
213
|
-
|
|
708
|
+
// No property values in logs; the parsed schema is logged below.
|
|
709
|
+
console.log("Avo Inspector: extracting schema");
|
|
214
710
|
}
|
|
215
711
|
const schema = AvoSchemaParser_1.AvoSchemaParser.extractSchema(eventProperties);
|
|
216
712
|
if (AvoInspector.shouldLog) {
|
|
217
|
-
|
|
218
|
-
console.log("Avo Inspector: Parsed schema {\n" + schemaString + "\n}");
|
|
713
|
+
console.log("Avo Inspector: Parsed schema " + (0, utils_1.formatSchema)(schema));
|
|
219
714
|
}
|
|
220
715
|
return schema;
|
|
221
716
|
}
|
|
222
717
|
catch (e) {
|
|
223
|
-
|
|
718
|
+
AvoLog_1.AvoLog.internal(e);
|
|
224
719
|
return [];
|
|
225
720
|
}
|
|
226
721
|
}
|
|
@@ -228,7 +723,7 @@ class AvoInspector {
|
|
|
228
723
|
* Fetch event spec and validate. Returns validation results + metadata if
|
|
229
724
|
* validation succeeds, or null if unavailable (prod, no spec, fetch error).
|
|
230
725
|
*/
|
|
231
|
-
fetchAndValidate(eventName, eventSchema, anonymousId, rawEventProperties,
|
|
726
|
+
fetchAndValidate(eventName, eventSchema, anonymousId, rawEventProperties, gatewayFields) {
|
|
232
727
|
return __awaiter(this, void 0, void 0, function* () {
|
|
233
728
|
if (!this.eventSpecFetcher || !this.eventSpecCache || !this.eventValidator) {
|
|
234
729
|
if (AvoInspector.shouldLog) {
|
|
@@ -239,7 +734,16 @@ class AvoInspector {
|
|
|
239
734
|
}
|
|
240
735
|
return null;
|
|
241
736
|
}
|
|
242
|
-
|
|
737
|
+
// The same normalized coordinates as on the track body: a gateway key answers the spec
|
|
738
|
+
// of that checkpoint, and each checkpoint's spec has its own cache entry and fetch.
|
|
739
|
+
const coordinates = {};
|
|
740
|
+
if (gatewayFields && gatewayFields.originHint !== undefined) {
|
|
741
|
+
coordinates.originHint = gatewayFields.originHint;
|
|
742
|
+
}
|
|
743
|
+
if (gatewayFields && gatewayFields.outputReference !== undefined) {
|
|
744
|
+
coordinates.outputReference = gatewayFields.outputReference;
|
|
745
|
+
}
|
|
746
|
+
const cacheKey = AvoEventSpecCache_1.AvoEventSpecCache.makeKey(this.apiKey, anonymousId, eventName, coordinates);
|
|
243
747
|
const cache = this.eventSpecCache;
|
|
244
748
|
const validator = this.eventValidator;
|
|
245
749
|
const doValidate = (specResponse) => {
|
|
@@ -249,52 +753,106 @@ class AvoInspector {
|
|
|
249
753
|
}
|
|
250
754
|
return null;
|
|
251
755
|
}
|
|
252
|
-
const
|
|
253
|
-
? { propertyValue: String(rawEventProperties[prop.propertyName]) }
|
|
254
|
-
: {}))));
|
|
756
|
+
const specEventCount = specResponse.eventSpec.events.length;
|
|
255
757
|
if (AvoInspector.shouldLog) {
|
|
256
758
|
console.log("Avo Inspector: Validating event: " + eventName
|
|
257
|
-
+ " with " +
|
|
258
|
-
+ " against " +
|
|
759
|
+
+ " with " + eventSchema.length + " properties"
|
|
760
|
+
+ " against " + specEventCount + " spec event" + (specEventCount === 1 ? "" : "s"));
|
|
259
761
|
}
|
|
260
|
-
const
|
|
261
|
-
const results = validator.validate(specResponse.eventSpec, eventProperties, validationId);
|
|
762
|
+
const results = validator.validate(specResponse.eventSpec, eventSchema, rawEventProperties);
|
|
262
763
|
if (AvoInspector.shouldLog) {
|
|
263
764
|
console.log("Avo Inspector: Validation complete for event: " + eventName
|
|
264
|
-
+ " with " + results.length + "
|
|
765
|
+
+ " with " + results.length + " failed propert" + (results.length === 1 ? "y" : "ies"));
|
|
265
766
|
}
|
|
266
767
|
return { metadata: specResponse.metadata, propertyResults: results };
|
|
267
768
|
};
|
|
268
769
|
// Check cache first
|
|
269
770
|
const cached = cache.get(cacheKey);
|
|
771
|
+
if (cached === null) {
|
|
772
|
+
if (AvoInspector.shouldLog) {
|
|
773
|
+
console.log("Avo Inspector: Event spec cache hit (empty) for event: " + eventName + ". Sending without validation.");
|
|
774
|
+
}
|
|
775
|
+
return null;
|
|
776
|
+
}
|
|
270
777
|
if (cached !== undefined) {
|
|
271
778
|
if (AvoInspector.shouldLog) {
|
|
272
779
|
console.log("Avo Inspector: Event spec cache hit for event: " + eventName);
|
|
273
780
|
}
|
|
274
781
|
return doValidate(cached);
|
|
275
782
|
}
|
|
276
|
-
// Cache miss — fetch spec (
|
|
783
|
+
// Cache miss — fetch spec (flush() awaits it via trackPending)
|
|
277
784
|
if (AvoInspector.shouldLog) {
|
|
278
785
|
console.log("Avo Inspector: Event spec cache miss for event: " + eventName + ". Fetching before sending.");
|
|
279
786
|
}
|
|
787
|
+
if (AvoInspector.waitingValidations >= MAX_WAITING_VALIDATIONS) {
|
|
788
|
+
if (AvoInspector.shouldLog) {
|
|
789
|
+
console.log("Avo Inspector: " + MAX_WAITING_VALIDATIONS + " events are already waiting for an event spec. Sending " + eventName + " without validation.");
|
|
790
|
+
}
|
|
791
|
+
return null;
|
|
792
|
+
}
|
|
793
|
+
AvoInspector.waitingValidations++;
|
|
794
|
+
let waiting = true;
|
|
795
|
+
const release = () => {
|
|
796
|
+
if (waiting) {
|
|
797
|
+
waiting = false;
|
|
798
|
+
AvoInspector.waitingValidations--;
|
|
799
|
+
this.releaseWaiting.delete(release);
|
|
800
|
+
}
|
|
801
|
+
};
|
|
802
|
+
this.releaseWaiting.add(release);
|
|
280
803
|
const fetcher = this.eventSpecFetcher;
|
|
281
|
-
return new Promise((resolve) => {
|
|
282
|
-
|
|
804
|
+
return new Promise((resolve, reject) => {
|
|
805
|
+
const onSpec = (result, notFound) => {
|
|
806
|
+
release();
|
|
283
807
|
if (result !== null) {
|
|
284
808
|
cache.set(cacheKey, result);
|
|
285
|
-
|
|
809
|
+
// A throw here would be swallowed by the fetcher and leave this promise pending;
|
|
810
|
+
// reject instead, so a miss falls back to an unvalidated send like a hit does.
|
|
811
|
+
try {
|
|
812
|
+
resolve(doValidate(result));
|
|
813
|
+
}
|
|
814
|
+
catch (err) {
|
|
815
|
+
reject(err);
|
|
816
|
+
}
|
|
286
817
|
}
|
|
287
818
|
else {
|
|
819
|
+
// Cached so a failing fetch is not repeated for every event: a 404 (no spec, such
|
|
820
|
+
// as a gateway key without originHint) for the spec TTL, any other failure briefly.
|
|
821
|
+
cache.set(cacheKey, null, notFound === true ? undefined : AvoEventSpecCache_1.TRANSIENT_FAILURE_TTL_MS);
|
|
288
822
|
if (AvoInspector.shouldLog) {
|
|
289
823
|
console.log("Avo Inspector: Event spec fetch returned null for event: " + eventName + ". Cached empty response. Sending without validation.");
|
|
290
824
|
}
|
|
291
825
|
resolve(null);
|
|
292
826
|
}
|
|
293
|
-
}
|
|
827
|
+
};
|
|
828
|
+
try {
|
|
829
|
+
fetcher.fetch(eventName, anonymousId, onSpec, coordinates);
|
|
830
|
+
}
|
|
831
|
+
catch (err) {
|
|
832
|
+
// No callback will come: free the slot and fall back to an unvalidated send.
|
|
833
|
+
release();
|
|
834
|
+
reject(err);
|
|
835
|
+
}
|
|
294
836
|
});
|
|
295
837
|
});
|
|
296
838
|
}
|
|
839
|
+
/**
|
|
840
|
+
* Cancels and cleans up: discards the pending batch unsent, abandons in-flight
|
|
841
|
+
* requests and stops the flush timer. Does not flush. After destroy() the instance
|
|
842
|
+
* is terminated and trackSchemaFromEvent resolves [] without sending; a call still
|
|
843
|
+
* waiting on a spec fetch or on its immediate send also resolves [].
|
|
844
|
+
*/
|
|
297
845
|
destroy() {
|
|
846
|
+
this.destroyed = true;
|
|
847
|
+
const waiters = Array.from(this.destroyWaiters);
|
|
848
|
+
this.destroyWaiters.clear();
|
|
849
|
+
waiters.forEach((settle) => settle());
|
|
850
|
+
this.batchQueue.clear();
|
|
851
|
+
this.pending.clear();
|
|
852
|
+
this.validations.clear();
|
|
853
|
+
Array.from(this.releaseWaiting).forEach((release) => release());
|
|
854
|
+
this.updateExitDrain();
|
|
855
|
+
this.avoNetworkCallsHandler.abortInFlight();
|
|
298
856
|
if (this.eventSpecFetcher) {
|
|
299
857
|
this.eventSpecFetcher.destroy();
|
|
300
858
|
this.eventSpecFetcher = null;
|
|
@@ -304,12 +862,122 @@ class AvoInspector {
|
|
|
304
862
|
this.eventSpecCache = null;
|
|
305
863
|
}
|
|
306
864
|
this.eventValidator = null;
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
this.keepAliveTimer = null;
|
|
310
|
-
}
|
|
311
|
-
this.pendingCount = 0;
|
|
865
|
+
// Destroy is final: report every pending count now.
|
|
866
|
+
AvoLog_1.AvoLog.flushPending();
|
|
312
867
|
}
|
|
313
868
|
}
|
|
314
869
|
exports.AvoInspector = AvoInspector;
|
|
870
|
+
// Best-effort delivery at exit: when the event loop empties ("beforeExit"), every
|
|
871
|
+
// instance with buffered or in-flight events is flushed. One listener serves all
|
|
872
|
+
// instances, and only instances with work are registered, so idle instances are not
|
|
873
|
+
// retained. The listener schedules nothing when there is nothing to send, so it never
|
|
874
|
+
// keeps the process alive on its own. "beforeExit" does not fire on process.exit() or
|
|
875
|
+
// on signals; callers flush() there.
|
|
876
|
+
AvoInspector.instancesWithWork = new Set();
|
|
877
|
+
AvoInspector.exitDrainArmed = false;
|
|
878
|
+
AvoInspector.exitReportArmed = false;
|
|
879
|
+
// One deadline for the whole exit, however often "beforeExit" fires during it. Cleared
|
|
880
|
+
// once every instance has drained, in case the process carries on.
|
|
881
|
+
AvoInspector.exitDeadline = null;
|
|
882
|
+
// Events waiting for a spec fetch, across every instance (at most MAX_WAITING_VALIDATIONS).
|
|
883
|
+
AvoInspector.waitingValidations = 0;
|
|
884
|
+
// When any instance last tracked an event (monotonic clock).
|
|
885
|
+
AvoInspector.lastTrackAt = -Infinity;
|
|
886
|
+
// The exit deadline whose validation grace has been used.
|
|
887
|
+
AvoInspector.validationGraceUsedFor = null;
|
|
888
|
+
// Deadlines of explicit flush() calls still running, by call.
|
|
889
|
+
AvoInspector.explicitFlushDeadlines = new Map();
|
|
890
|
+
AvoInspector.drainOnExit = () => {
|
|
891
|
+
AvoInspector.exitDrainArmed = false;
|
|
892
|
+
const instances = Array.from(AvoInspector.instancesWithWork);
|
|
893
|
+
if (instances.length === 0) {
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
const now = (0, utils_1.monotonicNowMs)();
|
|
897
|
+
// One deadline per exit: "beforeExit" re-fires while the same exit continues (also when
|
|
898
|
+
// the app's own beforeExit work keeps the loop alive past the deadline), so a hung
|
|
899
|
+
// endpoint cannot stretch it. Only a track after the deadline passed shows the process
|
|
900
|
+
// carried on from that exit; the next one then gets a new deadline.
|
|
901
|
+
const deadlinePassed = AvoInspector.exitDeadline !== null && now > AvoInspector.exitDeadline;
|
|
902
|
+
if (AvoInspector.exitDeadline === null ||
|
|
903
|
+
(deadlinePassed && AvoInspector.lastTrackAt > AvoInspector.exitDeadline)) {
|
|
904
|
+
AvoInspector.exitDeadline = now + DEFAULT_FLUSH_TIMEOUT_MS;
|
|
905
|
+
}
|
|
906
|
+
// An explicit flush() still running gets its full deadline, even past the drain's own.
|
|
907
|
+
let deadline = AvoInspector.exitDeadline;
|
|
908
|
+
AvoInspector.explicitFlushDeadlines.forEach((flushDeadline) => {
|
|
909
|
+
deadline = Math.max(deadline, flushDeadline);
|
|
910
|
+
});
|
|
911
|
+
const remaining = deadline - now;
|
|
912
|
+
if (remaining <= 0) {
|
|
913
|
+
// Out of time: let the process exit; what is still unsent is reported at the exit.
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
916
|
+
// Spec-fetch sockets are unref'd, so "beforeExit" also fires while a script awaits a
|
|
917
|
+
// track whose spec is being fetched. Once per deadline, give pending validations a short
|
|
918
|
+
// grace, sending nothing: with a working spec endpoint they settle well within it, the
|
|
919
|
+
// script carries on and its batches stay whole. Settled in time with no fetch given up
|
|
920
|
+
// counts as a completed idle point (the deadline is renewed); otherwise the grace is
|
|
921
|
+
// spent and the next "beforeExit" drains.
|
|
922
|
+
const validations = instances.reduce((all, inspector) => all.concat(Array.from(inspector.validationOutcomes)), []);
|
|
923
|
+
if (validations.length > 0 && AvoInspector.validationGraceUsedFor !== AvoInspector.exitDeadline) {
|
|
924
|
+
AvoInspector.validationGraceUsedFor = AvoInspector.exitDeadline;
|
|
925
|
+
const exitDeadline = AvoInspector.exitDeadline;
|
|
926
|
+
const timeoutsBefore = AvoEventSpecFetcher_1.AvoEventSpecFetcher.timeouts;
|
|
927
|
+
let grace;
|
|
928
|
+
const graceOver = new Promise((resolve) => {
|
|
929
|
+
grace = setTimeout(() => resolve(false), Math.min(VALIDATION_GRACE_MS, remaining));
|
|
930
|
+
});
|
|
931
|
+
Promise.race([Promise.allSettled(validations).then(() => true), graceOver]).then((settled) => {
|
|
932
|
+
clearTimeout(grace);
|
|
933
|
+
if (settled && (0, utils_1.monotonicNowMs)() < exitDeadline && AvoEventSpecFetcher_1.AvoEventSpecFetcher.timeouts === timeoutsBefore) {
|
|
934
|
+
AvoInspector.exitDeadline = null;
|
|
935
|
+
}
|
|
936
|
+
});
|
|
937
|
+
AvoInspector.armExitDrain();
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
// Request sockets are unref'd, so this timer is what keeps the process alive while
|
|
941
|
+
// everything left is sent at once (within the in-flight cap). Every instance drains
|
|
942
|
+
// what is ready now; events still being validated follow as they are validated, and
|
|
943
|
+
// halfway through the budget the validations still pending are given up, so those
|
|
944
|
+
// events go out unvalidated within the rest of it.
|
|
945
|
+
const keepAlive = setTimeout(() => { }, remaining);
|
|
946
|
+
const giveUp = setTimeout(() => {
|
|
947
|
+
instances.forEach((inspector) => inspector.giveUpValidations());
|
|
948
|
+
}, remaining / 2);
|
|
949
|
+
giveUp.unref();
|
|
950
|
+
Promise.all(instances.map((inspector) => inspector.flushWithin(remaining))).then((completed) => {
|
|
951
|
+
clearTimeout(keepAlive);
|
|
952
|
+
clearTimeout(giveUp);
|
|
953
|
+
// Finished before the budget ran out: the next "beforeExit" starts a new exit with a
|
|
954
|
+
// fresh deadline. Decided by which won the race, not by reading the clock: under load
|
|
955
|
+
// a timer can fire while the monotonic clock still reads just short of the deadline,
|
|
956
|
+
// and a timed-out drain would then be renewed for another 10 s.
|
|
957
|
+
if (completed.every((done) => done)) {
|
|
958
|
+
AvoInspector.exitDeadline = null;
|
|
959
|
+
}
|
|
960
|
+
});
|
|
961
|
+
// Re-arm so work added during the drain is sent at the next "beforeExit".
|
|
962
|
+
AvoInspector.armExitDrain();
|
|
963
|
+
};
|
|
964
|
+
// At the real exit (natural, or process.exit(); signals do not emit it), reports what
|
|
965
|
+
// every instance with work still holds, synchronously: events never sent (buffered,
|
|
966
|
+
// waiting for a slot, or still being validated) and events in sends not yet completed.
|
|
967
|
+
AvoInspector.reportOnExit = () => {
|
|
968
|
+
let unsent = 0;
|
|
969
|
+
let unconfirmed = 0;
|
|
970
|
+
AvoInspector.instancesWithWork.forEach((inspector) => {
|
|
971
|
+
unsent += inspector.batchQueue.length + inspector.batchQueue.waitingLength + inspector.validationOutcomes.size;
|
|
972
|
+
unconfirmed += inspector.batchQueue.inFlightEvents;
|
|
973
|
+
});
|
|
974
|
+
AvoLog_1.AvoLog.enterExit();
|
|
975
|
+
if (unsent > 0) {
|
|
976
|
+
AvoLog_1.AvoLog.dropped(unsent, "unsent at exit");
|
|
977
|
+
}
|
|
978
|
+
if (unconfirmed > 0) {
|
|
979
|
+
AvoLog_1.AvoLog.dropped(unconfirmed, "unconfirmed at exit");
|
|
980
|
+
}
|
|
981
|
+
AvoLog_1.AvoLog.flushPending();
|
|
982
|
+
};
|
|
315
983
|
AvoInspector._shouldLog = false;
|