@genn-inc/cluebase-frontend-sdk 0.0.6 → 0.0.9

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.
Files changed (69) hide show
  1. package/dist/_generated/correlation-attribute-keys.d.ts +3 -0
  2. package/dist/_generated/correlation-attribute-keys.js +35 -0
  3. package/dist/_generated/diagnostic-codes.d.ts +4 -0
  4. package/dist/_generated/diagnostic-codes.js +48 -0
  5. package/dist/_generated/drop-reasons.d.ts +2 -0
  6. package/dist/_generated/drop-reasons.js +25 -0
  7. package/dist/_generated/ingest-routes.d.ts +5 -0
  8. package/dist/_generated/ingest-routes.js +12 -0
  9. package/dist/context/trace.js +2 -4
  10. package/dist/exporter/batch.d.ts +23 -26
  11. package/dist/exporter/batch.js +254 -289
  12. package/dist/exporter/browser-outbox-records.d.ts +7 -24
  13. package/dist/exporter/browser-outbox-records.js +6 -94
  14. package/dist/exporter/browser-outbox.d.ts +23 -28
  15. package/dist/exporter/browser-outbox.js +99 -211
  16. package/dist/exporter/client.d.ts +24 -34
  17. package/dist/exporter/client.js +107 -318
  18. package/dist/exporter/delivery-pipeline.d.ts +72 -0
  19. package/dist/exporter/delivery-pipeline.js +199 -0
  20. package/dist/exporter/delivery-response-contract.d.ts +5 -81
  21. package/dist/exporter/delivery-response-contract.js +4 -166
  22. package/dist/exporter/delivery-response.d.ts +8 -4
  23. package/dist/exporter/delivery-response.js +62 -133
  24. package/dist/exporter/diagnostics.d.ts +2 -1
  25. package/dist/exporter/diagnostics.js +1 -25
  26. package/dist/exporter/event-id.d.ts +1 -0
  27. package/dist/exporter/event-id.js +3 -0
  28. package/dist/exporter/observation-source-event-exporter.d.ts +1 -1
  29. package/dist/exporter/observation-source-event-exporter.js +2 -2
  30. package/dist/exporter/parameter-encryption.d.ts +10 -0
  31. package/dist/exporter/parameter-encryption.js +219 -0
  32. package/dist/exporter/parameter-snapshot.js +25 -3
  33. package/dist/exporter/sdk-core-delivery.d.ts +226 -0
  34. package/dist/exporter/sdk-core-delivery.js +97 -0
  35. package/dist/exporter/sdk-core-wasm.d.ts +24 -1
  36. package/dist/exporter/sdk-core-wasm.js +31 -3
  37. package/dist/exporter/source-event.d.ts +2 -0
  38. package/dist/exporter/source-event.js +17 -0
  39. package/dist/exporter/span-to-source-signal.d.ts +1 -1
  40. package/dist/exporter/span-to-source-signal.js +20 -140
  41. package/dist/index.d.ts +3 -3
  42. package/dist/index.js +5 -3
  43. package/dist/instrumentation/network.d.ts +1 -1
  44. package/dist/instrumentation/network.js +17 -0
  45. package/dist/provider/_version.d.ts +1 -1
  46. package/dist/provider/_version.js +1 -1
  47. package/dist/provider/config.js +16 -3
  48. package/dist/provider/constants.js +0 -1
  49. package/dist/provider/contracts.d.ts +2 -0
  50. package/dist/provider/contracts.js +2 -0
  51. package/dist/provider/lifecycle-event-dedup.d.ts +7 -0
  52. package/dist/provider/lifecycle-event-dedup.js +31 -0
  53. package/dist/provider/sdk-encryption-store.d.ts +7 -0
  54. package/dist/provider/sdk-encryption-store.js +15 -0
  55. package/dist/provider/sdk-event-pipeline.d.ts +1 -3
  56. package/dist/provider/sdk-event-pipeline.js +0 -15
  57. package/dist/provider/sdk-otel-pipeline.d.ts +2 -1
  58. package/dist/provider/sdk-otel-pipeline.js +2 -11
  59. package/dist/provider/sdk-runtime-surfaces.d.ts +3 -2
  60. package/dist/provider/sdk-runtime-surfaces.js +7 -6
  61. package/dist/provider/sdk.js +88 -54
  62. package/dist/provider/types.d.ts +14 -19
  63. package/dist/wasm/pkg-web/cluebase_sdk_core_wasm.d.ts +18 -9
  64. package/dist/wasm/pkg-web/cluebase_sdk_core_wasm.js +170 -29
  65. package/dist/wasm/pkg-web/cluebase_sdk_core_wasm_bg.wasm +0 -0
  66. package/dist/wasm/pkg-web/cluebase_sdk_core_wasm_bg.wasm.d.ts +10 -9
  67. package/package.json +1 -1
  68. package/dist/provider/usage-governor.d.ts +0 -7
  69. package/dist/provider/usage-governor.js +0 -114
@@ -1,22 +1,32 @@
1
- import { CONSENT_DENIED_ABORT_REASON, DEFAULT_CONFIG, } from "../provider/constants.js";
2
- import { BrowserOutbox, } from "./browser-outbox.js";
3
- import { ownEvent } from "./browser-outbox-records.js";
1
+ import { DEFAULT_CONFIG } from "../provider/constants.js";
2
+ import { applyDeliveredPrivacyPolicy, resetPrivacyPolicy, } from "../provider/privacy-policy-store.js";
3
+ import { applyDeliveredSdkEncryption, resetSdkEncryption, } from "../provider/sdk-encryption-store.js";
4
+ import { BrowserOutbox } from "./browser-outbox.js";
5
+ import { ownEvent, sentByteLength } from "./browser-outbox-records.js";
6
+ import { DeliveryPipeline, } from "./delivery-pipeline.js";
4
7
  import { DiagnosticStore } from "./diagnostics.js";
8
+ import { settlePendingEncryptions } from "./parameter-encryption.js";
9
+ const CLOSE_DEADLINE_MS = 5000;
10
+ const CORE_CONSENT = {
11
+ unknown: "unknown",
12
+ granted: "granted",
13
+ denied: "denied",
14
+ };
5
15
  export class BatchTransport {
6
16
  constructor(options) {
7
- this.activeSendController = null;
8
- this.closeDeadlineController = null;
9
- this.closeDeadlineTimer = null;
17
+ this.idleHandle = null;
18
+ this.flushing = false;
19
+ this.flushAgainAfterCurrent = false;
10
20
  this.closeDeadlineAt = null;
11
- this.closePermitOrdinal = null;
12
- this.flushDeadlineController = null;
13
- this.flushDeadlineTimer = null;
14
- this.flushDeadlineAt = null;
21
+ this.clearedAt = null;
22
+ this.disposed = false;
23
+ this.closeDeadlineTimer = null;
15
24
  this.lifecycle = "open";
16
25
  this.closeHadTerminalDisposition = false;
26
+ this.restored = false;
27
+ this.tokenRefusedForGood = false;
17
28
  this.activeFlushPromise = null;
18
29
  this.enqueueTail = Promise.resolve();
19
- this.seenEventIds = new Set();
20
30
  this.enabled = options.enabled;
21
31
  this.flushIntervalMs = Math.max(100, options.flushIntervalMs);
22
32
  this.maxPayloadBytes = Math.max(1024, options.maxPayloadBytes);
@@ -25,29 +35,44 @@ export class BatchTransport {
25
35
  this.client = options.client;
26
36
  this.metrics = options.metrics ?? null;
27
37
  this.diagnostics = options.diagnostics ?? new DiagnosticStore();
28
- this.onGovernorFeedback = options.onGovernorFeedback ?? null;
29
38
  this.outbox = new BrowserOutbox({
30
39
  projectKey: options.projectKey ?? "unconfigured",
31
40
  initialConsent: options.initialConsent ?? "unknown",
32
41
  metrics: this.metrics ?? undefined,
33
42
  diagnostics: this.diagnostics,
34
- maxEvents: options.queueMaxItems ?? 5000,
35
- maxBytes: options.queueMaxBytes ?? 300 * 1024,
36
43
  onConsentRevoked: () => this.abortActiveSendForConsent(),
37
44
  ...(options.projectKey ? {} : { storage: null }),
38
45
  });
39
- this.idleHandle = null;
40
- this.flushing = false;
41
- this.flushAgainAfterCurrent = false;
46
+ this.pipeline = new DeliveryPipeline({
47
+ endpoint: options.endpoint,
48
+ projectKey: options.projectKey ?? "unconfigured",
49
+ sdkVersion: options.sdkVersion,
50
+ sourceSchemaVersion: options.sourceSchemaVersion,
51
+ sdkRequest: options.sdkRequest,
52
+ queueLimits: {
53
+ max_payload_bytes: this.maxPayloadBytes,
54
+ max_buffer_bytes: options.queueMaxBytes ?? 300 * 1024,
55
+ max_buffer_events: options.queueMaxItems ?? 5000,
56
+ },
57
+ ...(options.requestTimeoutMs === undefined
58
+ ? {}
59
+ : { requestTimeoutMs: options.requestTimeoutMs }),
60
+ initialConsent: CORE_CONSENT[options.initialConsent ?? "unknown"],
61
+ transport: (request) => this.client.perform(request),
62
+ clock: {
63
+ nowMs: monotonicNow,
64
+ sleep: (delayMs) => new Promise((resolve) => setTimeout(resolve, delayMs)),
65
+ },
66
+ });
67
+ this.adoptStoredRecords();
42
68
  this.recordPendingBatchCount();
43
69
  }
44
70
  getDiagnostics() {
45
71
  return this.diagnostics.snapshot();
46
72
  }
47
73
  start() {
48
- if (!this.enabled) {
74
+ if (!this.enabled)
49
75
  return;
50
- }
51
76
  }
52
77
  stop() {
53
78
  if (this.idleHandle) {
@@ -56,55 +81,65 @@ export class BatchTransport {
56
81
  }
57
82
  }
58
83
  dispose() {
84
+ this.disposed = true;
59
85
  this.stop();
60
86
  this.outbox.close();
87
+ this.pipeline.dispose();
61
88
  }
62
89
  currentConsent() {
63
90
  return this.outbox.currentConsent();
64
91
  }
65
92
  capturePermit() {
66
- return this.outbox.capturePermit();
93
+ return this.pipeline.permit();
67
94
  }
68
95
  setConsent(consent) {
69
- if (consent === "denied") {
96
+ if (consent === "denied")
70
97
  this.abortActiveSendForConsent();
71
- }
72
98
  const transition = this.outbox.setConsent(consent);
99
+ const report = this.pipeline.declareConsent(CORE_CONSENT[consent]);
100
+ this.outbox.releaseEvents(report.purged_event_keys);
101
+ for (const entry of report.diagnostics)
102
+ this.diagnostics.record(entry);
73
103
  void this.queueOutboxMutation(async () => {
74
104
  await this.outbox.withDeliveryLock(async () => this.outbox.commitConsentTransition(transition), { restore: false });
75
105
  this.recordPendingBatchCount();
76
106
  });
77
107
  }
78
108
  enqueue(event, permit) {
79
- if (this.lifecycle !== "open" &&
80
- !(this.lifecycle === "closing" &&
81
- permit !== undefined &&
82
- this.closePermitOrdinal !== null &&
83
- permit.captureOrdinal < this.closePermitOrdinal)) {
84
- this.metrics?.increment("dropped_count");
85
- return;
86
- }
87
- const collectionPermit = permit ?? this.outbox.capturePermit();
88
- const owned = ownEvent(event, collectionPermit);
89
- const eventId = event.event_id;
90
- if (!owned || typeof eventId !== "string" || eventId.length === 0) {
109
+ const collectionPermit = permit ?? this.pipeline.permit();
110
+ const owned = ownEvent(event);
111
+ if (!owned) {
91
112
  this.metrics?.increment("dropped_count");
92
113
  return;
93
114
  }
115
+ const snapshot = owned.value();
94
116
  void this.queueOutboxMutation(async () => {
95
- if (this.seenEventIds.has(eventId)) {
96
- return;
97
- }
98
- const retained = await this.outbox.withDeliveryLock(async () => this.outbox.addDraft(owned.value(), owned.permit));
99
- if (!retained) {
117
+ const settled = await settlePendingEncryptions(snapshot);
118
+ const draft = await this.outbox.withDeliveryLock(async () => {
119
+ this.adoptStoredRecords();
120
+ return this.outbox.holdDraft(settled);
121
+ });
122
+ if (!draft) {
100
123
  this.metrics?.increment("dropped_count");
101
124
  return;
102
125
  }
103
- this.seenEventIds.add(eventId);
104
- if (this.seenEventIds.size > BatchTransport.DEDUP_CAPACITY) {
105
- const oldest = this.seenEventIds.values().next().value;
106
- if (oldest !== undefined)
107
- this.seenEventIds.delete(oldest);
126
+ const report = this.pipeline.admit({
127
+ eventKey: draft.eventKeyHash,
128
+ byteLength: sentByteLength(draft.event),
129
+ diagnosticCanonicalBytes: this.client.diagnosticCanonicalPayload(JSON.stringify(draft.event)),
130
+ }, collectionPermit);
131
+ for (const entry of report.diagnostics)
132
+ this.diagnostics.record(entry);
133
+ this.outbox.releaseEvents(report.overflowed_event_keys);
134
+ if (report.overflowed_event_keys.length > 0) {
135
+ this.metrics?.increment("dropped_count", report.overflowed_event_keys.length);
136
+ }
137
+ if (!report.retained) {
138
+ if (report.overflowed_event_keys.length === 0) {
139
+ this.outbox.releaseEvents([draft.eventKeyHash]);
140
+ this.metrics?.increment("dropped_count");
141
+ }
142
+ return;
108
143
  }
109
144
  this.recordPendingBatchCount();
110
145
  this.scheduleFlushDeadline();
@@ -112,71 +147,33 @@ export class BatchTransport {
112
147
  void this.flush();
113
148
  return;
114
149
  }
115
- if (this.outbox.draftCount() >= this.maxSize ||
116
- this.outbox.estimatedDraftBytes((events, metadata) => this.client.measurePayloadBytes([...events], metadata), this.client.createBatchEnvelopeMetadata()) >= this.flushThresholdBytes) {
150
+ const depth = this.pipeline.depth(this.envelopeOverheadBytes());
151
+ if (depth.draft_count >= this.maxSize ||
152
+ depth.draft_body_bytes >= this.flushThresholdBytes) {
117
153
  void this.flush();
118
154
  }
119
155
  });
120
156
  }
121
- scheduleFlushDeadline() {
122
- if (!this.enabled) {
123
- return;
124
- }
125
- if (this.idleHandle) {
126
- return;
127
- }
128
- this.idleHandle = setTimeout(() => {
129
- this.idleHandle = null;
130
- void this.flush();
131
- }, this.flushIntervalMs);
132
- }
133
- beginFlushDeadline() {
134
- const active = this.currentDeadline();
135
- if (active)
136
- return active;
137
- const controller = new AbortController();
138
- const deadlineAt = monotonicNow() + 5000;
139
- this.flushDeadlineController = controller;
140
- this.flushDeadlineAt = deadlineAt;
141
- this.flushDeadlineTimer = setTimeout(() => {
142
- controller.abort("shutdown_deadline");
143
- this.activeSendController?.abort("shutdown_deadline");
144
- }, 5000);
145
- return { signal: controller.signal, deadlineAt };
146
- }
147
- endFlushDeadline(deadline) {
148
- if (!deadline || this.flushDeadlineController?.signal !== deadline.signal)
149
- return;
150
- if (this.flushDeadlineTimer)
151
- clearTimeout(this.flushDeadlineTimer);
152
- this.flushDeadlineTimer = null;
153
- this.flushDeadlineController = null;
154
- this.flushDeadlineAt = null;
155
- }
156
- async flush(deadline = this.currentDeadline()) {
157
+ async flush() {
157
158
  if (this.lifecycle === "closed")
158
159
  return;
160
+ this.tokenRefusedForGood = false;
159
161
  if (this.flushing) {
160
162
  this.flushAgainAfterCurrent = true;
161
- if (this.activeFlushPromise) {
163
+ if (this.activeFlushPromise)
162
164
  await this.activeFlushPromise;
163
- }
164
165
  return;
165
166
  }
166
- const enqueuesAtStart = this.enqueueTail;
167
- await enqueuesAtStart;
167
+ await this.enqueueTail;
168
168
  if (this.flushing) {
169
169
  this.flushAgainAfterCurrent = true;
170
170
  if (this.activeFlushPromise)
171
171
  await this.activeFlushPromise;
172
172
  return;
173
173
  }
174
- if (this.idleHandle) {
175
- clearTimeout(this.idleHandle);
176
- this.idleHandle = null;
177
- }
174
+ this.stop();
178
175
  this.flushing = true;
179
- this.activeFlushPromise = this.runFlushUntilIdle(deadline);
176
+ this.activeFlushPromise = this.runFlushUntilIdle();
180
177
  try {
181
178
  await this.activeFlushPromise;
182
179
  }
@@ -184,56 +181,46 @@ export class BatchTransport {
184
181
  this.activeFlushPromise = null;
185
182
  this.flushing = false;
186
183
  }
187
- this.diagnostics.setFlushOutcome(this.outbox.pendingEventCount() === 0 ? "clean" : "failed");
188
- return;
184
+ this.diagnostics.setFlushOutcome(this.pipeline.pendingCount() === 0 ? "clean" : "failed");
189
185
  }
190
- async runFlushUntilIdle(deadline) {
186
+ async runFlushUntilIdle() {
191
187
  while (true) {
192
188
  const enqueuesAtCycleStart = this.enqueueTail;
193
189
  await enqueuesAtCycleStart;
194
- await this.outbox.withDeliveryLock(() => this.runFlushCycle(deadline));
195
- const enqueuesAfterCycle = this.enqueueTail;
196
- if (this.lifecycle !== "open" ||
190
+ const stop = await this.outbox.withDeliveryLock(() => this.runFlushCycle());
191
+ if (stop === "retries_exhausted" ||
192
+ this.lifecycle !== "open" ||
197
193
  (!this.flushAgainAfterCurrent &&
198
- enqueuesAfterCycle === enqueuesAtCycleStart)) {
194
+ this.enqueueTail === enqueuesAtCycleStart)) {
199
195
  return;
200
196
  }
201
197
  this.flushAgainAfterCurrent = false;
202
198
  }
203
199
  }
204
200
  beginClose() {
205
- if (this.lifecycle === "closed")
206
- return null;
207
- if (this.lifecycle === "closing" && this.closeDeadlineController) {
208
- return this.closeDeadlineController.signal;
209
- }
201
+ if (this.lifecycle === "closed" || this.lifecycle === "closing")
202
+ return;
210
203
  this.lifecycle = "closing";
211
204
  this.closeHadTerminalDisposition = false;
212
205
  this.diagnostics.setLifecycle("closing");
213
206
  this.stop();
214
- const controller = new AbortController();
215
- this.closeDeadlineController = controller;
216
- this.closeDeadlineAt = monotonicNow() + 5000;
217
- this.closePermitOrdinal = this.outbox.capturePermit().captureOrdinal;
207
+ this.pipeline.beginClose();
208
+ this.closeDeadlineAt = monotonicNow() + CLOSE_DEADLINE_MS;
218
209
  this.closeDeadlineTimer = setTimeout(() => {
219
- controller.abort("shutdown_deadline");
220
- this.activeSendController?.abort("shutdown_deadline");
221
- }, 5000);
222
- return controller.signal;
210
+ this.client.abortAttempt("deadline");
211
+ }, CLOSE_DEADLINE_MS);
223
212
  }
224
213
  async close() {
225
214
  if (this.lifecycle === "closed")
226
215
  return;
227
- const signal = this.beginClose();
228
- if (!signal)
229
- return;
216
+ this.beginClose();
230
217
  try {
231
218
  await this.flush();
232
219
  }
233
220
  finally {
234
- this.clearCloseDeadline(signal);
221
+ this.clearCloseDeadline();
235
222
  }
236
- if (this.outbox.pendingEventCount() > 0) {
223
+ if (this.pipeline.pendingCount() > 0) {
237
224
  this.lifecycle = "closing_failed";
238
225
  this.diagnostics.setLifecycle("closing_failed");
239
226
  this.diagnostics.setCloseOutcome("retryable_failure");
@@ -244,211 +231,189 @@ export class BatchTransport {
244
231
  this.diagnostics.setLifecycle("closed");
245
232
  this.diagnostics.setCloseOutcome(this.closeHadTerminalDisposition ? "terminal_failure" : "clean");
246
233
  }
247
- async runFlushCycle(deadline) {
248
- const fence = this.outbox.prepareFence({
249
- maxPayloadBytes: this.maxPayloadBytes,
250
- createMetadata: () => this.client.createBatchEnvelopeMetadata(),
251
- measure: (events, metadata) => this.client.measurePayloadBytes([...events], metadata),
252
- });
253
- if (fence.oversized > 0)
254
- this.recordPendingBatchCount();
255
- while (true) {
256
- const batch = this.outbox.headThrough(fence.targetOrdinal);
257
- if (!batch) {
258
- break;
259
- }
260
- if (batch.events.length === 0 ||
261
- batch.events.some((event) => !this.outbox.permits(event.permit))) {
262
- this.outbox.removeHead(batch);
263
- this.recordPendingBatchCount();
264
- continue;
265
- }
266
- this.metrics?.increment("batch_flush_count");
267
- const result = await this.sendBatchWhilePermitted(batch, deadline);
268
- if (!result || this.applyDeliveryResult(batch, result) === "break")
269
- break;
270
- }
234
+ flushOnUnload() {
235
+ if (!this.client.hasUsableBrowserToken())
236
+ return;
237
+ this.beginClose();
238
+ void this.flush();
271
239
  }
272
- async sendBatchWhilePermitted(batch, deadline) {
273
- const sendController = new AbortController();
274
- const abortForClose = () => sendController.abort("shutdown_deadline");
275
- if (deadline?.signal.aborted)
276
- abortForClose();
277
- deadline?.signal.addEventListener("abort", abortForClose, { once: true });
278
- this.activeSendController = sendController;
279
- let result;
280
- try {
281
- result = await this.client.send(batch.events.map((event) => event.value()), {
282
- maxPayloadBytes: this.maxPayloadBytes,
283
- metadata: batch.metadata,
284
- signal: sendController.signal,
285
- deadlineAt: deadline?.deadlineAt,
240
+ clear() {
241
+ this.clearedAt = monotonicNow();
242
+ this.client.abortAttempt("shutdown");
243
+ this.outbox.clearMemory();
244
+ if (this.flushing && this.activeFlushPromise) {
245
+ void this.activeFlushPromise.finally(() => {
246
+ if (this.disposed)
247
+ return;
248
+ this.pipeline.reset();
249
+ this.clearedAt = null;
286
250
  });
287
251
  }
288
- finally {
289
- deadline?.signal.removeEventListener("abort", abortForClose);
290
- if (this.activeSendController === sendController) {
291
- this.activeSendController = null;
292
- }
293
- }
294
- if (batch.events.some((event) => !this.outbox.permits(event.permit))) {
295
- this.recordPendingBatchCount();
296
- return null;
252
+ else {
253
+ this.pipeline.reset();
254
+ this.clearedAt = null;
297
255
  }
298
- return result;
299
- }
300
- applyDeliveryResult(batch, result) {
301
- if (result.governor)
302
- this.onGovernorFeedback?.(result.governor);
303
- return result.ok
304
- ? this.applySuccessfulDelivery(batch, result)
305
- : this.applyFailedDelivery(batch, result);
256
+ void this.queueOutboxMutation(async () => {
257
+ await this.outbox.withDeliveryLock(async () => this.outbox.clear());
258
+ });
259
+ this.recordPendingBatchCount();
306
260
  }
307
- applyFailedDelivery(batch, result) {
308
- if (result.retryStrategy === "split_batch") {
309
- if (this.outbox.splitHead(batch, () => this.client.createBatchEnvelopeMetadata())) {
310
- this.recordPendingBatchCount();
311
- this.scheduleFlushDeadline();
312
- return "continue";
313
- }
314
- this.metrics?.increment("dropped_count", batch.events.length);
315
- this.metrics?.increment("oversized_event_dropped_count");
316
- this.outbox.removeHead(batch);
261
+ async runFlushCycle() {
262
+ this.adoptStoredRecords();
263
+ if (!(await this.ensureDeliveryToken()))
264
+ return null;
265
+ const fence = this.pipeline.fence(this.envelopeOverheadBytes());
266
+ for (const entry of fence.diagnostics)
267
+ this.diagnostics.record(entry);
268
+ if (fence.oversized_event_keys.length > 0) {
269
+ this.outbox.releaseEvents(fence.oversized_event_keys);
270
+ this.metrics?.increment("oversized_event_count", fence.oversized_event_keys.length);
271
+ this.metrics?.increment("dropped_count", fence.oversized_event_keys.length);
317
272
  this.recordPendingBatchCount();
318
- return "continue";
319
273
  }
320
- if (this.shouldRetainFailedBatch(result.retryStrategy)) {
321
- this.diagnostics.record({
322
- code: "retry_exhausted",
323
- strategy: "retry_same_batch",
324
- attempt: result.attempts,
325
- batch_fingerprint: this.diagnostics.batchFingerprint(batch.diagnosticCanonicalBytes),
326
- });
327
- this.outbox.retainHead(batch);
274
+ const summary = await this.pipeline.run({
275
+ body: (batchId, eventKeys) => this.serializeBatch(batchId, eventKeys),
276
+ onObservation: (observation) => this.applyObservation(observation),
277
+ deadlineMs: () => this.closeDeadlineAt ?? this.clearedAt ?? undefined,
278
+ });
279
+ if (summary.stop === "retries_exhausted") {
328
280
  this.metrics?.increment("transport_requeue_count");
329
- this.recordPendingBatchCount();
330
281
  if (this.lifecycle === "open")
331
282
  this.scheduleFlushDeadline();
332
- return "break";
333
283
  }
334
- this.diagnostics.record({
335
- code: result.retryStrategy === "drop_event"
336
- ? "intentional_drop"
337
- : "terminal_rejection",
338
- strategy: result.retryStrategy ?? undefined,
339
- count: batch.events.length,
340
- batch_fingerprint: this.diagnostics.batchFingerprint(batch.diagnosticCanonicalBytes),
284
+ this.recordPendingBatchCount();
285
+ return summary.stop;
286
+ }
287
+ serializeBatch(batchId, eventKeys) {
288
+ const claim = this.outbox.claimBatch({
289
+ batchId,
290
+ eventKeys,
291
+ createMetadata: () => this.client.createBatchEnvelopeMetadata(),
341
292
  });
342
- if (this.lifecycle === "closing")
343
- this.closeHadTerminalDisposition = true;
344
- this.metrics?.increment("dropped_count", batch.events.length);
345
- this.metrics?.increment("transport_failed_count");
346
- if (!this.outbox.removeHead(batch)) {
347
- this.metrics?.increment("transport_requeue_count");
348
- this.recordPendingBatchCount();
349
- return "break";
293
+ if (!claim)
294
+ return null;
295
+ const metadata = {
296
+ batch_id: claim.record.batchId,
297
+ idempotency_key: claim.record.idempotencyKey,
298
+ sent_at: claim.record.sentAt,
299
+ };
300
+ const body = this.client.stringifyPayload(claim.record.events.map((event) => event.event), metadata);
301
+ if (claim.minted) {
302
+ this.metrics?.increment("payload_size_bytes", new TextEncoder().encode(body).byteLength);
350
303
  }
351
- this.recordPendingBatchCount();
352
- return "continue";
304
+ return {
305
+ body,
306
+ diagnosticCanonicalBytes: this.client.diagnosticCanonicalPayload(body),
307
+ };
353
308
  }
354
- applySuccessfulDelivery(batch, result) {
355
- if (!this.outbox.removeHead(batch)) {
356
- this.metrics?.increment("transport_requeue_count");
357
- this.recordPendingBatchCount();
358
- return "break";
309
+ applyObservation(observation) {
310
+ for (const entry of observation.diagnostics)
311
+ this.diagnostics.record(entry);
312
+ if (observation.privacyPolicy !== undefined) {
313
+ applyDeliveredPrivacyPolicy(observation.privacyPolicy);
359
314
  }
360
- if (result.partial && this.lifecycle === "closing") {
361
- this.closeHadTerminalDisposition = true;
315
+ else if (observation.directiveUnreadable) {
316
+ resetPrivacyPolicy();
317
+ resetSdkEncryption();
318
+ }
319
+ if (observation.sdkEncryption !== undefined) {
320
+ applyDeliveredSdkEncryption(observation.sdkEncryption);
321
+ }
322
+ const progress = observation.progress;
323
+ if (progress.kind !== "retry_scheduled") {
324
+ this.metrics?.increment("batch_flush_count");
325
+ }
326
+ else {
327
+ this.metrics?.increment("retry_count");
328
+ this.metrics?.increment("transport_retry_count");
329
+ }
330
+ if (progress.kind === "settled") {
331
+ this.outbox.releaseBatch(progress.batch_id);
332
+ this.metrics?.increment("sent_count", progress.settled_event_keys.length);
333
+ if (observation.droppedEventCount > 0) {
334
+ this.metrics?.increment("ingest_dropped_event_count", observation.droppedEventCount);
335
+ if (this.lifecycle === "closing") {
336
+ this.closeHadTerminalDisposition = true;
337
+ }
338
+ }
339
+ return;
340
+ }
341
+ if (progress.kind === "dropped_oversized") {
342
+ this.outbox.releaseBatch(progress.batch_id);
343
+ this.metrics?.increment("dropped_count");
344
+ this.metrics?.increment("oversized_event_dropped_count");
345
+ return;
346
+ }
347
+ if (progress.kind === "split_scheduled") {
348
+ this.outbox.reopenBatch(progress.batch_id);
349
+ this.scheduleFlushDeadline();
350
+ return;
351
+ }
352
+ if (progress.kind === "consent_revoked") {
353
+ this.metrics?.increment("transport_failed_count");
354
+ return;
355
+ }
356
+ if (progress.kind === "retries_exhausted") {
357
+ this.metrics?.increment("transport_failed_count");
362
358
  }
363
- this.recordPendingBatchCount();
364
- return "continue";
365
359
  }
366
- flushOnUnload() {
367
- if (!this.client.hasUsableBrowserToken()) {
360
+ adoptStoredRecords() {
361
+ if (this.restored)
368
362
  return;
363
+ this.restored = true;
364
+ const restorable = this.outbox.restorable();
365
+ for (const batch of restorable.batches) {
366
+ const report = this.pipeline.adoptRestoredBatch(batch.ordinal, batch.events);
367
+ if (report.adopted && report.batch_id !== undefined) {
368
+ this.outbox.bindRestoredBatch(batch.storedBatchId, report.batch_id);
369
+ }
370
+ }
371
+ for (const draft of restorable.drafts) {
372
+ this.pipeline.admit({
373
+ eventKey: draft.eventKeyHash,
374
+ byteLength: sentByteLength(draft.event),
375
+ diagnosticCanonicalBytes: this.client.diagnosticCanonicalPayload(JSON.stringify(draft.event)),
376
+ }, this.pipeline.permit());
369
377
  }
370
- this.stop();
371
- void this.queueOutboxMutation(async () => {
372
- await this.outbox.withDeliveryLock(async () => {
373
- const fence = this.outbox.prepareFence({
374
- maxPayloadBytes: this.maxPayloadBytes,
375
- createMetadata: () => this.client.createBatchEnvelopeMetadata(),
376
- measure: (events, metadata) => this.client.measurePayloadBytes([...events], metadata),
377
- });
378
- this.recordPendingBatchCount();
379
- const batch = this.outbox.headThrough(fence.targetOrdinal);
380
- if (!batch)
381
- return;
382
- const sendController = new AbortController();
383
- this.activeSendController = sendController;
384
- try {
385
- await this.client.send(batch.events.map((event) => event.value()), {
386
- keepalive: true,
387
- useBeacon: false,
388
- requireCachedBrowserToken: true,
389
- maxPayloadBytes: this.maxPayloadBytes,
390
- metadata: batch.metadata,
391
- signal: sendController.signal,
392
- });
393
- }
394
- finally {
395
- if (this.activeSendController === sendController) {
396
- this.activeSendController = null;
397
- }
398
- }
399
- });
400
- });
401
378
  }
402
- clear() {
403
- this.activeSendController?.abort();
404
- this.activeSendController = null;
405
- this.outbox.clearMemory();
406
- void this.queueOutboxMutation(async () => {
407
- await this.outbox.withDeliveryLock(async () => this.outbox.clear());
408
- });
409
- this.recordPendingBatchCount();
379
+ async ensureDeliveryToken() {
380
+ const result = await this.client.ensureDeliveryToken();
381
+ if (!result.ok) {
382
+ this.metrics?.increment("send_failure_count");
383
+ this.tokenRefusedForGood = result.retryStrategy === "stop_and_report";
384
+ return false;
385
+ }
386
+ this.tokenRefusedForGood = false;
387
+ this.pipeline.rememberBrowserToken(result.token);
388
+ return true;
389
+ }
390
+ envelopeOverheadBytes() {
391
+ return this.client.measurePayloadBytes([], this.client.createBatchEnvelopeMetadata());
392
+ }
393
+ scheduleFlushDeadline() {
394
+ if (!this.enabled || this.idleHandle || this.tokenRefusedForGood)
395
+ return;
396
+ this.idleHandle = setTimeout(() => {
397
+ this.idleHandle = null;
398
+ void this.flush();
399
+ }, this.flushIntervalMs);
410
400
  }
411
401
  abortActiveSendForConsent() {
412
- this.activeSendController?.abort(CONSENT_DENIED_ABORT_REASON);
413
- this.activeSendController = null;
402
+ this.client.abortAttempt("consent_revoked");
414
403
  }
415
404
  recordPendingBatchCount() {
416
- this.metrics?.set("pending_batch_count", this.outbox.pendingEventCount());
405
+ this.metrics?.set("pending_batch_count", this.pipeline.pendingCount());
417
406
  }
418
- clearCloseDeadline(signal) {
419
- if (this.closeDeadlineController?.signal !== signal)
420
- return;
421
- if (this.closeDeadlineTimer) {
407
+ clearCloseDeadline() {
408
+ if (this.closeDeadlineTimer)
422
409
  clearTimeout(this.closeDeadlineTimer);
423
- this.closeDeadlineTimer = null;
424
- }
425
- this.closeDeadlineController = null;
410
+ this.closeDeadlineTimer = null;
426
411
  this.closeDeadlineAt = null;
427
- this.closePermitOrdinal = null;
428
- }
429
- currentDeadline() {
430
- if (this.closeDeadlineController && this.closeDeadlineAt !== null) {
431
- return {
432
- signal: this.closeDeadlineController.signal,
433
- deadlineAt: this.closeDeadlineAt,
434
- };
435
- }
436
- if (this.flushDeadlineController && this.flushDeadlineAt !== null) {
437
- return {
438
- signal: this.flushDeadlineController.signal,
439
- deadlineAt: this.flushDeadlineAt,
440
- };
441
- }
442
- return null;
443
412
  }
444
413
  queueOutboxMutation(operation) {
445
414
  const next = this.enqueueTail.then(operation, operation);
446
415
  this.enqueueTail = next.catch(() => undefined);
447
416
  return next;
448
417
  }
449
- shouldRetainFailedBatch(retryStrategy) {
450
- return retryStrategy === null || retryStrategy === "retry_same_batch";
451
- }
452
418
  }
453
- BatchTransport.DEDUP_CAPACITY = 1024;
454
419
  const monotonicNow = () => typeof performance !== "undefined" ? performance.now() : Date.now();