@traffical/node 0.9.0 → 0.10.1
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 +16 -6
- package/dist/assignment-logger.test.js +10 -1
- package/dist/assignment-logger.test.js.map +1 -1
- package/dist/canonical-options.test.d.ts +2 -0
- package/dist/canonical-options.test.d.ts.map +1 -0
- package/dist/canonical-options.test.js +167 -0
- package/dist/canonical-options.test.js.map +1 -0
- package/dist/client.d.ts +101 -17
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +266 -51
- package/dist/client.js.map +1 -1
- package/dist/contract-0.7.0.test.d.ts +2 -0
- package/dist/contract-0.7.0.test.d.ts.map +1 -0
- package/dist/contract-0.7.0.test.js +171 -0
- package/dist/contract-0.7.0.test.js.map +1 -0
- package/dist/event-batcher.d.ts +51 -2
- package/dist/event-batcher.d.ts.map +1 -1
- package/dist/event-batcher.js +135 -15
- package/dist/event-batcher.js.map +1 -1
- package/dist/event-logger.test.js +63 -3
- package/dist/event-logger.test.js.map +1 -1
- package/dist/malformed-bundle.test.d.ts +12 -0
- package/dist/malformed-bundle.test.d.ts.map +1 -0
- package/dist/malformed-bundle.test.js +110 -0
- package/dist/malformed-bundle.test.js.map +1 -0
- package/dist/track-exposure.test.d.ts +12 -0
- package/dist/track-exposure.test.d.ts.map +1 -0
- package/dist/track-exposure.test.js +112 -0
- package/dist/track-exposure.test.js.map +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +5 -5
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spec 0.7.0 contract behaviors for the Node SDK: bounded event queue with
|
|
3
|
+
* drop-oldest, exponential-backoff retry + re-queue, HTTP 401 kill-switch,
|
|
4
|
+
* trackReward value/decisionId forwarding, positional decide/getParams, and
|
|
5
|
+
* the close()/waitForReady() lifecycle verbs.
|
|
6
|
+
*/
|
|
7
|
+
import { describe, test, expect, beforeEach, afterEach, mock } from "bun:test";
|
|
8
|
+
import { EventBatcher } from "./event-batcher.js";
|
|
9
|
+
import { TrafficalClient } from "./client.js";
|
|
10
|
+
function trackEvent(id = "evt") {
|
|
11
|
+
return {
|
|
12
|
+
type: "track",
|
|
13
|
+
id,
|
|
14
|
+
orgId: "org_1",
|
|
15
|
+
projectId: "proj_1",
|
|
16
|
+
env: "test",
|
|
17
|
+
unitKey: "u1",
|
|
18
|
+
timestamp: new Date().toISOString(),
|
|
19
|
+
event: "e",
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
describe("EventBatcher bounded queue (S8)", () => {
|
|
23
|
+
let originalFetch;
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
originalFetch = globalThis.fetch;
|
|
26
|
+
});
|
|
27
|
+
afterEach(() => {
|
|
28
|
+
globalThis.fetch = originalFetch;
|
|
29
|
+
});
|
|
30
|
+
test("drops the oldest event when the queue is full and counts drops", () => {
|
|
31
|
+
// Never auto-flush (huge batchSize + interval), tiny queue cap.
|
|
32
|
+
const batcher = new EventBatcher({
|
|
33
|
+
endpoint: "https://x/v1/events/batch",
|
|
34
|
+
apiKey: "pk",
|
|
35
|
+
batchSize: 1000,
|
|
36
|
+
flushIntervalMs: 999999,
|
|
37
|
+
maxQueueSize: 2,
|
|
38
|
+
});
|
|
39
|
+
batcher.log(trackEvent("a"));
|
|
40
|
+
batcher.log(trackEvent("b"));
|
|
41
|
+
batcher.log(trackEvent("c")); // evicts "a"
|
|
42
|
+
expect(batcher.queueSize).toBe(2);
|
|
43
|
+
expect(batcher.droppedCount).toBe(1);
|
|
44
|
+
batcher.destroySync();
|
|
45
|
+
});
|
|
46
|
+
test("HTTP 401 permanently disables delivery and clears the queue", async () => {
|
|
47
|
+
globalThis.fetch = mock(() => Promise.resolve(new Response("no", { status: 401 })));
|
|
48
|
+
const batcher = new EventBatcher({
|
|
49
|
+
endpoint: "https://x/v1/events/batch",
|
|
50
|
+
apiKey: "pk",
|
|
51
|
+
batchSize: 1000,
|
|
52
|
+
flushIntervalMs: 999999,
|
|
53
|
+
});
|
|
54
|
+
batcher.log(trackEvent("a"));
|
|
55
|
+
await batcher.flush();
|
|
56
|
+
expect(batcher.isDisabled).toBe(true);
|
|
57
|
+
expect(batcher.queueSize).toBe(0);
|
|
58
|
+
// Subsequent logs are dropped (no buffering after kill-switch).
|
|
59
|
+
batcher.log(trackEvent("b"));
|
|
60
|
+
expect(batcher.queueSize).toBe(0);
|
|
61
|
+
batcher.destroySync();
|
|
62
|
+
});
|
|
63
|
+
test("retries a 5xx with backoff, then re-queues the batch", async () => {
|
|
64
|
+
const fetchMock = mock(() => Promise.resolve(new Response("err", { status: 503 })));
|
|
65
|
+
globalThis.fetch = fetchMock;
|
|
66
|
+
const errors = [];
|
|
67
|
+
const batcher = new EventBatcher({
|
|
68
|
+
endpoint: "https://x/v1/events/batch",
|
|
69
|
+
apiKey: "pk",
|
|
70
|
+
batchSize: 1000,
|
|
71
|
+
flushIntervalMs: 999999,
|
|
72
|
+
maxRetries: 2,
|
|
73
|
+
retryBackoffMs: 1,
|
|
74
|
+
onError: (e) => errors.push(e),
|
|
75
|
+
});
|
|
76
|
+
batcher.log(trackEvent("a"));
|
|
77
|
+
await batcher.flush();
|
|
78
|
+
// 1 initial + 2 retries = 3 attempts, then re-queued for a later flush.
|
|
79
|
+
expect(fetchMock.mock.calls.length).toBe(3);
|
|
80
|
+
expect(batcher.queueSize).toBe(1);
|
|
81
|
+
expect(errors).toHaveLength(1);
|
|
82
|
+
expect(batcher.isDisabled).toBe(false);
|
|
83
|
+
batcher.destroySync();
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
describe("Node client contract (S8 + A1)", () => {
|
|
87
|
+
let originalFetch;
|
|
88
|
+
beforeEach(() => {
|
|
89
|
+
originalFetch = globalThis.fetch;
|
|
90
|
+
// Config fetch 404 => fail-open; client stays usable with caller defaults.
|
|
91
|
+
globalThis.fetch = mock(() => Promise.resolve(new Response("not found", { status: 404 })));
|
|
92
|
+
});
|
|
93
|
+
afterEach(() => {
|
|
94
|
+
globalThis.fetch = originalFetch;
|
|
95
|
+
});
|
|
96
|
+
test("waitForReady() resolves even when the bundle 404s (fail-open)", async () => {
|
|
97
|
+
const client = new TrafficalClient({
|
|
98
|
+
orgId: "o",
|
|
99
|
+
projectId: "p",
|
|
100
|
+
env: "test",
|
|
101
|
+
apiKey: "pk",
|
|
102
|
+
disableCloudEvents: true,
|
|
103
|
+
});
|
|
104
|
+
await client.initialize();
|
|
105
|
+
await client.waitForReady(); // must not hang
|
|
106
|
+
expect(true).toBe(true);
|
|
107
|
+
await client.close();
|
|
108
|
+
});
|
|
109
|
+
test("positional decide(context, defaults) matches the legacy object bag", async () => {
|
|
110
|
+
const client = new TrafficalClient({
|
|
111
|
+
orgId: "o",
|
|
112
|
+
projectId: "p",
|
|
113
|
+
env: "test",
|
|
114
|
+
apiKey: "pk",
|
|
115
|
+
disableCloudEvents: true,
|
|
116
|
+
trackDecisions: false,
|
|
117
|
+
});
|
|
118
|
+
await client.initialize();
|
|
119
|
+
const positional = client.decide({ userId: "u1" }, { "ui.color": "#000" });
|
|
120
|
+
const bag = client.decide({ context: { userId: "u1" }, defaults: { "ui.color": "#000" } });
|
|
121
|
+
expect(positional.assignments).toEqual(bag.assignments);
|
|
122
|
+
expect(positional.metadata.unitKeyValue).toBe(bag.metadata.unitKeyValue);
|
|
123
|
+
await client.close();
|
|
124
|
+
});
|
|
125
|
+
test("trackReward forwards value and decisionId (previously dropped)", async () => {
|
|
126
|
+
const captured = [];
|
|
127
|
+
const client = new TrafficalClient({
|
|
128
|
+
orgId: "o",
|
|
129
|
+
projectId: "p",
|
|
130
|
+
env: "test",
|
|
131
|
+
apiKey: "pk",
|
|
132
|
+
disableCloudEvents: true,
|
|
133
|
+
trackDecisions: false,
|
|
134
|
+
eventLogger: (e) => captured.push(e),
|
|
135
|
+
});
|
|
136
|
+
await client.initialize();
|
|
137
|
+
const decision = client.decide({ userId: "u1" }, { "ui.color": "#000" });
|
|
138
|
+
client.trackReward({ event: "purchase", value: 42, decisionId: decision.decisionId });
|
|
139
|
+
const track = captured.find((e) => e.type === "track");
|
|
140
|
+
expect(track).toBeDefined();
|
|
141
|
+
expect(track.value).toBe(42);
|
|
142
|
+
expect(track.decisionId).toBe(decision.decisionId);
|
|
143
|
+
await client.close();
|
|
144
|
+
});
|
|
145
|
+
test("track() options bag carries value/values/eventTimestamp", async () => {
|
|
146
|
+
const captured = [];
|
|
147
|
+
const client = new TrafficalClient({
|
|
148
|
+
orgId: "o",
|
|
149
|
+
projectId: "p",
|
|
150
|
+
env: "test",
|
|
151
|
+
apiKey: "pk",
|
|
152
|
+
disableCloudEvents: true,
|
|
153
|
+
trackDecisions: false,
|
|
154
|
+
eventLogger: (e) => captured.push(e),
|
|
155
|
+
});
|
|
156
|
+
await client.initialize();
|
|
157
|
+
const ts = "2024-06-01T00:00:00.000Z";
|
|
158
|
+
client.track("purchase", { orderId: "o1" }, {
|
|
159
|
+
unitKey: "u1",
|
|
160
|
+
value: 10,
|
|
161
|
+
values: { revenue: 10, items: 2 },
|
|
162
|
+
eventTimestamp: ts,
|
|
163
|
+
});
|
|
164
|
+
const track = captured.find((e) => e.type === "track");
|
|
165
|
+
expect(track.value).toBe(10);
|
|
166
|
+
expect(track.values).toEqual({ revenue: 10, items: 2 });
|
|
167
|
+
expect(track.eventTimestamp).toBe(ts);
|
|
168
|
+
await client.close();
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
//# sourceMappingURL=contract-0.7.0.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contract-0.7.0.test.js","sourceRoot":"","sources":["../src/contract-0.7.0.test.ts"],"names":[],"mappings":"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"}
|
package/dist/event-batcher.d.ts
CHANGED
|
@@ -31,6 +31,20 @@ export interface EventBatcherOptions {
|
|
|
31
31
|
* events are re-queued for retry.
|
|
32
32
|
*/
|
|
33
33
|
requestTimeoutMs?: number;
|
|
34
|
+
/**
|
|
35
|
+
* Maximum number of events buffered in memory (default: 1000). When the
|
|
36
|
+
* queue is full, the OLDEST event is dropped (a counter is bumped) — the
|
|
37
|
+
* queue never grows without bound. Aligns with the Python SDK's model.
|
|
38
|
+
*/
|
|
39
|
+
maxQueueSize?: number;
|
|
40
|
+
/**
|
|
41
|
+
* Max retry attempts (after the first) for a transient delivery failure
|
|
42
|
+
* (network error, timeout, 429, 5xx) before the batch is re-queued for a
|
|
43
|
+
* later flush (default: 3).
|
|
44
|
+
*/
|
|
45
|
+
maxRetries?: number;
|
|
46
|
+
/** Base for exponential retry backoff in ms: base * 2^(attempt-1) (default: 250). */
|
|
47
|
+
retryBackoffMs?: number;
|
|
34
48
|
/** Callback on flush error */
|
|
35
49
|
onError?: (error: Error) => void;
|
|
36
50
|
/** Enable debug logging */
|
|
@@ -44,6 +58,9 @@ export declare class EventBatcher {
|
|
|
44
58
|
private readonly _batchSize;
|
|
45
59
|
private readonly _flushIntervalMs;
|
|
46
60
|
private readonly _requestTimeoutMs;
|
|
61
|
+
private readonly _maxQueueSize;
|
|
62
|
+
private readonly _maxRetries;
|
|
63
|
+
private readonly _retryBackoffMs;
|
|
47
64
|
private readonly _onError?;
|
|
48
65
|
private readonly _onSchemaWarnings?;
|
|
49
66
|
private readonly _debug;
|
|
@@ -51,19 +68,45 @@ export declare class EventBatcher {
|
|
|
51
68
|
private _flushTimer;
|
|
52
69
|
private _isFlushing;
|
|
53
70
|
private _isDestroyed;
|
|
71
|
+
/** Permanently true after an HTTP 401 kill-switch fires. */
|
|
72
|
+
private _isDisabled;
|
|
73
|
+
/** Count of events dropped because the bounded queue overflowed. */
|
|
74
|
+
private _droppedCount;
|
|
54
75
|
constructor(options: EventBatcherOptions);
|
|
55
76
|
/**
|
|
56
|
-
* Log an event (added to batch queue).
|
|
77
|
+
* Log an event (added to the bounded batch queue). When the queue is full
|
|
78
|
+
* the OLDEST event is dropped (drop-oldest) so memory can't grow without
|
|
79
|
+
* bound. After a 401 kill-switch, events are silently discarded.
|
|
57
80
|
*/
|
|
58
81
|
log(event: TrackableEvent): void;
|
|
59
82
|
/**
|
|
60
|
-
* Flush
|
|
83
|
+
* Flush queued events immediately.
|
|
84
|
+
*
|
|
85
|
+
* Drains the queue in batch-sized chunks. Each batch is delivered with
|
|
86
|
+
* exponential-backoff retry on transient failures (network/timeout/429/5xx).
|
|
87
|
+
* A batch that still fails after `maxRetries` is re-queued at the FRONT
|
|
88
|
+
* (bounded) and draining stops until the next flush. A non-retryable 4xx
|
|
89
|
+
* drops the batch. An HTTP 401 permanently disables delivery and clears the
|
|
90
|
+
* queue (auth kill-switch).
|
|
61
91
|
*/
|
|
62
92
|
flush(): Promise<void>;
|
|
93
|
+
/**
|
|
94
|
+
* Delivers one batch with bounded exponential-backoff retry.
|
|
95
|
+
* onError fires exactly once per batch that ends in a retry-later.
|
|
96
|
+
*/
|
|
97
|
+
private _deliverWithRetry;
|
|
98
|
+
/** Re-queues a failed batch at the front, dropping oldest on overflow. */
|
|
99
|
+
private _requeueFront;
|
|
100
|
+
private _disable;
|
|
101
|
+
private _sleep;
|
|
63
102
|
/**
|
|
64
103
|
* Get the number of events in the queue.
|
|
65
104
|
*/
|
|
66
105
|
get queueSize(): number;
|
|
106
|
+
/** Number of events dropped because the bounded queue overflowed. */
|
|
107
|
+
get droppedCount(): number;
|
|
108
|
+
/** True once an HTTP 401 permanently disabled delivery. */
|
|
109
|
+
get isDisabled(): boolean;
|
|
67
110
|
/**
|
|
68
111
|
* Check if the batcher is destroyed.
|
|
69
112
|
*/
|
|
@@ -77,6 +120,12 @@ export declare class EventBatcher {
|
|
|
77
120
|
* Does not wait for flush to complete.
|
|
78
121
|
*/
|
|
79
122
|
destroySync(): void;
|
|
123
|
+
/**
|
|
124
|
+
* Sends one batch and returns the HTTP status code. Throws only on a
|
|
125
|
+
* transport-level failure (network error / abort), which the caller treats
|
|
126
|
+
* as a transient, retryable error. HTTP status classification (2xx / 401 /
|
|
127
|
+
* 4xx / 5xx) is done by the caller.
|
|
128
|
+
*/
|
|
80
129
|
private _sendEvents;
|
|
81
130
|
private _startFlushTimer;
|
|
82
131
|
private _log;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"event-batcher.d.ts","sourceRoot":"","sources":["../src/event-batcher.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,gBAAgB,EAAsB,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"event-batcher.d.ts","sourceRoot":"","sources":["../src/event-batcher.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,gBAAgB,EAAsB,MAAM,iBAAiB,CAAC;AAS5F;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,8BAA8B;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,iCAAiC;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,iDAAiD;IACjD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,iDAAiD;IACjD,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,qFAAqF;IACrF,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,8BAA8B;IAC9B,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IACjC,2BAA2B;IAC3B,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,qFAAqF;IACrF,gBAAgB,CAAC,EAAE,gBAAgB,CAAC;CACrC;AAKD,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAS;IAC1C,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAS;IAC3C,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IACvC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAS;IACzC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAyB;IACnD,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAC,CAAmB;IACtD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAU;IAEjC,OAAO,CAAC,MAAM,CAAwB;IACtC,OAAO,CAAC,WAAW,CAA+C;IAClE,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,YAAY,CAAS;IAC7B,4DAA4D;IAC5D,OAAO,CAAC,WAAW,CAAS;IAC5B,oEAAoE;IACpE,OAAO,CAAC,aAAa,CAAK;gBAEd,OAAO,EAAE,mBAAmB;IAiBxC;;;;OAIG;IACH,GAAG,CAAC,KAAK,EAAE,cAAc,GAAG,IAAI;IA6BhC;;;;;;;;;OASG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAiC5B;;;OAGG;YACW,iBAAiB;IAwC/B,0EAA0E;IAC1E,OAAO,CAAC,aAAa;IAQrB,OAAO,CAAC,QAAQ;IAShB,OAAO,CAAC,MAAM;IASd;;OAEG;IACH,IAAI,SAAS,IAAI,MAAM,CAEtB;IAED,qEAAqE;IACrE,IAAI,YAAY,IAAI,MAAM,CAEzB;IAED,2DAA2D;IAC3D,IAAI,UAAU,IAAI,OAAO,CAExB;IAED;;OAEG;IACH,IAAI,WAAW,IAAI,OAAO,CAEzB;IAED;;OAEG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAoB9B;;;OAGG;IACH,WAAW,IAAI,IAAI;IAgBnB;;;;;OAKG;YACW,WAAW;IAuCzB,OAAO,CAAC,gBAAgB;IAoBxB,OAAO,CAAC,IAAI;CAKb"}
|
package/dist/event-batcher.js
CHANGED
|
@@ -15,12 +15,18 @@
|
|
|
15
15
|
const DEFAULT_BATCH_SIZE = 10;
|
|
16
16
|
const DEFAULT_FLUSH_INTERVAL_MS = 30_000; // 30 seconds
|
|
17
17
|
const DEFAULT_REQUEST_TIMEOUT_MS = 10_000; // 10 seconds
|
|
18
|
+
const DEFAULT_MAX_QUEUE_SIZE = 1_000; // bounded queue; overflow drops oldest
|
|
19
|
+
const DEFAULT_MAX_RETRIES = 3; // attempts after the first = 3
|
|
20
|
+
const DEFAULT_RETRY_BACKOFF_MS = 250; // exponential: base * 2^(attempt-1)
|
|
18
21
|
export class EventBatcher {
|
|
19
22
|
_endpoint;
|
|
20
23
|
_apiKey;
|
|
21
24
|
_batchSize;
|
|
22
25
|
_flushIntervalMs;
|
|
23
26
|
_requestTimeoutMs;
|
|
27
|
+
_maxQueueSize;
|
|
28
|
+
_maxRetries;
|
|
29
|
+
_retryBackoffMs;
|
|
24
30
|
_onError;
|
|
25
31
|
_onSchemaWarnings;
|
|
26
32
|
_debug;
|
|
@@ -28,12 +34,19 @@ export class EventBatcher {
|
|
|
28
34
|
_flushTimer = null;
|
|
29
35
|
_isFlushing = false;
|
|
30
36
|
_isDestroyed = false;
|
|
37
|
+
/** Permanently true after an HTTP 401 kill-switch fires. */
|
|
38
|
+
_isDisabled = false;
|
|
39
|
+
/** Count of events dropped because the bounded queue overflowed. */
|
|
40
|
+
_droppedCount = 0;
|
|
31
41
|
constructor(options) {
|
|
32
42
|
this._endpoint = options.endpoint;
|
|
33
43
|
this._apiKey = options.apiKey;
|
|
34
44
|
this._batchSize = options.batchSize ?? DEFAULT_BATCH_SIZE;
|
|
35
45
|
this._flushIntervalMs = options.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS;
|
|
36
46
|
this._requestTimeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
47
|
+
this._maxQueueSize = options.maxQueueSize ?? DEFAULT_MAX_QUEUE_SIZE;
|
|
48
|
+
this._maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
49
|
+
this._retryBackoffMs = options.retryBackoffMs ?? DEFAULT_RETRY_BACKOFF_MS;
|
|
37
50
|
this._onError = options.onError;
|
|
38
51
|
this._onSchemaWarnings = options.onSchemaWarnings;
|
|
39
52
|
this._debug = options.debug ?? false;
|
|
@@ -41,13 +54,25 @@ export class EventBatcher {
|
|
|
41
54
|
this._startFlushTimer();
|
|
42
55
|
}
|
|
43
56
|
/**
|
|
44
|
-
* Log an event (added to batch queue).
|
|
57
|
+
* Log an event (added to the bounded batch queue). When the queue is full
|
|
58
|
+
* the OLDEST event is dropped (drop-oldest) so memory can't grow without
|
|
59
|
+
* bound. After a 401 kill-switch, events are silently discarded.
|
|
45
60
|
*/
|
|
46
61
|
log(event) {
|
|
47
62
|
if (this._isDestroyed) {
|
|
48
63
|
this._log("Attempted to log event after destroy, ignoring");
|
|
49
64
|
return;
|
|
50
65
|
}
|
|
66
|
+
if (this._isDisabled) {
|
|
67
|
+
// Delivery permanently disabled (401) — stop buffering entirely.
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
// Bounded queue: drop the oldest event on overflow.
|
|
71
|
+
if (this._queue.length >= this._maxQueueSize) {
|
|
72
|
+
this._queue.shift();
|
|
73
|
+
this._droppedCount++;
|
|
74
|
+
this._log(`Queue full, dropped oldest event (dropped total: ${this._droppedCount})`);
|
|
75
|
+
}
|
|
51
76
|
this._queue.push(event);
|
|
52
77
|
this._log(`Event queued (queue size: ${this._queue.length})`);
|
|
53
78
|
// Auto-flush if batch is full
|
|
@@ -59,36 +84,124 @@ export class EventBatcher {
|
|
|
59
84
|
}
|
|
60
85
|
}
|
|
61
86
|
/**
|
|
62
|
-
* Flush
|
|
87
|
+
* Flush queued events immediately.
|
|
88
|
+
*
|
|
89
|
+
* Drains the queue in batch-sized chunks. Each batch is delivered with
|
|
90
|
+
* exponential-backoff retry on transient failures (network/timeout/429/5xx).
|
|
91
|
+
* A batch that still fails after `maxRetries` is re-queued at the FRONT
|
|
92
|
+
* (bounded) and draining stops until the next flush. A non-retryable 4xx
|
|
93
|
+
* drops the batch. An HTTP 401 permanently disables delivery and clears the
|
|
94
|
+
* queue (auth kill-switch).
|
|
63
95
|
*/
|
|
64
96
|
async flush() {
|
|
65
|
-
if (this._isFlushing || this._queue.length === 0) {
|
|
97
|
+
if (this._isFlushing || this._isDisabled || this._queue.length === 0) {
|
|
66
98
|
return;
|
|
67
99
|
}
|
|
68
100
|
this._isFlushing = true;
|
|
69
|
-
// Take current queue
|
|
70
|
-
const events = [...this._queue];
|
|
71
|
-
this._queue = [];
|
|
72
101
|
try {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
102
|
+
while (this._queue.length > 0 && !this._isDisabled) {
|
|
103
|
+
// Drain a batch OUT of the queue (so concurrent log()s can't alias it).
|
|
104
|
+
const batch = this._queue.splice(0, this._batchSize);
|
|
105
|
+
const outcome = await this._deliverWithRetry(batch);
|
|
106
|
+
if (outcome === "delivered") {
|
|
107
|
+
this._log(`Flushed ${batch.length} events successfully`);
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (outcome === "dropped") {
|
|
111
|
+
continue; // non-retryable rejection; batch discarded
|
|
112
|
+
}
|
|
113
|
+
if (outcome === "disabled") {
|
|
114
|
+
this._queue = []; // 401 kill-switch
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
// retry-later: put the batch back at the front (bounded) and stop.
|
|
118
|
+
this._requeueFront(batch);
|
|
119
|
+
this._log(`Flush failed, ${batch.length} events re-queued`);
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
81
122
|
}
|
|
82
123
|
finally {
|
|
83
124
|
this._isFlushing = false;
|
|
84
125
|
}
|
|
85
126
|
}
|
|
127
|
+
/**
|
|
128
|
+
* Delivers one batch with bounded exponential-backoff retry.
|
|
129
|
+
* onError fires exactly once per batch that ends in a retry-later.
|
|
130
|
+
*/
|
|
131
|
+
async _deliverWithRetry(batch) {
|
|
132
|
+
let lastError;
|
|
133
|
+
for (let attempt = 0; attempt <= this._maxRetries; attempt++) {
|
|
134
|
+
if (attempt > 0) {
|
|
135
|
+
// Stop retrying inline once we're shutting down; the batch is
|
|
136
|
+
// re-queued and best-effort flushed by the caller.
|
|
137
|
+
if (this._isDestroyed)
|
|
138
|
+
break;
|
|
139
|
+
await this._sleep(this._retryBackoffMs * 2 ** (attempt - 1));
|
|
140
|
+
}
|
|
141
|
+
let status;
|
|
142
|
+
try {
|
|
143
|
+
status = await this._sendEvents(batch);
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
// Network error / abort (timeout) — transient, retry.
|
|
147
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (status >= 200 && status < 300)
|
|
151
|
+
return "delivered";
|
|
152
|
+
if (status === 401) {
|
|
153
|
+
this._disable();
|
|
154
|
+
return "disabled";
|
|
155
|
+
}
|
|
156
|
+
if (status === 429 || status >= 500) {
|
|
157
|
+
lastError = new Error(`HTTP ${status}`);
|
|
158
|
+
continue; // transient, retry
|
|
159
|
+
}
|
|
160
|
+
// Other 4xx — permanent rejection, drop the batch.
|
|
161
|
+
this._onError?.(new Error(`HTTP ${status}: batch rejected, dropping ${batch.length} events`));
|
|
162
|
+
return "dropped";
|
|
163
|
+
}
|
|
164
|
+
if (lastError)
|
|
165
|
+
this._onError?.(lastError);
|
|
166
|
+
return "retry-later";
|
|
167
|
+
}
|
|
168
|
+
/** Re-queues a failed batch at the front, dropping oldest on overflow. */
|
|
169
|
+
_requeueFront(batch) {
|
|
170
|
+
this._queue.unshift(...batch);
|
|
171
|
+
while (this._queue.length > this._maxQueueSize) {
|
|
172
|
+
this._queue.shift();
|
|
173
|
+
this._droppedCount++;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
_disable() {
|
|
177
|
+
if (this._isDisabled)
|
|
178
|
+
return;
|
|
179
|
+
this._isDisabled = true;
|
|
180
|
+
this._queue = [];
|
|
181
|
+
console.warn("[Traffical] API key rejected (HTTP 401); event delivery disabled for this client");
|
|
182
|
+
}
|
|
183
|
+
_sleep(ms) {
|
|
184
|
+
return new Promise((resolve) => {
|
|
185
|
+
const t = setTimeout(resolve, ms);
|
|
186
|
+
if (typeof t.unref === "function") {
|
|
187
|
+
t.unref();
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
}
|
|
86
191
|
/**
|
|
87
192
|
* Get the number of events in the queue.
|
|
88
193
|
*/
|
|
89
194
|
get queueSize() {
|
|
90
195
|
return this._queue.length;
|
|
91
196
|
}
|
|
197
|
+
/** Number of events dropped because the bounded queue overflowed. */
|
|
198
|
+
get droppedCount() {
|
|
199
|
+
return this._droppedCount;
|
|
200
|
+
}
|
|
201
|
+
/** True once an HTTP 401 permanently disabled delivery. */
|
|
202
|
+
get isDisabled() {
|
|
203
|
+
return this._isDisabled;
|
|
204
|
+
}
|
|
92
205
|
/**
|
|
93
206
|
* Check if the batcher is destroyed.
|
|
94
207
|
*/
|
|
@@ -131,6 +244,12 @@ export class EventBatcher {
|
|
|
131
244
|
});
|
|
132
245
|
}
|
|
133
246
|
}
|
|
247
|
+
/**
|
|
248
|
+
* Sends one batch and returns the HTTP status code. Throws only on a
|
|
249
|
+
* transport-level failure (network error / abort), which the caller treats
|
|
250
|
+
* as a transient, retryable error. HTTP status classification (2xx / 401 /
|
|
251
|
+
* 4xx / 5xx) is done by the caller.
|
|
252
|
+
*/
|
|
134
253
|
async _sendEvents(events) {
|
|
135
254
|
// Abort the request if the edge hangs so the flush settles and events
|
|
136
255
|
// go down the re-queue-for-retry path (same as any network failure).
|
|
@@ -152,7 +271,7 @@ export class EventBatcher {
|
|
|
152
271
|
clearTimeout(timeoutId);
|
|
153
272
|
}
|
|
154
273
|
if (!response.ok) {
|
|
155
|
-
|
|
274
|
+
return response.status;
|
|
156
275
|
}
|
|
157
276
|
if (this._onSchemaWarnings) {
|
|
158
277
|
try {
|
|
@@ -165,6 +284,7 @@ export class EventBatcher {
|
|
|
165
284
|
// Response parsing is best-effort for dev-mode warnings
|
|
166
285
|
}
|
|
167
286
|
}
|
|
287
|
+
return response.status;
|
|
168
288
|
}
|
|
169
289
|
_startFlushTimer() {
|
|
170
290
|
if (this._flushIntervalMs <= 0) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"event-batcher.js","sourceRoot":"","sources":["../src/event-batcher.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAIH,MAAM,kBAAkB,GAAG,EAAE,CAAC;AAC9B,MAAM,yBAAyB,GAAG,MAAM,CAAC,CAAC,aAAa;AACvD,MAAM,0BAA0B,GAAG,MAAM,CAAC,CAAC,aAAa;
|
|
1
|
+
{"version":3,"file":"event-batcher.js","sourceRoot":"","sources":["../src/event-batcher.ts"],"names":[],"mappings":"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"}
|
|
@@ -68,6 +68,64 @@ describe("Node TrafficalClient eventLogger", () => {
|
|
|
68
68
|
expect(decisions[0]?.unitKey).toBe("user-1");
|
|
69
69
|
await client.destroy();
|
|
70
70
|
});
|
|
71
|
+
test("decision and exposure events carry configVersion", async () => {
|
|
72
|
+
setupFetchMock();
|
|
73
|
+
const events = [];
|
|
74
|
+
const client = new TrafficalClient({
|
|
75
|
+
...clientOpts,
|
|
76
|
+
eventLogger: (event) => events.push(event),
|
|
77
|
+
});
|
|
78
|
+
await client.initialize();
|
|
79
|
+
const decision = client.decide({
|
|
80
|
+
context: { userId: "user-1" },
|
|
81
|
+
defaults: { "ui.color": "#000" },
|
|
82
|
+
});
|
|
83
|
+
client.trackExposure(decision);
|
|
84
|
+
const decisionEvent = events.find((e) => e.type === "decision");
|
|
85
|
+
const exposureEvent = events.find((e) => e.type === "exposure");
|
|
86
|
+
expect(decisionEvent).toBeDefined();
|
|
87
|
+
expect(exposureEvent).toBeDefined();
|
|
88
|
+
// Server mode: getConfigVersion() surfaces the resolve stateVersion.
|
|
89
|
+
expect(decisionEvent.configVersion).toBe(serverResolveResponse.stateVersion);
|
|
90
|
+
expect(exposureEvent.configVersion).toBe(serverResolveResponse.stateVersion);
|
|
91
|
+
await client.destroy();
|
|
92
|
+
});
|
|
93
|
+
test("configVersion is snapshotted at decide() time, not event-build time", async () => {
|
|
94
|
+
// The mock serves stateVersion v1 first, then v2 after the refresh.
|
|
95
|
+
let stateVersion = "2024-01-01T00:00:00Z";
|
|
96
|
+
const fetchMock = mock(async () => {
|
|
97
|
+
return {
|
|
98
|
+
ok: true,
|
|
99
|
+
status: 200,
|
|
100
|
+
statusText: "OK",
|
|
101
|
+
json: async () => ({ ...serverResolveResponse, stateVersion }),
|
|
102
|
+
headers: new Headers({ ETag: '"v1"' }),
|
|
103
|
+
};
|
|
104
|
+
});
|
|
105
|
+
globalThis.fetch = fetchMock;
|
|
106
|
+
const events = [];
|
|
107
|
+
const client = new TrafficalClient({
|
|
108
|
+
...clientOpts,
|
|
109
|
+
eventLogger: (event) => events.push(event),
|
|
110
|
+
});
|
|
111
|
+
await client.initialize();
|
|
112
|
+
// Decide against v1, THEN refresh to v2 before the exposure is built.
|
|
113
|
+
const decision = client.decide({
|
|
114
|
+
context: { userId: "user-1" },
|
|
115
|
+
defaults: { "ui.color": "#000" },
|
|
116
|
+
});
|
|
117
|
+
expect(decision.metadata.configVersion).toBe("2024-01-01T00:00:00Z");
|
|
118
|
+
stateVersion = "2024-06-01T00:00:00Z";
|
|
119
|
+
await client.refreshConfig();
|
|
120
|
+
expect(client.getConfigVersion()).toBe("2024-06-01T00:00:00Z");
|
|
121
|
+
client.trackExposure(decision);
|
|
122
|
+
// The exposure event carries the decision-time snapshot (v1), not the
|
|
123
|
+
// stateVersion current at event-build time (v2).
|
|
124
|
+
const exposureEvent = events.find((e) => e.type === "exposure");
|
|
125
|
+
expect(exposureEvent).toBeDefined();
|
|
126
|
+
expect(exposureEvent.configVersion).toBe("2024-01-01T00:00:00Z");
|
|
127
|
+
await client.destroy();
|
|
128
|
+
});
|
|
71
129
|
test("trackExposure() and track() emit exposure + track events", async () => {
|
|
72
130
|
setupFetchMock();
|
|
73
131
|
const events = [];
|
|
@@ -96,15 +154,17 @@ describe("Node TrafficalClient eventLogger", () => {
|
|
|
96
154
|
eventLogger: (event) => events.push(event),
|
|
97
155
|
});
|
|
98
156
|
await client.initialize();
|
|
99
|
-
const callsAfterInit = fetchMock.mock.calls.length;
|
|
100
157
|
const decision = client.decide({
|
|
101
158
|
context: { userId: "user-1" },
|
|
102
159
|
defaults: { "ui.color": "#000" },
|
|
103
160
|
});
|
|
104
161
|
client.trackExposure(decision);
|
|
105
162
|
await client.flushEvents();
|
|
106
|
-
// No
|
|
107
|
-
|
|
163
|
+
// No fetch calls to the EVENT-delivery endpoint (disableCloudEvents). A
|
|
164
|
+
// per-call /v1/resolve may fire in server mode, but nothing hits
|
|
165
|
+
// /v1/events/batch.
|
|
166
|
+
const eventBatchCalls = fetchMock.mock.calls.filter((c) => String(c[0]).includes("/v1/events"));
|
|
167
|
+
expect(eventBatchCalls).toHaveLength(0);
|
|
108
168
|
expect(events.length).toBeGreaterThan(0);
|
|
109
169
|
await client.destroy();
|
|
110
170
|
});
|