@genn-inc/cluebase-frontend-sdk 0.0.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 +222 -0
- package/dist/_generated/correlation-attribute-keys.d.ts +2 -0
- package/dist/_generated/correlation-attribute-keys.js +33 -0
- package/dist/_generated/identity-pii-policy.d.ts +38 -0
- package/dist/_generated/identity-pii-policy.js +103 -0
- package/dist/_generated/ingest-privacy-tables.d.ts +120 -0
- package/dist/_generated/ingest-privacy-tables.js +890 -0
- package/dist/_generated/otel-resource-keys.d.ts +21 -0
- package/dist/_generated/otel-resource-keys.js +72 -0
- package/dist/_generated/otel-span-conversion.d.ts +48 -0
- package/dist/_generated/otel-span-conversion.js +69 -0
- package/dist/_generated/parameter-snapshot-leaf.d.ts +11 -0
- package/dist/_generated/parameter-snapshot-leaf.js +37 -0
- package/dist/_generated/sdk-runtime-policy.d.ts +7 -0
- package/dist/_generated/sdk-runtime-policy.js +14 -0
- package/dist/_generated/usage-governor-tables.d.ts +15 -0
- package/dist/_generated/usage-governor-tables.js +44 -0
- package/dist/context/browser-storage.d.ts +15 -0
- package/dist/context/browser-storage.js +38 -0
- package/dist/context/environment.d.ts +37 -0
- package/dist/context/environment.js +229 -0
- package/dist/context/identity.d.ts +16 -0
- package/dist/context/identity.js +139 -0
- package/dist/context/organization.d.ts +16 -0
- package/dist/context/organization.js +60 -0
- package/dist/context/session.d.ts +28 -0
- package/dist/context/session.js +130 -0
- package/dist/context/tab.d.ts +22 -0
- package/dist/context/tab.js +117 -0
- package/dist/context/trace.d.ts +76 -0
- package/dist/context/trace.js +163 -0
- package/dist/exporter/batch.d.ts +94 -0
- package/dist/exporter/batch.js +485 -0
- package/dist/exporter/browser-outbox-records.d.ts +159 -0
- package/dist/exporter/browser-outbox-records.js +341 -0
- package/dist/exporter/browser-outbox.d.ts +112 -0
- package/dist/exporter/browser-outbox.js +876 -0
- package/dist/exporter/client.d.ts +92 -0
- package/dist/exporter/client.js +541 -0
- package/dist/exporter/delivery-response-contract.d.ts +103 -0
- package/dist/exporter/delivery-response-contract.js +147 -0
- package/dist/exporter/delivery-response.d.ts +37 -0
- package/dist/exporter/delivery-response.js +151 -0
- package/dist/exporter/diagnostics.d.ts +42 -0
- package/dist/exporter/diagnostics.js +148 -0
- package/dist/exporter/event-id.d.ts +10 -0
- package/dist/exporter/event-id.js +75 -0
- package/dist/exporter/frontend-resource.d.ts +20 -0
- package/dist/exporter/frontend-resource.js +52 -0
- package/dist/exporter/local-minimization.d.ts +29 -0
- package/dist/exporter/local-minimization.js +87 -0
- package/dist/exporter/observation-source-event-exporter.d.ts +69 -0
- package/dist/exporter/observation-source-event-exporter.js +154 -0
- package/dist/exporter/parameter-snapshot.d.ts +14 -0
- package/dist/exporter/parameter-snapshot.js +215 -0
- package/dist/exporter/path-template.d.ts +1 -0
- package/dist/exporter/path-template.js +67 -0
- package/dist/exporter/raw-event-to-source-signal.d.ts +12 -0
- package/dist/exporter/raw-event-to-source-signal.js +297 -0
- package/dist/exporter/retry.d.ts +8 -0
- package/dist/exporter/retry.js +8 -0
- package/dist/exporter/sanitize.d.ts +30 -0
- package/dist/exporter/sanitize.js +177 -0
- package/dist/exporter/schema-evidence.d.ts +20 -0
- package/dist/exporter/schema-evidence.js +232 -0
- package/dist/exporter/source-event.d.ts +99 -0
- package/dist/exporter/source-event.js +13 -0
- package/dist/exporter/span-to-source-signal.d.ts +8 -0
- package/dist/exporter/span-to-source-signal.js +174 -0
- package/dist/exporter/text-fingerprint.d.ts +9 -0
- package/dist/exporter/text-fingerprint.js +16 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +158 -0
- package/dist/instrumentation/action.d.ts +2 -0
- package/dist/instrumentation/action.js +171 -0
- package/dist/instrumentation/cluebase-authored-attributes.d.ts +4 -0
- package/dist/instrumentation/cluebase-authored-attributes.js +32 -0
- package/dist/instrumentation/common.d.ts +73 -0
- package/dist/instrumentation/common.js +90 -0
- package/dist/instrumentation/dom.d.ts +63 -0
- package/dist/instrumentation/dom.js +640 -0
- package/dist/instrumentation/drag.d.ts +2 -0
- package/dist/instrumentation/drag.js +82 -0
- package/dist/instrumentation/error.d.ts +2 -0
- package/dist/instrumentation/error.js +318 -0
- package/dist/instrumentation/form.d.ts +2 -0
- package/dist/instrumentation/form.js +132 -0
- package/dist/instrumentation/input.d.ts +2 -0
- package/dist/instrumentation/input.js +180 -0
- package/dist/instrumentation/location.d.ts +11 -0
- package/dist/instrumentation/location.js +69 -0
- package/dist/instrumentation/naming-evidence.d.ts +20 -0
- package/dist/instrumentation/naming-evidence.js +11 -0
- package/dist/instrumentation/navigation.d.ts +2 -0
- package/dist/instrumentation/navigation.js +144 -0
- package/dist/instrumentation/network-request.d.ts +103 -0
- package/dist/instrumentation/network-request.js +285 -0
- package/dist/instrumentation/network-span-attributes.d.ts +25 -0
- package/dist/instrumentation/network-span-attributes.js +153 -0
- package/dist/instrumentation/network.d.ts +16 -0
- package/dist/instrumentation/network.js +502 -0
- package/dist/instrumentation/page.d.ts +2 -0
- package/dist/instrumentation/page.js +56 -0
- package/dist/instrumentation/performance.d.ts +2 -0
- package/dist/instrumentation/performance.js +365 -0
- package/dist/integrations/next-router.d.ts +16 -0
- package/dist/integrations/next-router.js +18 -0
- package/dist/integrations/react-router.d.ts +7 -0
- package/dist/integrations/react-router.js +61 -0
- package/dist/internal/internal-log.d.ts +23 -0
- package/dist/internal/internal-log.js +47 -0
- package/dist/internal/metrics.d.ts +10 -0
- package/dist/internal/metrics.js +50 -0
- package/dist/provider/_version.d.ts +1 -0
- package/dist/provider/_version.js +3 -0
- package/dist/provider/config.d.ts +12 -0
- package/dist/provider/config.js +177 -0
- package/dist/provider/constants.d.ts +62 -0
- package/dist/provider/constants.js +118 -0
- package/dist/provider/contracts.d.ts +51 -0
- package/dist/provider/contracts.js +46 -0
- package/dist/provider/privacy-policy-store.d.ts +20 -0
- package/dist/provider/privacy-policy-store.js +56 -0
- package/dist/provider/sampling.d.ts +2 -0
- package/dist/provider/sampling.js +17 -0
- package/dist/provider/sdk-event-pipeline.d.ts +60 -0
- package/dist/provider/sdk-event-pipeline.js +478 -0
- package/dist/provider/sdk-input-normalization.d.ts +7 -0
- package/dist/provider/sdk-input-normalization.js +113 -0
- package/dist/provider/sdk-otel-pipeline.d.ts +14 -0
- package/dist/provider/sdk-otel-pipeline.js +148 -0
- package/dist/provider/sdk-runtime-surfaces.d.ts +49 -0
- package/dist/provider/sdk-runtime-surfaces.js +216 -0
- package/dist/provider/sdk.d.ts +2 -0
- package/dist/provider/sdk.js +614 -0
- package/dist/provider/types.d.ts +323 -0
- package/dist/provider/types.js +1 -0
- package/dist/provider/usage-governor.d.ts +7 -0
- package/dist/provider/usage-governor.js +116 -0
- package/package.json +59 -0
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
import { CONSENT_DENIED_ABORT_REASON } from "../provider/constants.js";
|
|
2
|
+
import { BrowserOutbox, } from "./browser-outbox.js";
|
|
3
|
+
import { ownEvent } from "./browser-outbox-records.js";
|
|
4
|
+
import { DiagnosticStore } from "./diagnostics.js";
|
|
5
|
+
export class BatchTransport {
|
|
6
|
+
constructor(options) {
|
|
7
|
+
this.activeSendController = null;
|
|
8
|
+
this.closeDeadlineController = null;
|
|
9
|
+
this.closeDeadlineTimer = null;
|
|
10
|
+
this.closeDeadlineAt = null;
|
|
11
|
+
this.closePermitOrdinal = null;
|
|
12
|
+
this.flushDeadlineController = null;
|
|
13
|
+
this.flushDeadlineTimer = null;
|
|
14
|
+
this.flushDeadlineAt = null;
|
|
15
|
+
this.lifecycle = "open";
|
|
16
|
+
this.closeHadTerminalDisposition = false;
|
|
17
|
+
// Promise tracking the currently executing flush cycle. Awaiters that
|
|
18
|
+
// arrive while a flush is in flight await this promise so the SDK's public
|
|
19
|
+
// `flush()` resolves after token issuance and canonical ingest have
|
|
20
|
+
// completed.
|
|
21
|
+
this.activeFlushPromise = null;
|
|
22
|
+
// A public enqueue cannot await Web Locks, so it appends its durable draft
|
|
23
|
+
// mutation here. A flush snapshots and awaits this chain before acquiring
|
|
24
|
+
// the same project lock, which prevents a flush from racing an enqueue.
|
|
25
|
+
this.enqueueTail = Promise.resolve();
|
|
26
|
+
// Capture modules and OTel span export both feed source signals into this
|
|
27
|
+
// transport. The bounded ring prevents duplicate event_id rows from being
|
|
28
|
+
// sent to ingest.
|
|
29
|
+
this.seenEventIds = new Set();
|
|
30
|
+
this.enabled = options.enabled;
|
|
31
|
+
this.flushIntervalMs = Math.max(100, options.flushIntervalMs);
|
|
32
|
+
this.maxPayloadBytes = Math.max(1024, options.maxPayloadBytes);
|
|
33
|
+
this.flushThresholdBytes = Math.min(this.maxPayloadBytes, Math.max(1024, options.flushThresholdBytes ?? Math.floor(this.maxPayloadBytes * 0.8)));
|
|
34
|
+
this.client = options.client;
|
|
35
|
+
this.metrics = options.metrics ?? null;
|
|
36
|
+
this.diagnostics = options.diagnostics ?? new DiagnosticStore();
|
|
37
|
+
this.onGovernorFeedback = options.onGovernorFeedback ?? null;
|
|
38
|
+
this.outbox = new BrowserOutbox({
|
|
39
|
+
projectKey: options.projectKey ?? "unconfigured",
|
|
40
|
+
initialConsent: options.initialConsent ?? "unknown",
|
|
41
|
+
metrics: this.metrics ?? undefined,
|
|
42
|
+
diagnostics: this.diagnostics,
|
|
43
|
+
maxEvents: options.queueMaxItems ?? 5000,
|
|
44
|
+
maxBytes: options.queueMaxBytes ?? 300 * 1024,
|
|
45
|
+
onConsentRevoked: () => this.abortActiveSendForConsent(),
|
|
46
|
+
...(options.projectKey ? {} : { storage: null }),
|
|
47
|
+
});
|
|
48
|
+
this.idleHandle = null;
|
|
49
|
+
this.flushing = false;
|
|
50
|
+
this.flushAgainAfterCurrent = false;
|
|
51
|
+
this.recordPendingBatchCount();
|
|
52
|
+
}
|
|
53
|
+
getDiagnostics() {
|
|
54
|
+
return this.diagnostics.snapshot();
|
|
55
|
+
}
|
|
56
|
+
start() {
|
|
57
|
+
if (!this.enabled) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
stop() {
|
|
62
|
+
if (this.idleHandle) {
|
|
63
|
+
clearTimeout(this.idleHandle);
|
|
64
|
+
this.idleHandle = null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
dispose() {
|
|
68
|
+
this.stop();
|
|
69
|
+
this.outbox.close();
|
|
70
|
+
}
|
|
71
|
+
currentConsent() {
|
|
72
|
+
return this.outbox.currentConsent();
|
|
73
|
+
}
|
|
74
|
+
capturePermit() {
|
|
75
|
+
return this.outbox.capturePermit();
|
|
76
|
+
}
|
|
77
|
+
setConsent(consent) {
|
|
78
|
+
if (consent === "denied") {
|
|
79
|
+
this.abortActiveSendForConsent();
|
|
80
|
+
}
|
|
81
|
+
const transition = this.outbox.setConsent(consent);
|
|
82
|
+
void this.queueOutboxMutation(async () => {
|
|
83
|
+
await this.outbox.withDeliveryLock(async () => this.outbox.commitConsentTransition(transition), { restore: false });
|
|
84
|
+
this.recordPendingBatchCount();
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
enqueue(event, permit) {
|
|
88
|
+
if (this.lifecycle !== "open" &&
|
|
89
|
+
!(this.lifecycle === "closing" &&
|
|
90
|
+
permit !== undefined &&
|
|
91
|
+
this.closePermitOrdinal !== null &&
|
|
92
|
+
permit.captureOrdinal < this.closePermitOrdinal)) {
|
|
93
|
+
this.metrics?.increment("dropped_count");
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
// Take ownership of the event before enqueue returns: `ownEvent` deep
|
|
97
|
+
// clones and validates it, so a caller mutating the object afterwards
|
|
98
|
+
// cannot taint what is retained and sent. Events without a usable
|
|
99
|
+
// event_id cannot be deduped or retained, so drop them here.
|
|
100
|
+
const collectionPermit = permit ?? this.outbox.capturePermit();
|
|
101
|
+
const owned = ownEvent(event, collectionPermit);
|
|
102
|
+
const eventId = event.event_id;
|
|
103
|
+
if (!owned || typeof eventId !== "string" || eventId.length === 0) {
|
|
104
|
+
this.metrics?.increment("dropped_count");
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
void this.queueOutboxMutation(async () => {
|
|
108
|
+
// Dedup by event_id with a bounded LRU so converged capture and OTel
|
|
109
|
+
// emission cannot produce duplicate ingest rows. Registration is
|
|
110
|
+
// committed on the serialized mutation queue only after retention
|
|
111
|
+
// succeeds, so a rejected retention (capacity or stale generation)
|
|
112
|
+
// leaves the id retriable instead of silently swallowing the retry.
|
|
113
|
+
if (this.seenEventIds.has(eventId)) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const retained = await this.outbox.withDeliveryLock(async () => this.outbox.addDraft(owned.value(), owned.permit));
|
|
117
|
+
if (!retained) {
|
|
118
|
+
this.metrics?.increment("dropped_count");
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
this.seenEventIds.add(eventId);
|
|
122
|
+
if (this.seenEventIds.size > BatchTransport.DEDUP_CAPACITY) {
|
|
123
|
+
// Drop the oldest entry to keep the set bounded. Set iteration
|
|
124
|
+
// order is insertion order in JS, so the first key is the oldest.
|
|
125
|
+
const oldest = this.seenEventIds.values().next().value;
|
|
126
|
+
if (oldest !== undefined)
|
|
127
|
+
this.seenEventIds.delete(oldest);
|
|
128
|
+
}
|
|
129
|
+
this.recordPendingBatchCount();
|
|
130
|
+
this.scheduleFlushDeadline();
|
|
131
|
+
if (!this.enabled) {
|
|
132
|
+
void this.flush();
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (this.outbox.estimatedDraftBytes((events, metadata) => this.client.measurePayloadBytes([...events], metadata), this.client.createBatchEnvelopeMetadata()) >= this.flushThresholdBytes) {
|
|
136
|
+
void this.flush();
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
scheduleFlushDeadline() {
|
|
141
|
+
if (!this.enabled) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (this.idleHandle) {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
this.idleHandle = setTimeout(() => {
|
|
148
|
+
this.idleHandle = null;
|
|
149
|
+
void this.flush();
|
|
150
|
+
}, this.flushIntervalMs);
|
|
151
|
+
}
|
|
152
|
+
beginFlushDeadline() {
|
|
153
|
+
const active = this.currentDeadline();
|
|
154
|
+
if (active)
|
|
155
|
+
return active;
|
|
156
|
+
const controller = new AbortController();
|
|
157
|
+
const deadlineAt = monotonicNow() + 5000;
|
|
158
|
+
this.flushDeadlineController = controller;
|
|
159
|
+
this.flushDeadlineAt = deadlineAt;
|
|
160
|
+
this.flushDeadlineTimer = setTimeout(() => {
|
|
161
|
+
controller.abort("shutdown_deadline");
|
|
162
|
+
this.activeSendController?.abort("shutdown_deadline");
|
|
163
|
+
}, 5000);
|
|
164
|
+
return { signal: controller.signal, deadlineAt };
|
|
165
|
+
}
|
|
166
|
+
endFlushDeadline(deadline) {
|
|
167
|
+
if (!deadline || this.flushDeadlineController?.signal !== deadline.signal)
|
|
168
|
+
return;
|
|
169
|
+
if (this.flushDeadlineTimer)
|
|
170
|
+
clearTimeout(this.flushDeadlineTimer);
|
|
171
|
+
this.flushDeadlineTimer = null;
|
|
172
|
+
this.flushDeadlineController = null;
|
|
173
|
+
this.flushDeadlineAt = null;
|
|
174
|
+
}
|
|
175
|
+
async flush(deadline = this.currentDeadline()) {
|
|
176
|
+
if (this.lifecycle === "closed")
|
|
177
|
+
return;
|
|
178
|
+
// Link onto the in-flight flush so awaiters resolve after the token and
|
|
179
|
+
// ingest pipeline has actually drained.
|
|
180
|
+
if (this.flushing) {
|
|
181
|
+
this.flushAgainAfterCurrent = true;
|
|
182
|
+
if (this.activeFlushPromise) {
|
|
183
|
+
await this.activeFlushPromise;
|
|
184
|
+
}
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
const enqueuesAtStart = this.enqueueTail;
|
|
188
|
+
await enqueuesAtStart;
|
|
189
|
+
if (this.flushing) {
|
|
190
|
+
this.flushAgainAfterCurrent = true;
|
|
191
|
+
if (this.activeFlushPromise)
|
|
192
|
+
await this.activeFlushPromise;
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
if (this.idleHandle) {
|
|
196
|
+
clearTimeout(this.idleHandle);
|
|
197
|
+
this.idleHandle = null;
|
|
198
|
+
}
|
|
199
|
+
this.flushing = true;
|
|
200
|
+
this.activeFlushPromise = this.runFlushUntilIdle(deadline);
|
|
201
|
+
try {
|
|
202
|
+
await this.activeFlushPromise;
|
|
203
|
+
}
|
|
204
|
+
finally {
|
|
205
|
+
this.activeFlushPromise = null;
|
|
206
|
+
this.flushing = false;
|
|
207
|
+
}
|
|
208
|
+
this.diagnostics.setFlushOutcome(this.outbox.pendingEventCount() === 0 ? "clean" : "failed");
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
async runFlushUntilIdle(deadline) {
|
|
212
|
+
while (true) {
|
|
213
|
+
// Enqueue mutations acquire the same project Web Lock as the flush
|
|
214
|
+
// cycle. Wait for mutations outside that lock; awaiting `enqueueTail`
|
|
215
|
+
// from inside `withDeliveryLock` deadlocks when a capture event arrives
|
|
216
|
+
// while the first batch is being sent.
|
|
217
|
+
const enqueuesAtCycleStart = this.enqueueTail;
|
|
218
|
+
await enqueuesAtCycleStart;
|
|
219
|
+
await this.outbox.withDeliveryLock(() => this.runFlushCycle(deadline));
|
|
220
|
+
const enqueuesAfterCycle = this.enqueueTail;
|
|
221
|
+
if (this.lifecycle !== "open" ||
|
|
222
|
+
(!this.flushAgainAfterCurrent &&
|
|
223
|
+
enqueuesAfterCycle === enqueuesAtCycleStart)) {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
this.flushAgainAfterCurrent = false;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
beginClose() {
|
|
230
|
+
if (this.lifecycle === "closed")
|
|
231
|
+
return null;
|
|
232
|
+
if (this.lifecycle === "closing" && this.closeDeadlineController) {
|
|
233
|
+
return this.closeDeadlineController.signal;
|
|
234
|
+
}
|
|
235
|
+
this.lifecycle = "closing";
|
|
236
|
+
this.closeHadTerminalDisposition = false;
|
|
237
|
+
this.diagnostics.setLifecycle("closing");
|
|
238
|
+
this.stop();
|
|
239
|
+
const controller = new AbortController();
|
|
240
|
+
this.closeDeadlineController = controller;
|
|
241
|
+
this.closeDeadlineAt = monotonicNow() + 5000;
|
|
242
|
+
this.closePermitOrdinal = this.outbox.capturePermit().captureOrdinal;
|
|
243
|
+
this.closeDeadlineTimer = setTimeout(() => {
|
|
244
|
+
controller.abort("shutdown_deadline");
|
|
245
|
+
this.activeSendController?.abort("shutdown_deadline");
|
|
246
|
+
}, 5000);
|
|
247
|
+
return controller.signal;
|
|
248
|
+
}
|
|
249
|
+
async close() {
|
|
250
|
+
if (this.lifecycle === "closed")
|
|
251
|
+
return;
|
|
252
|
+
const signal = this.beginClose();
|
|
253
|
+
if (!signal)
|
|
254
|
+
return;
|
|
255
|
+
try {
|
|
256
|
+
await this.flush();
|
|
257
|
+
}
|
|
258
|
+
finally {
|
|
259
|
+
this.clearCloseDeadline(signal);
|
|
260
|
+
}
|
|
261
|
+
if (this.outbox.pendingEventCount() > 0) {
|
|
262
|
+
this.lifecycle = "closing_failed";
|
|
263
|
+
this.diagnostics.setLifecycle("closing_failed");
|
|
264
|
+
this.diagnostics.setCloseOutcome("retryable_failure");
|
|
265
|
+
this.diagnostics.record({ code: "shutdown_failure" });
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
this.lifecycle = "closed";
|
|
269
|
+
this.diagnostics.setLifecycle("closed");
|
|
270
|
+
this.diagnostics.setCloseOutcome(this.closeHadTerminalDisposition ? "terminal_failure" : "clean");
|
|
271
|
+
}
|
|
272
|
+
async runFlushCycle(deadline) {
|
|
273
|
+
const fence = this.outbox.prepareFence({
|
|
274
|
+
maxPayloadBytes: this.maxPayloadBytes,
|
|
275
|
+
createMetadata: () => this.client.createBatchEnvelopeMetadata(),
|
|
276
|
+
measure: (events, metadata) => this.client.measurePayloadBytes([...events], metadata),
|
|
277
|
+
});
|
|
278
|
+
if (fence.oversized > 0)
|
|
279
|
+
this.recordPendingBatchCount();
|
|
280
|
+
while (true) {
|
|
281
|
+
const batch = this.outbox.headThrough(fence.targetOrdinal);
|
|
282
|
+
if (!batch) {
|
|
283
|
+
break;
|
|
284
|
+
}
|
|
285
|
+
if (batch.events.length === 0 ||
|
|
286
|
+
batch.events.some((event) => !this.outbox.permits(event.permit))) {
|
|
287
|
+
this.outbox.removeHead(batch);
|
|
288
|
+
this.recordPendingBatchCount();
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
this.metrics?.increment("batch_flush_count");
|
|
292
|
+
const result = await this.sendBatchWhilePermitted(batch, deadline);
|
|
293
|
+
if (!result || this.applyDeliveryResult(batch, result) === "break")
|
|
294
|
+
break;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
async sendBatchWhilePermitted(batch, deadline) {
|
|
298
|
+
const sendController = new AbortController();
|
|
299
|
+
const abortForClose = () => sendController.abort("shutdown_deadline");
|
|
300
|
+
if (deadline?.signal.aborted)
|
|
301
|
+
abortForClose();
|
|
302
|
+
deadline?.signal.addEventListener("abort", abortForClose, { once: true });
|
|
303
|
+
this.activeSendController = sendController;
|
|
304
|
+
let result;
|
|
305
|
+
try {
|
|
306
|
+
result = await this.client.send(batch.events.map((event) => event.value()), {
|
|
307
|
+
maxPayloadBytes: this.maxPayloadBytes,
|
|
308
|
+
metadata: batch.metadata,
|
|
309
|
+
signal: sendController.signal,
|
|
310
|
+
deadlineAt: deadline?.deadlineAt,
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
finally {
|
|
314
|
+
deadline?.signal.removeEventListener("abort", abortForClose);
|
|
315
|
+
if (this.activeSendController === sendController) {
|
|
316
|
+
this.activeSendController = null;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
if (batch.events.some((event) => !this.outbox.permits(event.permit))) {
|
|
320
|
+
this.recordPendingBatchCount();
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
323
|
+
return result;
|
|
324
|
+
}
|
|
325
|
+
applyDeliveryResult(batch, result) {
|
|
326
|
+
if (result.governor)
|
|
327
|
+
this.onGovernorFeedback?.(result.governor);
|
|
328
|
+
return result.ok
|
|
329
|
+
? this.applySuccessfulDelivery(batch, result)
|
|
330
|
+
: this.applyFailedDelivery(batch, result);
|
|
331
|
+
}
|
|
332
|
+
applyFailedDelivery(batch, result) {
|
|
333
|
+
if (result.retryStrategy === "split_batch") {
|
|
334
|
+
if (this.outbox.splitHead(batch, () => this.client.createBatchEnvelopeMetadata())) {
|
|
335
|
+
this.recordPendingBatchCount();
|
|
336
|
+
this.scheduleFlushDeadline();
|
|
337
|
+
return "continue";
|
|
338
|
+
}
|
|
339
|
+
this.metrics?.increment("dropped_count", batch.events.length);
|
|
340
|
+
this.metrics?.increment("oversized_event_dropped_count");
|
|
341
|
+
this.outbox.removeHead(batch);
|
|
342
|
+
this.recordPendingBatchCount();
|
|
343
|
+
return "continue";
|
|
344
|
+
}
|
|
345
|
+
if (this.shouldRetainFailedBatch(result.retryStrategy)) {
|
|
346
|
+
this.diagnostics.record({
|
|
347
|
+
code: "retry_exhausted",
|
|
348
|
+
strategy: "retry_same_batch",
|
|
349
|
+
attempt: result.attempts,
|
|
350
|
+
batch_fingerprint: this.diagnostics.batchFingerprint(batch.diagnosticCanonicalBytes),
|
|
351
|
+
});
|
|
352
|
+
this.outbox.retainHead(batch);
|
|
353
|
+
this.metrics?.increment("transport_requeue_count");
|
|
354
|
+
this.recordPendingBatchCount();
|
|
355
|
+
if (this.lifecycle === "open")
|
|
356
|
+
this.scheduleFlushDeadline();
|
|
357
|
+
return "break";
|
|
358
|
+
}
|
|
359
|
+
this.diagnostics.record({
|
|
360
|
+
code: result.retryStrategy === "drop_event"
|
|
361
|
+
? "intentional_drop"
|
|
362
|
+
: "terminal_rejection",
|
|
363
|
+
strategy: result.retryStrategy ?? undefined,
|
|
364
|
+
count: batch.events.length,
|
|
365
|
+
batch_fingerprint: this.diagnostics.batchFingerprint(batch.diagnosticCanonicalBytes),
|
|
366
|
+
});
|
|
367
|
+
if (this.lifecycle === "closing")
|
|
368
|
+
this.closeHadTerminalDisposition = true;
|
|
369
|
+
this.metrics?.increment("dropped_count", batch.events.length);
|
|
370
|
+
this.metrics?.increment("transport_failed_count");
|
|
371
|
+
if (!this.outbox.removeHead(batch)) {
|
|
372
|
+
this.metrics?.increment("transport_requeue_count");
|
|
373
|
+
this.recordPendingBatchCount();
|
|
374
|
+
return "break";
|
|
375
|
+
}
|
|
376
|
+
this.recordPendingBatchCount();
|
|
377
|
+
return "continue";
|
|
378
|
+
}
|
|
379
|
+
applySuccessfulDelivery(batch, result) {
|
|
380
|
+
if (!this.outbox.removeHead(batch)) {
|
|
381
|
+
this.metrics?.increment("transport_requeue_count");
|
|
382
|
+
this.recordPendingBatchCount();
|
|
383
|
+
return "break";
|
|
384
|
+
}
|
|
385
|
+
if (result.partial) {
|
|
386
|
+
if (this.lifecycle === "closing")
|
|
387
|
+
this.closeHadTerminalDisposition = true;
|
|
388
|
+
this.diagnostics.record({
|
|
389
|
+
code: "intentional_drop",
|
|
390
|
+
count: result.droppedEventCount ?? 0,
|
|
391
|
+
batch_fingerprint: this.diagnostics.batchFingerprint(batch.diagnosticCanonicalBytes),
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
this.recordPendingBatchCount();
|
|
395
|
+
return "continue";
|
|
396
|
+
}
|
|
397
|
+
flushOnUnload() {
|
|
398
|
+
if (!this.client.hasUsableBrowserToken()) {
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
this.stop();
|
|
402
|
+
void this.queueOutboxMutation(async () => {
|
|
403
|
+
await this.outbox.withDeliveryLock(async () => {
|
|
404
|
+
const fence = this.outbox.prepareFence({
|
|
405
|
+
maxPayloadBytes: this.maxPayloadBytes,
|
|
406
|
+
createMetadata: () => this.client.createBatchEnvelopeMetadata(),
|
|
407
|
+
measure: (events, metadata) => this.client.measurePayloadBytes([...events], metadata),
|
|
408
|
+
});
|
|
409
|
+
this.recordPendingBatchCount();
|
|
410
|
+
const batch = this.outbox.headThrough(fence.targetOrdinal);
|
|
411
|
+
if (!batch)
|
|
412
|
+
return;
|
|
413
|
+
const sendController = new AbortController();
|
|
414
|
+
this.activeSendController = sendController;
|
|
415
|
+
try {
|
|
416
|
+
await this.client.send(batch.events.map((event) => event.value()), {
|
|
417
|
+
keepalive: true,
|
|
418
|
+
useBeacon: false,
|
|
419
|
+
requireCachedBrowserToken: true,
|
|
420
|
+
maxPayloadBytes: this.maxPayloadBytes,
|
|
421
|
+
metadata: batch.metadata,
|
|
422
|
+
signal: sendController.signal,
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
finally {
|
|
426
|
+
if (this.activeSendController === sendController) {
|
|
427
|
+
this.activeSendController = null;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
clear() {
|
|
434
|
+
this.activeSendController?.abort();
|
|
435
|
+
this.activeSendController = null;
|
|
436
|
+
this.outbox.clearMemory();
|
|
437
|
+
void this.queueOutboxMutation(async () => {
|
|
438
|
+
await this.outbox.withDeliveryLock(async () => this.outbox.clear());
|
|
439
|
+
});
|
|
440
|
+
this.recordPendingBatchCount();
|
|
441
|
+
}
|
|
442
|
+
abortActiveSendForConsent() {
|
|
443
|
+
this.activeSendController?.abort(CONSENT_DENIED_ABORT_REASON);
|
|
444
|
+
this.activeSendController = null;
|
|
445
|
+
}
|
|
446
|
+
recordPendingBatchCount() {
|
|
447
|
+
this.metrics?.set("pending_batch_count", this.outbox.pendingEventCount());
|
|
448
|
+
}
|
|
449
|
+
clearCloseDeadline(signal) {
|
|
450
|
+
if (this.closeDeadlineController?.signal !== signal)
|
|
451
|
+
return;
|
|
452
|
+
if (this.closeDeadlineTimer) {
|
|
453
|
+
clearTimeout(this.closeDeadlineTimer);
|
|
454
|
+
this.closeDeadlineTimer = null;
|
|
455
|
+
}
|
|
456
|
+
this.closeDeadlineController = null;
|
|
457
|
+
this.closeDeadlineAt = null;
|
|
458
|
+
this.closePermitOrdinal = null;
|
|
459
|
+
}
|
|
460
|
+
currentDeadline() {
|
|
461
|
+
if (this.closeDeadlineController && this.closeDeadlineAt !== null) {
|
|
462
|
+
return {
|
|
463
|
+
signal: this.closeDeadlineController.signal,
|
|
464
|
+
deadlineAt: this.closeDeadlineAt,
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
if (this.flushDeadlineController && this.flushDeadlineAt !== null) {
|
|
468
|
+
return {
|
|
469
|
+
signal: this.flushDeadlineController.signal,
|
|
470
|
+
deadlineAt: this.flushDeadlineAt,
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
return null;
|
|
474
|
+
}
|
|
475
|
+
queueOutboxMutation(operation) {
|
|
476
|
+
const next = this.enqueueTail.then(operation, operation);
|
|
477
|
+
this.enqueueTail = next.catch(() => undefined);
|
|
478
|
+
return next;
|
|
479
|
+
}
|
|
480
|
+
shouldRetainFailedBatch(retryStrategy) {
|
|
481
|
+
return retryStrategy === null || retryStrategy === "retry_same_batch";
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
BatchTransport.DEDUP_CAPACITY = 1024;
|
|
485
|
+
const monotonicNow = () => typeof performance !== "undefined" ? performance.now() : Date.now();
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import type { BatchEnvelopeMetadata } from "./client.js";
|
|
2
|
+
import type { CluebaseConsentState } from "../provider/types.js";
|
|
3
|
+
import { type BrowserSourceEvent } from "./source-event.js";
|
|
4
|
+
export type StorageLike = Pick<Storage, "getItem" | "setItem" | "removeItem" | "key"> & {
|
|
5
|
+
readonly length: number;
|
|
6
|
+
};
|
|
7
|
+
export type CollectionPermit = {
|
|
8
|
+
readonly generation: string;
|
|
9
|
+
readonly permittedAtStart: boolean;
|
|
10
|
+
readonly captureOrdinal: number;
|
|
11
|
+
};
|
|
12
|
+
export type OwnedBrowserEvent = {
|
|
13
|
+
readonly byteLength: number;
|
|
14
|
+
readonly permit: CollectionPermit;
|
|
15
|
+
readonly diagnosticCanonicalBytes: string;
|
|
16
|
+
value(): BrowserSourceEvent;
|
|
17
|
+
};
|
|
18
|
+
export type PendingBrowserBatch = {
|
|
19
|
+
readonly events: readonly OwnedBrowserEvent[];
|
|
20
|
+
readonly metadata: BatchEnvelopeMetadata;
|
|
21
|
+
readonly generation: string;
|
|
22
|
+
readonly ordinal: number;
|
|
23
|
+
readonly diagnosticCanonicalBytes: string;
|
|
24
|
+
readonly recordKey: string;
|
|
25
|
+
readonly ownerBatchId: string;
|
|
26
|
+
readonly planOrdinal: number | null;
|
|
27
|
+
};
|
|
28
|
+
type RecordBase = {
|
|
29
|
+
readonly storageVersion: 2;
|
|
30
|
+
readonly projectHash: string;
|
|
31
|
+
readonly createdAt: string;
|
|
32
|
+
readonly recordVerifierSha256: string | null;
|
|
33
|
+
};
|
|
34
|
+
export type OwnedStoredEvent = {
|
|
35
|
+
readonly eventId: string;
|
|
36
|
+
readonly eventKeyHash: string;
|
|
37
|
+
readonly recordVerifierSha256: string;
|
|
38
|
+
readonly event: BrowserSourceEvent;
|
|
39
|
+
readonly permittedAtStart: boolean;
|
|
40
|
+
};
|
|
41
|
+
export type DraftRecord = RecordBase & {
|
|
42
|
+
readonly kind: "draft";
|
|
43
|
+
readonly origin: "v2" | "legacy_v1";
|
|
44
|
+
readonly eventId: string;
|
|
45
|
+
readonly eventKeyHash: string;
|
|
46
|
+
readonly event: BrowserSourceEvent;
|
|
47
|
+
readonly collectionGeneration: string;
|
|
48
|
+
readonly permittedAtStart: boolean;
|
|
49
|
+
readonly checksumSha256: string;
|
|
50
|
+
};
|
|
51
|
+
export type PendingRecord = RecordBase & {
|
|
52
|
+
readonly kind: "pending";
|
|
53
|
+
readonly batchId: string;
|
|
54
|
+
readonly idempotencyKey: string;
|
|
55
|
+
readonly sentAt: string;
|
|
56
|
+
readonly collectionGeneration: string;
|
|
57
|
+
readonly events: readonly OwnedStoredEvent[];
|
|
58
|
+
readonly eventKeyHashes: readonly string[];
|
|
59
|
+
readonly lifetimeAttemptTotal: number;
|
|
60
|
+
readonly ordinal: number;
|
|
61
|
+
readonly checksumSha256: string;
|
|
62
|
+
};
|
|
63
|
+
export type SplitPlanRecord = RecordBase & {
|
|
64
|
+
readonly kind: "split_plan";
|
|
65
|
+
readonly batchId: string;
|
|
66
|
+
readonly idempotencyKey: string;
|
|
67
|
+
readonly sentAt: string;
|
|
68
|
+
readonly collectionGeneration: string;
|
|
69
|
+
readonly events: readonly OwnedStoredEvent[];
|
|
70
|
+
readonly eventKeyHashes: readonly string[];
|
|
71
|
+
readonly lifetimeAttemptTotal: number;
|
|
72
|
+
readonly ordinal: number;
|
|
73
|
+
readonly children: readonly SplitPlanChild[];
|
|
74
|
+
readonly nextOrdinal: number;
|
|
75
|
+
readonly checksumSha256: string;
|
|
76
|
+
};
|
|
77
|
+
export type SplitPlanChild = {
|
|
78
|
+
readonly start: number;
|
|
79
|
+
readonly end: number;
|
|
80
|
+
readonly metadata: BatchEnvelopeMetadata;
|
|
81
|
+
};
|
|
82
|
+
export type TombstoneRecord = RecordBase & {
|
|
83
|
+
readonly kind: "legacy_tombstone";
|
|
84
|
+
readonly eventKeyHash: string;
|
|
85
|
+
readonly importedAt: string;
|
|
86
|
+
readonly checksumSha256: string;
|
|
87
|
+
};
|
|
88
|
+
export type ConsentRecord = RecordBase & {
|
|
89
|
+
readonly kind: "consent";
|
|
90
|
+
readonly state: CluebaseConsentState;
|
|
91
|
+
readonly collectionGeneration: string;
|
|
92
|
+
readonly legacyImportBlocked: boolean;
|
|
93
|
+
readonly updatedAt: string;
|
|
94
|
+
readonly checksumSha256: string;
|
|
95
|
+
};
|
|
96
|
+
export type OutboxRecord = DraftRecord | PendingRecord | SplitPlanRecord | TombstoneRecord | ConsentRecord;
|
|
97
|
+
export type PendingRecordLike = PendingRecord | SplitPlanRecord;
|
|
98
|
+
type StoredWithoutChecksum = Omit<OutboxRecord, "checksumSha256">;
|
|
99
|
+
export declare const UNANCHORED_COLLECTION_GENERATION = "__cluebase_unanchored_generation__";
|
|
100
|
+
export declare const LEGACY_TOMBSTONE_LIMIT = 10000;
|
|
101
|
+
export declare const createDraft: (input: {
|
|
102
|
+
readonly event: BrowserSourceEvent;
|
|
103
|
+
readonly permit: CollectionPermit;
|
|
104
|
+
readonly projectHash: string;
|
|
105
|
+
readonly createdAt: string;
|
|
106
|
+
readonly origin?: "v2" | "legacy_v1";
|
|
107
|
+
}) => DraftRecord | null;
|
|
108
|
+
export declare const createLegacyTombstone: (input: {
|
|
109
|
+
readonly projectHash: string;
|
|
110
|
+
readonly eventKeyHash: string;
|
|
111
|
+
readonly verifier: string;
|
|
112
|
+
}) => TombstoneRecord;
|
|
113
|
+
export declare const createConsentRecord: (input: {
|
|
114
|
+
readonly projectHash: string;
|
|
115
|
+
readonly state: CluebaseConsentState;
|
|
116
|
+
readonly collectionGeneration: string;
|
|
117
|
+
readonly legacyImportBlocked: boolean;
|
|
118
|
+
}) => ConsentRecord;
|
|
119
|
+
export declare const createPending: (input: {
|
|
120
|
+
readonly projectHash: string;
|
|
121
|
+
readonly metadata: BatchEnvelopeMetadata;
|
|
122
|
+
readonly drafts: readonly DraftRecord[];
|
|
123
|
+
readonly ordinal: number;
|
|
124
|
+
}) => PendingRecord;
|
|
125
|
+
export declare const toPendingBatch: (record: PendingRecordLike, recordKey: string, events: readonly OwnedStoredEvent[], metadata: BatchEnvelopeMetadata, planOrdinal: number | null) => PendingBrowserBatch;
|
|
126
|
+
export declare const toHeadBatch: (record: PendingRecordLike, recordKey: string) => PendingBrowserBatch | null;
|
|
127
|
+
export declare const splitPendingRecord: (record: PendingRecordLike, createMetadata: () => BatchEnvelopeMetadata) => SplitPlanRecord | null;
|
|
128
|
+
export declare const largestFittingPrefix: (drafts: readonly DraftRecord[], maxBytes: number, metadata: BatchEnvelopeMetadata, measure: (events: readonly BrowserSourceEvent[], metadata: BatchEnvelopeMetadata) => number) => number;
|
|
129
|
+
export declare const ownEvent: (event: BrowserSourceEvent, permit: CollectionPermit) => OwnedBrowserEvent | null;
|
|
130
|
+
export declare const isOwnedEvent: (value: OwnedBrowserEvent | null) => value is OwnedBrowserEvent;
|
|
131
|
+
export declare const recordBatchId: (batch: PendingBrowserBatch) => string;
|
|
132
|
+
export declare const withChecksum: <T extends StoredWithoutChecksum>(record: T) => T & {
|
|
133
|
+
readonly checksumSha256: string;
|
|
134
|
+
};
|
|
135
|
+
export declare const withoutChecksum: <T extends OutboxRecord>(record: T) => Omit<T, "checksumSha256">;
|
|
136
|
+
export declare const parseRecord: (raw: string, projectHash: string) => OutboxRecord | null;
|
|
137
|
+
export declare const isRecord: (value: unknown) => value is Record<string, unknown>;
|
|
138
|
+
export declare const sha256Hex: (value: string) => string;
|
|
139
|
+
export declare const keyHash: (id: string) => string;
|
|
140
|
+
export declare const verifyHash: (id: string) => string;
|
|
141
|
+
export declare const nowIso: () => string;
|
|
142
|
+
export declare const consentRecordKey: (prefix: string) => string;
|
|
143
|
+
export declare const draftRecordKey: (prefix: string, eventKeyHash: string) => string;
|
|
144
|
+
export declare const pendingRecordKey: (prefix: string, batchId: string) => string;
|
|
145
|
+
export declare const tombstoneRecordKey: (prefix: string, eventKeyHash: string) => string;
|
|
146
|
+
export declare const browserEventsByteLength: (events: readonly BrowserSourceEvent[]) => number;
|
|
147
|
+
export declare const outboxEventCount: (drafts: readonly DraftRecord[], pending: readonly PendingRecordLike[]) => number;
|
|
148
|
+
export declare const hasLegacyTombstone: (draft: DraftRecord, tombstones: readonly TombstoneRecord[]) => boolean;
|
|
149
|
+
export declare const createGeneration: () => string;
|
|
150
|
+
export declare const readStorage: () => StorageLike | null;
|
|
151
|
+
export type LockManagerLike = {
|
|
152
|
+
request<T>(name: string, options: {
|
|
153
|
+
readonly mode: "exclusive";
|
|
154
|
+
}, callback: () => Promise<T>): Promise<T>;
|
|
155
|
+
};
|
|
156
|
+
export declare const readLocks: () => LockManagerLike | null;
|
|
157
|
+
export declare const createConsentChannel: (projectHash: string, onDenied: (generation: string) => void) => BroadcastChannel | null;
|
|
158
|
+
export declare const addStorageListener: (key: string, onChanged: () => void) => (() => void);
|
|
159
|
+
export {};
|