node-avo-inspector 2.0.0-beta.1 → 2.0.0-beta.2
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/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/README.md
CHANGED
|
@@ -318,6 +318,8 @@ To test against a local mock server, set the `AVO_INSPECTOR_MOCK_ENDPOINT` envir
|
|
|
318
318
|
|
|
319
319
|
Update the `VERSION` constant in `src/AvoInspectorVersion.ts` and the `version` in `package.json` on every release. `VERSION` is sent to Avo as `libVersion`, and a unit test fails if the two differ.
|
|
320
320
|
|
|
321
|
+
`npm pack` and `npm publish` run the `prepack` script, which deletes `dist/` and rebuilds it from `src/`, so a stale local `dist/` (it is git-ignored) is not packed. This needs npm lifecycle scripts enabled: with `ignore-scripts` set, `prepack` is skipped and whatever is in `dist/` is packed. Before publishing, run `npm pack --dry-run` and check that it prints the `prepack` build and lists the files you expect.
|
|
322
|
+
|
|
321
323
|
## Conformance suite
|
|
322
324
|
|
|
323
325
|
`scripts/run-conformance.sh` builds the SDK and runs the spec's conformance suite (36 fixtures) against the harness in `conformance/avo-inspector-conformance.js` (runner contract 1.1.0). It fetches the spec at the pinned commit into `.spec-repo/`; set `SPEC_DIR` to use a local checkout instead.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { InspectorBody } from "./AvoNetworkCallsHandler";
|
|
2
|
+
export declare const MAX_TIMER_MS = 2147483647;
|
|
3
|
+
export declare const MAX_IN_FLIGHT_SENDS = 4;
|
|
4
|
+
export declare const MAX_WAITING_EVENTS = 10000;
|
|
5
|
+
export declare const BACKPRESSURE_WAITING_EVENTS = 1000;
|
|
6
|
+
export declare const BACKPRESSURE_MAX_WAIT_MS = 10000;
|
|
7
|
+
export interface AvoBatchOptions {
|
|
8
|
+
batchSize: number;
|
|
9
|
+
batchFlushSeconds: number;
|
|
10
|
+
maxQueueSize: number;
|
|
11
|
+
disableBatchTimer: boolean;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* In-memory pending batch buffer. Node runs this on a single thread, so appending and
|
|
15
|
+
* the swap-and-clear in drain() are atomic without a lock; the send itself happens in
|
|
16
|
+
* the dispatch callback, after the buffer has been swapped out. At most
|
|
17
|
+
* MAX_IN_FLIGHT_SENDS dispatches run at once; later batches wait in order.
|
|
18
|
+
*/
|
|
19
|
+
export declare class AvoBatchQueue<T> {
|
|
20
|
+
private options;
|
|
21
|
+
private dispatch;
|
|
22
|
+
private dropped;
|
|
23
|
+
private track;
|
|
24
|
+
private buffer;
|
|
25
|
+
private waiting;
|
|
26
|
+
private waitingEvents;
|
|
27
|
+
private inFlight;
|
|
28
|
+
private inFlightEventCount;
|
|
29
|
+
private capacityWaiters;
|
|
30
|
+
private capacityTimer;
|
|
31
|
+
private flushTimer;
|
|
32
|
+
private bufferOutcome;
|
|
33
|
+
/**
|
|
34
|
+
* @param dispatch sends one batch; must not reject.
|
|
35
|
+
* @param dropped the outcome for a batch discarded before it was sent.
|
|
36
|
+
* @param track called with each batch's outcome promise as soon as it is swapped out.
|
|
37
|
+
*/
|
|
38
|
+
constructor(options: AvoBatchOptions, dispatch: (batch: Array<InspectorBody>) => Promise<T>, dropped: T, track?: (outcome: Promise<T>) => Promise<T>);
|
|
39
|
+
/** Events in the unsent buffer (bounded by maxQueueSize). */
|
|
40
|
+
get length(): number;
|
|
41
|
+
/** Events in batches waiting for a send slot (bounded by MAX_WAITING_EVENTS). */
|
|
42
|
+
get waitingLength(): number;
|
|
43
|
+
/** Events in batches being sent. */
|
|
44
|
+
get inFlightEvents(): number;
|
|
45
|
+
/**
|
|
46
|
+
* Appends an event and evaluates the size trigger. Returns the send promise when
|
|
47
|
+
* this call dispatched a batch, otherwise null.
|
|
48
|
+
*/
|
|
49
|
+
enqueue(event: InspectorBody): Promise<T> | null;
|
|
50
|
+
/**
|
|
51
|
+
* The outcome of the batch that will carry the events buffered now, whichever drain (size
|
|
52
|
+
* trigger, timer, flush) swaps them out. Null when the buffer is empty.
|
|
53
|
+
*/
|
|
54
|
+
bufferedBatchOutcome(): Promise<T> | null;
|
|
55
|
+
/** Swaps out every buffered event and dispatches them as one batch. */
|
|
56
|
+
drain(): Promise<T> | null;
|
|
57
|
+
/**
|
|
58
|
+
* Resolves once fewer than BACKPRESSURE_WAITING_EVENTS events wait for a send slot (at
|
|
59
|
+
* once if they already do), when the backlog overflows, and at the latest after
|
|
60
|
+
* BACKPRESSURE_MAX_WAIT_MS. clear() resolves it too.
|
|
61
|
+
*/
|
|
62
|
+
whenBelowBackpressure(): Promise<void>;
|
|
63
|
+
/** Tracks waiting on backpressure. */
|
|
64
|
+
get capacityWaiterCount(): number;
|
|
65
|
+
private releaseCapacityWaiters;
|
|
66
|
+
/** Discards every buffered or waiting event unsent and cancels the scheduled flush. */
|
|
67
|
+
clear(): void;
|
|
68
|
+
private startSends;
|
|
69
|
+
private dropOldestWaiting;
|
|
70
|
+
private scheduleFlush;
|
|
71
|
+
private clearTimer;
|
|
72
|
+
get hasScheduledFlush(): boolean;
|
|
73
|
+
}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AvoBatchQueue = exports.BACKPRESSURE_MAX_WAIT_MS = exports.BACKPRESSURE_WAITING_EVENTS = exports.MAX_WAITING_EVENTS = exports.MAX_IN_FLIGHT_SENDS = exports.MAX_TIMER_MS = void 0;
|
|
4
|
+
const AvoLog_1 = require("./AvoLog");
|
|
5
|
+
// setTimeout fires almost at once for delays above this (2^31 - 1 ms, about 24.8 days).
|
|
6
|
+
exports.MAX_TIMER_MS = 2147483647;
|
|
7
|
+
// Batch sends in flight at once. Batches swapped out beyond this wait for a free slot, so
|
|
8
|
+
// a fast producer or a slow endpoint cannot open an unbounded number of requests.
|
|
9
|
+
exports.MAX_IN_FLIGHT_SENDS = 4;
|
|
10
|
+
// Events that may wait for a send slot, across all waiting batches. Separate from
|
|
11
|
+
// maxQueueSize, which bounds only the unsent buffer; past this the oldest waiting events
|
|
12
|
+
// are dropped.
|
|
13
|
+
exports.MAX_WAITING_EVENTS = 10000;
|
|
14
|
+
// Backpressure: from this many events waiting for a send slot, an awaited track waits until
|
|
15
|
+
// fewer wait (see whenBelowBackpressure), so awaited loops slow to the speed of the sends.
|
|
16
|
+
exports.BACKPRESSURE_WAITING_EVENTS = 1000;
|
|
17
|
+
// The longest a track waits on backpressure: one request timeout.
|
|
18
|
+
exports.BACKPRESSURE_MAX_WAIT_MS = 10000;
|
|
19
|
+
/**
|
|
20
|
+
* In-memory pending batch buffer. Node runs this on a single thread, so appending and
|
|
21
|
+
* the swap-and-clear in drain() are atomic without a lock; the send itself happens in
|
|
22
|
+
* the dispatch callback, after the buffer has been swapped out. At most
|
|
23
|
+
* MAX_IN_FLIGHT_SENDS dispatches run at once; later batches wait in order.
|
|
24
|
+
*/
|
|
25
|
+
class AvoBatchQueue {
|
|
26
|
+
/**
|
|
27
|
+
* @param dispatch sends one batch; must not reject.
|
|
28
|
+
* @param dropped the outcome for a batch discarded before it was sent.
|
|
29
|
+
* @param track called with each batch's outcome promise as soon as it is swapped out.
|
|
30
|
+
*/
|
|
31
|
+
constructor(options, dispatch, dropped, track = (outcome) => outcome) {
|
|
32
|
+
this.options = options;
|
|
33
|
+
this.dispatch = dispatch;
|
|
34
|
+
this.dropped = dropped;
|
|
35
|
+
this.track = track;
|
|
36
|
+
this.buffer = [];
|
|
37
|
+
this.waiting = [];
|
|
38
|
+
this.waitingEvents = 0;
|
|
39
|
+
this.inFlight = 0;
|
|
40
|
+
this.inFlightEventCount = 0;
|
|
41
|
+
// Resolved once fewer than BACKPRESSURE_WAITING_EVENTS events wait, when the backlog
|
|
42
|
+
// overflows, when the wait timer fires, or by clear().
|
|
43
|
+
this.capacityWaiters = [];
|
|
44
|
+
// Ref'd while there are waiters: a stalled awaited loop is pending work, so "beforeExit"
|
|
45
|
+
// does not fire in the middle of it. Fires after BACKPRESSURE_MAX_WAIT_MS at the latest.
|
|
46
|
+
this.capacityTimer = null;
|
|
47
|
+
this.flushTimer = null;
|
|
48
|
+
// Settled with the outcome of the batch that takes the current buffer, whichever drain
|
|
49
|
+
// swaps it out (or `dropped` if clear() discards it). Created on demand.
|
|
50
|
+
this.bufferOutcome = null;
|
|
51
|
+
}
|
|
52
|
+
/** Events in the unsent buffer (bounded by maxQueueSize). */
|
|
53
|
+
get length() {
|
|
54
|
+
return this.buffer.length;
|
|
55
|
+
}
|
|
56
|
+
/** Events in batches waiting for a send slot (bounded by MAX_WAITING_EVENTS). */
|
|
57
|
+
get waitingLength() {
|
|
58
|
+
return this.waitingEvents;
|
|
59
|
+
}
|
|
60
|
+
/** Events in batches being sent. */
|
|
61
|
+
get inFlightEvents() {
|
|
62
|
+
return this.inFlightEventCount;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Appends an event and evaluates the size trigger. Returns the send promise when
|
|
66
|
+
* this call dispatched a batch, otherwise null.
|
|
67
|
+
*/
|
|
68
|
+
enqueue(event) {
|
|
69
|
+
this.buffer.push(event);
|
|
70
|
+
const overflow = this.buffer.length - this.options.maxQueueSize;
|
|
71
|
+
if (overflow > 0) {
|
|
72
|
+
this.buffer.splice(0, overflow);
|
|
73
|
+
AvoLog_1.AvoLog.dropped(overflow, "queue full");
|
|
74
|
+
}
|
|
75
|
+
if (this.buffer.length >= this.options.batchSize) {
|
|
76
|
+
return this.drain();
|
|
77
|
+
}
|
|
78
|
+
this.scheduleFlush();
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* The outcome of the batch that will carry the events buffered now, whichever drain (size
|
|
83
|
+
* trigger, timer, flush) swaps them out. Null when the buffer is empty.
|
|
84
|
+
*/
|
|
85
|
+
bufferedBatchOutcome() {
|
|
86
|
+
if (this.buffer.length === 0) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
if (this.bufferOutcome === null) {
|
|
90
|
+
let settle;
|
|
91
|
+
const promise = new Promise((resolve) => (settle = resolve));
|
|
92
|
+
this.bufferOutcome = { promise, settle };
|
|
93
|
+
}
|
|
94
|
+
return this.bufferOutcome.promise;
|
|
95
|
+
}
|
|
96
|
+
/** Swaps out every buffered event and dispatches them as one batch. */
|
|
97
|
+
drain() {
|
|
98
|
+
this.clearTimer();
|
|
99
|
+
if (this.buffer.length === 0) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
const batch = this.buffer;
|
|
103
|
+
this.buffer = [];
|
|
104
|
+
const outcome = new Promise((settle) => {
|
|
105
|
+
this.waiting.push({ events: batch, settle });
|
|
106
|
+
});
|
|
107
|
+
if (this.bufferOutcome !== null) {
|
|
108
|
+
this.bufferOutcome.settle(outcome);
|
|
109
|
+
this.bufferOutcome = null;
|
|
110
|
+
}
|
|
111
|
+
this.waitingEvents += batch.length;
|
|
112
|
+
this.startSends();
|
|
113
|
+
const excess = this.waitingEvents - exports.MAX_WAITING_EVENTS;
|
|
114
|
+
if (excess > 0) {
|
|
115
|
+
this.dropOldestWaiting(excess);
|
|
116
|
+
AvoLog_1.AvoLog.dropped(excess, "send backlog full");
|
|
117
|
+
// An awaited loop stalls near BACKPRESSURE_WAITING_EVENTS, so an overflow means the
|
|
118
|
+
// callers are not awaiting: release them all. Calls that are not awaited therefore
|
|
119
|
+
// hold at most the waiters registered since the last overflow (under one batch),
|
|
120
|
+
// while an awaited loop that reaches the cap is still throttled between overflows.
|
|
121
|
+
this.releaseCapacityWaiters(true);
|
|
122
|
+
}
|
|
123
|
+
return this.track(outcome);
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Resolves once fewer than BACKPRESSURE_WAITING_EVENTS events wait for a send slot (at
|
|
127
|
+
* once if they already do), when the backlog overflows, and at the latest after
|
|
128
|
+
* BACKPRESSURE_MAX_WAIT_MS. clear() resolves it too.
|
|
129
|
+
*/
|
|
130
|
+
whenBelowBackpressure() {
|
|
131
|
+
if (this.waitingEvents < exports.BACKPRESSURE_WAITING_EVENTS) {
|
|
132
|
+
return Promise.resolve();
|
|
133
|
+
}
|
|
134
|
+
return new Promise((resolve) => {
|
|
135
|
+
this.capacityWaiters.push(resolve);
|
|
136
|
+
if (this.capacityTimer === null) {
|
|
137
|
+
this.capacityTimer = setTimeout(() => {
|
|
138
|
+
this.capacityTimer = null;
|
|
139
|
+
this.releaseCapacityWaiters(true);
|
|
140
|
+
}, exports.BACKPRESSURE_MAX_WAIT_MS);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
/** Tracks waiting on backpressure. */
|
|
145
|
+
get capacityWaiterCount() {
|
|
146
|
+
return this.capacityWaiters.length;
|
|
147
|
+
}
|
|
148
|
+
releaseCapacityWaiters(all = false) {
|
|
149
|
+
if (!all && this.waitingEvents >= exports.BACKPRESSURE_WAITING_EVENTS) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (this.capacityTimer !== null) {
|
|
153
|
+
clearTimeout(this.capacityTimer);
|
|
154
|
+
this.capacityTimer = null;
|
|
155
|
+
}
|
|
156
|
+
const waiters = this.capacityWaiters;
|
|
157
|
+
this.capacityWaiters = [];
|
|
158
|
+
waiters.forEach((resolve) => resolve());
|
|
159
|
+
}
|
|
160
|
+
/** Discards every buffered or waiting event unsent and cancels the scheduled flush. */
|
|
161
|
+
clear() {
|
|
162
|
+
this.clearTimer();
|
|
163
|
+
this.buffer = [];
|
|
164
|
+
if (this.bufferOutcome !== null) {
|
|
165
|
+
this.bufferOutcome.settle(this.dropped);
|
|
166
|
+
this.bufferOutcome = null;
|
|
167
|
+
}
|
|
168
|
+
const discarded = this.waiting;
|
|
169
|
+
this.waiting = [];
|
|
170
|
+
this.waitingEvents = 0;
|
|
171
|
+
discarded.forEach((batch) => batch.settle(this.dropped));
|
|
172
|
+
this.releaseCapacityWaiters(true);
|
|
173
|
+
}
|
|
174
|
+
startSends() {
|
|
175
|
+
while (this.inFlight < exports.MAX_IN_FLIGHT_SENDS && this.waiting.length > 0) {
|
|
176
|
+
const batch = this.waiting.shift();
|
|
177
|
+
this.waitingEvents -= batch.events.length;
|
|
178
|
+
this.inFlight++;
|
|
179
|
+
this.inFlightEventCount += batch.events.length;
|
|
180
|
+
const done = (outcome) => {
|
|
181
|
+
this.inFlight--;
|
|
182
|
+
this.inFlightEventCount -= batch.events.length;
|
|
183
|
+
batch.settle(outcome);
|
|
184
|
+
this.startSends();
|
|
185
|
+
};
|
|
186
|
+
// A dispatch that throws or rejects is an internal error: its events are lost, so both
|
|
187
|
+
// are reported. The slot is still freed and the batch still settles.
|
|
188
|
+
const failedInternally = (err) => {
|
|
189
|
+
AvoLog_1.AvoLog.internal(err);
|
|
190
|
+
AvoLog_1.AvoLog.dropped(batch.events.length, "internal error");
|
|
191
|
+
done(this.dropped);
|
|
192
|
+
};
|
|
193
|
+
let sent;
|
|
194
|
+
try {
|
|
195
|
+
sent = this.dispatch(batch.events);
|
|
196
|
+
}
|
|
197
|
+
catch (err) {
|
|
198
|
+
// Settled asynchronously, like any other dispatch outcome.
|
|
199
|
+
sent = Promise.reject(err);
|
|
200
|
+
}
|
|
201
|
+
sent.then(done, failedInternally);
|
|
202
|
+
}
|
|
203
|
+
this.releaseCapacityWaiters();
|
|
204
|
+
}
|
|
205
|
+
dropOldestWaiting(count) {
|
|
206
|
+
while (count > 0 && this.waiting.length > 0) {
|
|
207
|
+
const batch = this.waiting[0];
|
|
208
|
+
const removed = batch.events.splice(0, Math.min(count, batch.events.length)).length;
|
|
209
|
+
this.waitingEvents -= removed;
|
|
210
|
+
count -= removed;
|
|
211
|
+
if (batch.events.length === 0) {
|
|
212
|
+
this.waiting.shift();
|
|
213
|
+
batch.settle(this.dropped);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
// Fires once the oldest buffered event is batchFlushSeconds old. Unref'd so it never
|
|
218
|
+
// holds the process open.
|
|
219
|
+
scheduleFlush() {
|
|
220
|
+
if (this.options.disableBatchTimer || this.flushTimer !== null) {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
this.flushTimer = setTimeout(() => {
|
|
224
|
+
this.flushTimer = null;
|
|
225
|
+
this.drain();
|
|
226
|
+
}, Math.min(this.options.batchFlushSeconds * 1000, exports.MAX_TIMER_MS));
|
|
227
|
+
this.flushTimer.unref();
|
|
228
|
+
}
|
|
229
|
+
clearTimer() {
|
|
230
|
+
if (this.flushTimer !== null) {
|
|
231
|
+
clearTimeout(this.flushTimer);
|
|
232
|
+
this.flushTimer = null;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
get hasScheduledFlush() {
|
|
236
|
+
return this.flushTimer !== null;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
exports.AvoBatchQueue = AvoBatchQueue;
|
|
@@ -1,11 +1,18 @@
|
|
|
1
|
+
interface Registration {
|
|
2
|
+
time: number;
|
|
3
|
+
key: string;
|
|
4
|
+
generation: number;
|
|
5
|
+
}
|
|
1
6
|
export declare class AvoDeduplicator {
|
|
2
|
-
avoFunctionsEvents:
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
[time: number]: string;
|
|
7
|
-
};
|
|
7
|
+
avoFunctionsEvents: Array<Registration>;
|
|
8
|
+
manualEvents: Array<Registration>;
|
|
9
|
+
private avoFunctionsHead;
|
|
10
|
+
private manualHead;
|
|
8
11
|
private msToConsiderOld;
|
|
12
|
+
private now;
|
|
13
|
+
private avoFunctionsLatest;
|
|
14
|
+
private manualLatest;
|
|
15
|
+
private nextGeneration;
|
|
9
16
|
avoFunctionsEventsParams: {
|
|
10
17
|
[key: string]: {
|
|
11
18
|
[propName: string]: any;
|
|
@@ -27,5 +34,7 @@ export declare class AvoDeduplicator {
|
|
|
27
34
|
}, checkInAvoFunctions: boolean): boolean;
|
|
28
35
|
private lookForEventParamsIn;
|
|
29
36
|
private clearOldEvents;
|
|
37
|
+
private expire;
|
|
30
38
|
private _clearEvents;
|
|
31
39
|
}
|
|
40
|
+
export {};
|
package/dist/AvoDeduplicator.js
CHANGED
|
@@ -4,9 +4,21 @@ exports.AvoDeduplicator = void 0;
|
|
|
4
4
|
const utils_1 = require("./utils");
|
|
5
5
|
class AvoDeduplicator {
|
|
6
6
|
constructor() {
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
// Every registration, oldest first; `head` is the first one not yet expired. Cleanup pops
|
|
8
|
+
// from the front, so its cost is amortised O(1) per call.
|
|
9
|
+
this.avoFunctionsEvents = [];
|
|
10
|
+
this.manualEvents = [];
|
|
11
|
+
this.avoFunctionsHead = 0;
|
|
12
|
+
this.manualHead = 0;
|
|
9
13
|
this.msToConsiderOld = 500;
|
|
14
|
+
// Milliseconds from a monotonic clock: the log must stay in time order, and a wall clock
|
|
15
|
+
// stepping back (NTP, manual change) would stall expiry. Replaceable in tests.
|
|
16
|
+
this.now = utils_1.monotonicNowMs;
|
|
17
|
+
// The generation of each key's latest registration. A key's params always belong to its
|
|
18
|
+
// latest registration, so only that registration's expiry may delete them.
|
|
19
|
+
this.avoFunctionsLatest = {};
|
|
20
|
+
this.manualLatest = {};
|
|
21
|
+
this.nextGeneration = 0;
|
|
10
22
|
// Keyed by streamId\0eventName to prevent cross-stream suppression on server
|
|
11
23
|
this.avoFunctionsEventsParams = {};
|
|
12
24
|
this.manualEventsParams = {};
|
|
@@ -17,12 +29,15 @@ class AvoDeduplicator {
|
|
|
17
29
|
shouldRegisterEvent(eventName, params, fromAvoFunction, streamId = "") {
|
|
18
30
|
this.clearOldEvents();
|
|
19
31
|
const key = AvoDeduplicator.dedupKey(eventName, streamId);
|
|
32
|
+
const registration = { time: this.now(), key, generation: this.nextGeneration++ };
|
|
20
33
|
if (fromAvoFunction) {
|
|
21
|
-
this.avoFunctionsEvents
|
|
34
|
+
this.avoFunctionsEvents.push(registration);
|
|
35
|
+
this.avoFunctionsLatest[key] = registration.generation;
|
|
22
36
|
this.avoFunctionsEventsParams[key] = params;
|
|
23
37
|
}
|
|
24
38
|
else {
|
|
25
|
-
this.manualEvents
|
|
39
|
+
this.manualEvents.push(registration);
|
|
40
|
+
this.manualLatest[key] = registration.generation;
|
|
26
41
|
this.manualEventsParams[key] = params;
|
|
27
42
|
}
|
|
28
43
|
let checkInAvoFunctions = !fromAvoFunction;
|
|
@@ -81,34 +96,41 @@ class AvoDeduplicator {
|
|
|
81
96
|
return false;
|
|
82
97
|
}
|
|
83
98
|
clearOldEvents() {
|
|
84
|
-
const now =
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
delete this.avoFunctionsEvents[time];
|
|
91
|
-
delete this.avoFunctionsEventsParams[key];
|
|
92
|
-
}
|
|
93
|
-
}
|
|
99
|
+
const now = this.now();
|
|
100
|
+
this.avoFunctionsHead = this.expire(this.avoFunctionsEvents, this.avoFunctionsHead, this.avoFunctionsEventsParams, this.avoFunctionsLatest, now);
|
|
101
|
+
this.manualHead = this.expire(this.manualEvents, this.manualHead, this.manualEventsParams, this.manualLatest, now);
|
|
102
|
+
if (this.avoFunctionsHead > 1024 && this.avoFunctionsHead * 2 > this.avoFunctionsEvents.length) {
|
|
103
|
+
this.avoFunctionsEvents = this.avoFunctionsEvents.slice(this.avoFunctionsHead);
|
|
104
|
+
this.avoFunctionsHead = 0;
|
|
94
105
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
106
|
+
if (this.manualHead > 1024 && this.manualHead * 2 > this.manualEvents.length) {
|
|
107
|
+
this.manualEvents = this.manualEvents.slice(this.manualHead);
|
|
108
|
+
this.manualHead = 0;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// Pops every registration older than msToConsiderOld and deletes a key's params only when
|
|
112
|
+
// the popped registration is still that key's latest; returns the new head.
|
|
113
|
+
expire(registrations, head, paramsByKey, latestByKey, now) {
|
|
114
|
+
while (head < registrations.length && now - registrations[head].time > this.msToConsiderOld) {
|
|
115
|
+
const { key, generation } = registrations[head];
|
|
116
|
+
if (latestByKey[key] === generation) {
|
|
117
|
+
delete paramsByKey[key];
|
|
118
|
+
delete latestByKey[key];
|
|
103
119
|
}
|
|
120
|
+
head++;
|
|
104
121
|
}
|
|
122
|
+
return head;
|
|
105
123
|
}
|
|
106
124
|
// used in tests
|
|
107
125
|
_clearEvents() {
|
|
108
|
-
this.avoFunctionsEvents =
|
|
109
|
-
this.manualEvents =
|
|
126
|
+
this.avoFunctionsEvents = [];
|
|
127
|
+
this.manualEvents = [];
|
|
128
|
+
this.avoFunctionsHead = 0;
|
|
129
|
+
this.manualHead = 0;
|
|
110
130
|
this.avoFunctionsEventsParams = {};
|
|
111
131
|
this.manualEventsParams = {};
|
|
132
|
+
this.avoFunctionsLatest = {};
|
|
133
|
+
this.manualLatest = {};
|
|
112
134
|
}
|
|
113
135
|
}
|
|
114
136
|
exports.AvoDeduplicator = AvoDeduplicator;
|
package/dist/AvoInspector.d.ts
CHANGED
|
@@ -1,6 +1,41 @@
|
|
|
1
1
|
import { AvoInspectorEnvValueType } from "./AvoInspectorEnv";
|
|
2
2
|
import { AvoNetworkCallsHandler } from "./AvoNetworkCallsHandler";
|
|
3
3
|
import { AvoDeduplicator } from "./AvoDeduplicator";
|
|
4
|
+
/**
|
|
5
|
+
* One observed event, the single argument of `trackSchemaFromEvent`. The gateway fields
|
|
6
|
+
* are for a gateway-scoped Inspector API key; blank values are treated as absent.
|
|
7
|
+
*/
|
|
8
|
+
export interface InspectorEvent {
|
|
9
|
+
/** The event name. A missing, empty or whitespace-only name is sent as "Missing Event Name". */
|
|
10
|
+
eventName: string;
|
|
11
|
+
/** The event's properties. Absent or null: an event with no properties. */
|
|
12
|
+
eventProperties?: {
|
|
13
|
+
[propName: string]: any;
|
|
14
|
+
} | null;
|
|
15
|
+
/** Optional stream id to correlate events. */
|
|
16
|
+
streamId?: string;
|
|
17
|
+
/** Reference of the gateway output this observation was bound for. Absent = gateway checkpoint. */
|
|
18
|
+
outputReference?: string;
|
|
19
|
+
/** Low-cardinality label of the source the event came from (e.g. "web"). Never a user id. */
|
|
20
|
+
originHint?: string;
|
|
21
|
+
/** App version of the source that produced this event; overrides the instance version. */
|
|
22
|
+
originAppVersion?: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The gateway fields of a Codegen call (`_avoFunctionTrackSchemaFromEvent`), with the same
|
|
26
|
+
* meaning as on InspectorEvent. Not re-exported from the package root, but not marked internal:
|
|
27
|
+
* stripInternal would drop it from the declarations while that public method still uses it.
|
|
28
|
+
*/
|
|
29
|
+
export interface CodegenGatewayFields {
|
|
30
|
+
outputReference?: string;
|
|
31
|
+
originHint?: string;
|
|
32
|
+
originAppVersion?: string;
|
|
33
|
+
}
|
|
34
|
+
type SchemaEntry = {
|
|
35
|
+
propertyName: string;
|
|
36
|
+
propertyType: string;
|
|
37
|
+
children?: any;
|
|
38
|
+
};
|
|
4
39
|
export declare class AvoInspector {
|
|
5
40
|
environment: AvoInspectorEnvValueType;
|
|
6
41
|
avoNetworkCallsHandler: AvoNetworkCallsHandler;
|
|
@@ -12,45 +47,116 @@ export declare class AvoInspector {
|
|
|
12
47
|
private eventSpecCache;
|
|
13
48
|
private eventValidator;
|
|
14
49
|
private generatedAnonymousId;
|
|
15
|
-
private
|
|
16
|
-
private
|
|
50
|
+
private batchSize;
|
|
51
|
+
private batchQueue;
|
|
52
|
+
private destroyed;
|
|
53
|
+
private destroyWaiters;
|
|
54
|
+
private pending;
|
|
55
|
+
private validations;
|
|
56
|
+
private drainScheduled;
|
|
57
|
+
private drainThisTurn;
|
|
58
|
+
private untilDestroyed;
|
|
59
|
+
private untilGivenUp;
|
|
60
|
+
/** The exit drain ran out of time for validations: their events are sent unvalidated. */
|
|
61
|
+
private giveUpValidations;
|
|
62
|
+
private get pendingCount();
|
|
17
63
|
private trackPending;
|
|
64
|
+
private static instancesWithWork;
|
|
65
|
+
private static exitDrainArmed;
|
|
66
|
+
private static exitReportArmed;
|
|
67
|
+
private static exitDeadline;
|
|
68
|
+
private static waitingValidations;
|
|
69
|
+
private releaseWaiting;
|
|
70
|
+
private validationOutcomes;
|
|
71
|
+
private giveUpWaiters;
|
|
72
|
+
private static lastTrackAt;
|
|
73
|
+
private static validationGraceUsedFor;
|
|
74
|
+
private static explicitFlushDeadlines;
|
|
75
|
+
private static drainOnExit;
|
|
76
|
+
private static reportOnExit;
|
|
77
|
+
private static armExitDrain;
|
|
78
|
+
private static disarmExitDrain;
|
|
79
|
+
private updateExitDrain;
|
|
18
80
|
private static _shouldLog;
|
|
81
|
+
/** Whether the SDK writes its diagnostic log lines (shared by every instance). */
|
|
19
82
|
static get shouldLog(): boolean;
|
|
20
83
|
static set shouldLog(enable: boolean);
|
|
84
|
+
/**
|
|
85
|
+
* Creates an Inspector instance. A missing or unsupported `env` falls back to dev.
|
|
86
|
+
*
|
|
87
|
+
* @throws if `apiKey` or `version` is blank, or `apiKey` contains a character that
|
|
88
|
+
* cannot be sent in an HTTP header.
|
|
89
|
+
*/
|
|
21
90
|
constructor(options: {
|
|
22
91
|
apiKey: string;
|
|
23
92
|
env: AvoInspectorEnvValueType;
|
|
24
93
|
version: string;
|
|
25
94
|
appName?: string;
|
|
26
95
|
publicEncryptionKey?: string;
|
|
96
|
+
/** Flush when this many events are buffered. Default 30; always 1 in dev. */
|
|
97
|
+
batchSize?: number;
|
|
98
|
+
/** Flush once the oldest buffered event is this many seconds old. Default 30. */
|
|
99
|
+
batchFlushSeconds?: number;
|
|
100
|
+
/** Maximum buffered events; the oldest are dropped first. Default 1000. */
|
|
101
|
+
maxQueueSize?: number;
|
|
102
|
+
/** Start no background flush timer (recommended for serverless). Default false. */
|
|
103
|
+
disableBatchTimer?: boolean;
|
|
27
104
|
});
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
105
|
+
private static resolveBatchOptions;
|
|
106
|
+
/**
|
|
107
|
+
* Resolves the gateway options for one event. outputReference / originHint are
|
|
108
|
+
* omitted when blank; appVersion is the per-event override, null for a
|
|
109
|
+
* source-scoped event without one, else the instance version.
|
|
110
|
+
*/
|
|
111
|
+
private resolveGatewayFields;
|
|
112
|
+
private static warnUnknownEventKeys;
|
|
113
|
+
/**
|
|
114
|
+
* Extracts the event schema and queues it for the Inspector API.
|
|
115
|
+
*
|
|
116
|
+
* Resolves with the schema once the event is queued. In dev (batch size 1) the send
|
|
117
|
+
* happens within the call and a non-200 response resolves `[]`; with batching the
|
|
118
|
+
* batch's HTTP outcome is not observable here. Buffered events are only delivered
|
|
119
|
+
* by a later size or time trigger, or by `flush()` — call `flush()` before exit.
|
|
120
|
+
*
|
|
121
|
+
* Anything but an object (such as a 1.x call `trackSchemaFromEvent(name, properties)`)
|
|
122
|
+
* sends nothing, prints an always-on error naming the fix, and resolves `[]`.
|
|
123
|
+
*/
|
|
124
|
+
trackSchemaFromEvent(event: InspectorEvent): Promise<Array<SchemaEntry>>;
|
|
125
|
+
/**
|
|
126
|
+
* Codegen entry point: like `trackSchemaFromEvent`, and additionally records the
|
|
127
|
+
* Avo function's `eventId` / `eventHash` so the event is sent with `avoFunction: true`.
|
|
128
|
+
*/
|
|
35
129
|
_avoFunctionTrackSchemaFromEvent(eventName: string, eventProperties: {
|
|
36
130
|
[propName: string]: any;
|
|
37
|
-
}, eventId: string, eventHash: string, streamId?: string): Promise<Array<
|
|
38
|
-
|
|
39
|
-
propertyType: string;
|
|
40
|
-
children?: any;
|
|
41
|
-
}>>;
|
|
131
|
+
}, eventId: string, eventHash: string, streamId?: string, options?: CodegenGatewayFields): Promise<Array<SchemaEntry>>;
|
|
132
|
+
private track;
|
|
42
133
|
/**
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
*
|
|
49
|
-
*
|
|
134
|
+
* Codegen/manual deduplication applies only to plain calls. A call carrying gateway
|
|
135
|
+
* options is a distinct observation per gateway output and is always sent.
|
|
136
|
+
*/
|
|
137
|
+
private shouldRegisterEvent;
|
|
138
|
+
/**
|
|
139
|
+
* Samples the event, then queues it. When event spec validation is active the spec
|
|
140
|
+
* is fetched (or read from cache) first, so the event joins the queue once its
|
|
141
|
+
* validation results are merged in; flush() awaits that fetch before draining.
|
|
50
142
|
*/
|
|
51
|
-
private
|
|
52
|
-
private
|
|
143
|
+
private sampleAndEnqueue;
|
|
144
|
+
private enqueue;
|
|
145
|
+
private sendBatch;
|
|
146
|
+
private isValidationActive;
|
|
147
|
+
/**
|
|
148
|
+
* Sends every buffered event, then waits until all in-flight sends (and spec fetches
|
|
149
|
+
* that will enqueue) have completed or `timeoutMs` (default 10000) has elapsed.
|
|
150
|
+
* Resolves `true` if, when it resolves, this instance has nothing buffered, waiting or in
|
|
151
|
+
* flight (always after destroy()), and `false` if the timeout won. Never rejects. Call it
|
|
152
|
+
* before process exit or before a serverless handler returns.
|
|
153
|
+
*/
|
|
154
|
+
flush(timeoutMs?: number): Promise<boolean>;
|
|
155
|
+
private hasDrained;
|
|
156
|
+
private flushWithin;
|
|
157
|
+
/** Turns the SDK's diagnostic logging on or off for every instance. */
|
|
53
158
|
enableLogging(enable: boolean): void;
|
|
159
|
+
/** Returns the schema of `eventProperties` without tracking or sending anything. */
|
|
54
160
|
extractSchema(eventProperties: {
|
|
55
161
|
[propName: string]: any;
|
|
56
162
|
}, shouldLogIfEnabled?: boolean): Array<{
|
|
@@ -63,5 +169,12 @@ export declare class AvoInspector {
|
|
|
63
169
|
* validation succeeds, or null if unavailable (prod, no spec, fetch error).
|
|
64
170
|
*/
|
|
65
171
|
private fetchAndValidate;
|
|
172
|
+
/**
|
|
173
|
+
* Cancels and cleans up: discards the pending batch unsent, abandons in-flight
|
|
174
|
+
* requests and stops the flush timer. Does not flush. After destroy() the instance
|
|
175
|
+
* is terminated and trackSchemaFromEvent resolves [] without sending; a call still
|
|
176
|
+
* waiting on a spec fetch or on its immediate send also resolves [].
|
|
177
|
+
*/
|
|
66
178
|
destroy(): void;
|
|
67
179
|
}
|
|
180
|
+
export {};
|