@genn-inc/cluebase-frontend-sdk 0.0.3 → 0.0.8

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