@genn-inc/cluebase-frontend-sdk 0.0.6 → 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 (62) hide show
  1. package/dist/_generated/correlation-attribute-keys.d.ts +3 -0
  2. package/dist/_generated/correlation-attribute-keys.js +34 -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 +247 -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 +105 -318
  18. package/dist/exporter/delivery-pipeline.d.ts +71 -0
  19. package/dist/exporter/delivery-pipeline.js +196 -0
  20. package/dist/exporter/delivery-response-contract.d.ts +1 -81
  21. package/dist/exporter/delivery-response-contract.js +0 -168
  22. package/dist/exporter/delivery-response.d.ts +7 -4
  23. package/dist/exporter/delivery-response.js +59 -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/sdk-core-delivery.d.ts +225 -0
  31. package/dist/exporter/sdk-core-delivery.js +97 -0
  32. package/dist/exporter/sdk-core-wasm.d.ts +7 -1
  33. package/dist/exporter/sdk-core-wasm.js +24 -3
  34. package/dist/exporter/source-event.d.ts +2 -0
  35. package/dist/exporter/source-event.js +17 -0
  36. package/dist/exporter/span-to-source-signal.d.ts +1 -1
  37. package/dist/exporter/span-to-source-signal.js +20 -140
  38. package/dist/index.d.ts +3 -3
  39. package/dist/index.js +5 -3
  40. package/dist/instrumentation/network.d.ts +1 -1
  41. package/dist/instrumentation/network.js +17 -0
  42. package/dist/provider/_version.d.ts +1 -1
  43. package/dist/provider/_version.js +1 -1
  44. package/dist/provider/config.js +16 -3
  45. package/dist/provider/constants.js +0 -1
  46. package/dist/provider/lifecycle-event-dedup.d.ts +7 -0
  47. package/dist/provider/lifecycle-event-dedup.js +31 -0
  48. package/dist/provider/sdk-event-pipeline.d.ts +1 -3
  49. package/dist/provider/sdk-event-pipeline.js +0 -15
  50. package/dist/provider/sdk-otel-pipeline.d.ts +2 -1
  51. package/dist/provider/sdk-otel-pipeline.js +2 -11
  52. package/dist/provider/sdk-runtime-surfaces.d.ts +3 -2
  53. package/dist/provider/sdk-runtime-surfaces.js +7 -6
  54. package/dist/provider/sdk.js +88 -54
  55. package/dist/provider/types.d.ts +12 -19
  56. package/dist/wasm/pkg-web/cluebase_sdk_core_wasm.d.ts +12 -9
  57. package/dist/wasm/pkg-web/cluebase_sdk_core_wasm.js +117 -29
  58. package/dist/wasm/pkg-web/cluebase_sdk_core_wasm_bg.wasm +0 -0
  59. package/dist/wasm/pkg-web/cluebase_sdk_core_wasm_bg.wasm.d.ts +8 -9
  60. package/package.json +1 -1
  61. package/dist/provider/usage-governor.d.ts +0 -7
  62. package/dist/provider/usage-governor.js +0 -114
@@ -1,40 +1,14 @@
1
1
  import { canonicalize as canonicalJsonStringify } from "json-canonicalize";
2
2
  import { CONSENT_DENIED_ABORT_REASON, DEFAULT_FRONTEND_SOURCE_IDENTIFIER, } from "../provider/constants.js";
3
- import { appliedPrivacyPolicyToken, applyDeliveredPrivacyPolicy, } from "../provider/privacy-policy-store.js";
4
- import { classifyIngestHttpStatus, evaluateBrowserDeliveryResponse, } from "./delivery-response.js";
5
- import { generateEventId } from "./event-id.js";
6
- import { computeRetryDelay, RETRY_CONFIG } from "./retry.js";
7
- import { isBrowserSourceSignalEvent, SDK_SOURCE_SIGNAL_EVENT_NAME, SDK_SOURCE_SIGNAL_EVENT_TYPE, } from "./source-event.js";
3
+ import { appliedPrivacyPolicyToken } from "../provider/privacy-policy-store.js";
4
+ import { classifyIngestHttpStatus, } from "./delivery-response.js";
5
+ import { generateBatchId } from "./event-id.js";
6
+ import { describeIngestEndpoint, } from "./sdk-core-wasm.js";
7
+ import { withSourceEvidence } from "./source-event.js";
8
8
  export const CLUEBASE_SDK_REQUEST_HEADER = "x-cluebase-sdk-request";
9
9
  export const CLUEBASE_SDK_VERSION_HEADER = "x-cluebase-sdk-version";
10
10
  export const CLUEBASE_SOURCE_SCHEMA_VERSION_HEADER = "x-cluebase-source-schema-version";
11
11
  export const CLUEBASE_BROWSER_TOKEN_HEADER = "x-cluebase-browser-token";
12
- export const CANONICAL_BROWSER_INGEST_PATH = "/api/v1/ingest/browser";
13
- export const CANONICAL_BROWSER_TOKEN_PATH = "/api/v1/ingest/browser-tokens";
14
- const CANONICAL_INGEST_PATH_SUFFIX = CANONICAL_BROWSER_INGEST_PATH.replace(/^\/api\/v1/, "");
15
- export const resolveCanonicalIngestEndpoint = (endpoint) => {
16
- if (!endpoint) {
17
- return endpoint;
18
- }
19
- if (endpoint.endsWith(CANONICAL_INGEST_PATH_SUFFIX)) {
20
- return endpoint;
21
- }
22
- const trimmed = endpoint.endsWith("/") ? endpoint.slice(0, -1) : endpoint;
23
- if (trimmed.endsWith("/api/v1")) {
24
- return `${trimmed}${CANONICAL_INGEST_PATH_SUFFIX}`;
25
- }
26
- try {
27
- const parsed = new URL(trimmed);
28
- if (parsed.pathname === "" || parsed.pathname === "/") {
29
- parsed.pathname = CANONICAL_BROWSER_INGEST_PATH;
30
- parsed.search = "";
31
- parsed.hash = "";
32
- return parsed.toString();
33
- }
34
- }
35
- catch { }
36
- return endpoint;
37
- };
38
12
  const BROWSER_TOKEN_REFRESH_LEEWAY_MS = 60000;
39
13
  class BrowserTokenRequestError extends Error {
40
14
  constructor(status) {
@@ -43,163 +17,85 @@ class BrowserTokenRequestError extends Error {
43
17
  this.name = "BrowserTokenRequestError";
44
18
  }
45
19
  }
46
- export const resolveBrowserTokenEndpoint = (endpoint) => {
47
- if (!endpoint) {
48
- return CANONICAL_BROWSER_TOKEN_PATH;
49
- }
50
- try {
51
- const parsed = new URL(endpoint);
52
- parsed.pathname = CANONICAL_BROWSER_TOKEN_PATH;
53
- parsed.search = "";
54
- parsed.hash = "";
55
- return parsed.toString();
56
- }
57
- catch {
58
- const trimmed = endpoint.endsWith("/") ? endpoint.slice(0, -1) : endpoint;
59
- const apiV1Idx = trimmed.indexOf("/api/v1");
60
- if (apiV1Idx >= 0) {
61
- return `${trimmed.slice(0, apiV1Idx)}${CANONICAL_BROWSER_TOKEN_PATH}`;
62
- }
63
- return `${trimmed}${CANONICAL_BROWSER_TOKEN_PATH}`;
64
- }
65
- };
66
- const NORMAL_ATTEMPT_TIMEOUT_MS = 15000;
67
20
  export class IngestClient {
68
21
  constructor(options) {
69
- this.sourceIdentifier = DEFAULT_FRONTEND_SOURCE_IDENTIFIER;
22
+ this.describedEndpoint = null;
23
+ this.describePromise = null;
70
24
  this.producerId = DEFAULT_FRONTEND_SOURCE_IDENTIFIER;
71
25
  this.cachedToken = null;
72
26
  this.pendingTokenRequest = null;
73
- this.endpoint = resolveCanonicalIngestEndpoint(options.endpoint);
74
- this.browserTokenEndpoint = resolveBrowserTokenEndpoint(options.endpoint);
27
+ this.inFlight = null;
28
+ this.configuredEndpoint = options.endpoint;
75
29
  this.projectKey = options.projectKey;
76
30
  this.sdkType = options.sdkType;
77
31
  this.sdkVersion = options.sdkVersion;
78
32
  this.schemaVersion = options.schemaVersion;
79
33
  this.metrics = options.metrics ?? null;
80
34
  this.diagnostics = options.diagnostics ?? null;
35
+ this.onEndpointsResolved = options.onEndpointsResolved ?? null;
81
36
  }
82
- async send(events, options = {}) {
83
- if (!hasDeadlineRemaining(options.deadlineAt)) {
84
- return this.failedSend(null, 0, "retry_same_batch");
85
- }
86
- const sourceSignalEvents = events.filter(isBrowserSourceSignalEvent);
87
- const droppedEventCount = events.length - sourceSignalEvents.length;
88
- if (droppedEventCount > 0) {
89
- this.metrics?.increment("dropped_count", droppedEventCount);
90
- }
91
- if (sourceSignalEvents.length === 0) {
37
+ async endpoints() {
38
+ this.describePromise ?? (this.describePromise = describeIngestEndpoint(this.configuredEndpoint).then((described) => {
39
+ this.describedEndpoint = described;
40
+ this.onEndpointsResolved?.(described);
41
+ return described;
42
+ }));
43
+ return await this.describePromise;
44
+ }
45
+ knownEndpoints() {
46
+ return this.describedEndpoint;
47
+ }
48
+ async perform(request) {
49
+ const active = {
50
+ controller: new AbortController(),
51
+ cause: null,
52
+ timedOut: false,
53
+ };
54
+ this.inFlight = active;
55
+ const timeout = setTimeout(() => {
56
+ active.timedOut = true;
57
+ active.controller.abort();
58
+ }, request.timeout_ms);
59
+ try {
60
+ const response = await fetch(request.url, {
61
+ method: request.method,
62
+ headers: Object.fromEntries(request.headers.map((header) => [header.name, header.value])),
63
+ body: request.body,
64
+ redirect: request.follow_redirects ? "follow" : "error",
65
+ signal: active.controller.signal,
66
+ ...(request.omit_credentials ? { credentials: "omit" } : {}),
67
+ ...(request.keepalive ? { keepalive: true } : {}),
68
+ });
69
+ const body = await readBoundedBody(response, request.max_response_body_bytes);
92
70
  return {
93
- ok: true,
94
- status: null,
95
- attempts: 0,
96
- governor: null,
97
- retryStrategy: null,
71
+ kind: "response",
72
+ status: response.status,
73
+ body: body.text,
74
+ body_over_limit: body.overLimit,
98
75
  };
99
76
  }
100
- const maxPayloadBytes = Math.max(1024, options.maxPayloadBytes ?? Number.MAX_SAFE_INTEGER);
101
- const metadata = options.metadata ?? this.createBatchEnvelopeMetadata();
102
- const payload = this.stringifyPayload(sourceSignalEvents, metadata);
103
- const keepalive = options.keepalive ?? options.useBeacon ?? false;
104
- const payloadSize = new TextEncoder().encode(payload).byteLength;
105
- this.metrics?.increment("payload_size_bytes", payloadSize);
106
- if (payloadSize > maxPayloadBytes) {
107
- return this.failedSend(null, 0, "split_batch");
108
- }
109
- const browserTokenResult = await this.safeEnsureBrowserToken({
110
- signal: options.signal,
111
- requireCachedToken: options.requireCachedBrowserToken,
112
- deadlineAt: options.deadlineAt,
113
- });
114
- if (!browserTokenResult.ok) {
115
- this.metrics?.increment("send_failure_count");
116
- this.metrics?.increment("transport_failed_count");
117
- return {
118
- ok: false,
119
- status: null,
120
- attempts: 0,
121
- governor: null,
122
- retryStrategy: browserTokenResult.retryStrategy ??
123
- (options.requireCachedBrowserToken ? "drop_event" : null),
124
- };
77
+ catch {
78
+ if (active.cause)
79
+ return { kind: "aborted", cause: active.cause };
80
+ if (active.timedOut)
81
+ return { kind: "timeout" };
82
+ return { kind: "network_error" };
125
83
  }
126
- let browserToken = browserTokenResult.token;
127
- let refreshedToken = false;
128
- let lastStatus = null;
129
- for (let attempts = 1; attempts <= RETRY_CONFIG.maxAttempts; attempts += 1) {
130
- if (options.signal?.aborted ||
131
- !hasDeadlineRemaining(options.deadlineAt)) {
132
- return this.failedSend(lastStatus, attempts - 1, isConsentAbort(options.signal) ? "drop_event" : "retry_same_batch");
133
- }
134
- const attempt = await this.postBatch({
135
- payload,
136
- browserToken,
137
- keepalive,
138
- signal: options.signal,
139
- deadlineAt: options.deadlineAt,
140
- localSubmittedCount: sourceSignalEvents.length,
141
- });
142
- lastStatus = attempt.status;
143
- const decision = attempt.decision;
144
- this.recordDecision(decision, lastStatus, attempts, payload);
145
- if (decision.ok) {
146
- if (decision.droppedEventCount > 0) {
147
- this.metrics?.increment("ingest_dropped_event_count", decision.droppedEventCount);
148
- }
149
- this.metrics?.increment("sent_count", decision.acceptedEventCount);
150
- return {
151
- ok: true,
152
- status: lastStatus,
153
- attempts,
154
- governor: decision.governor,
155
- retryStrategy: null,
156
- observationIngestBatchId: decision.observationIngestBatchId,
157
- partial: decision.acceptedPartial,
158
- droppedEventCount: decision.droppedEventCount,
159
- reason: decision.reason,
160
- };
161
- }
162
- if (decision.droppedEventCount > 0) {
163
- this.metrics?.increment("ingest_dropped_event_count", decision.droppedEventCount);
164
- }
165
- if (!options.requireCachedBrowserToken &&
166
- !refreshedToken &&
167
- (decision.reason === "expired_browser_token" ||
168
- decision.reason === "stale_browser_token")) {
169
- this.cachedToken = null;
170
- const refreshed = await this.safeEnsureBrowserToken({
171
- signal: options.signal,
172
- deadlineAt: options.deadlineAt,
173
- });
174
- if (!refreshed.ok) {
175
- return this.failedSend(lastStatus, attempts, refreshed.retryStrategy ?? "retry_same_batch");
176
- }
177
- browserToken = refreshed.token;
178
- refreshedToken = true;
179
- continue;
180
- }
181
- if (decision.retryStrategy !== "retry_same_batch" ||
182
- attempts === RETRY_CONFIG.maxAttempts) {
183
- return this.failedSend(lastStatus, attempts, decision.retryStrategy, decision.governor);
184
- }
185
- this.metrics?.increment("retry_count");
186
- this.metrics?.increment("transport_retry_count");
187
- this.diagnostics?.record({
188
- code: "retry_scheduled",
189
- strategy: "retry_same_batch",
190
- http_status: lastStatus ?? undefined,
191
- attempt: attempts,
192
- batch_fingerprint: this.diagnostics.batchFingerprint(diagnosticCanonicalPayload(payload)),
193
- });
194
- if (!(await sleep(computeRetryDelay(attempts, RETRY_CONFIG), options.signal, options.deadlineAt))) {
195
- return this.failedSend(lastStatus, attempts, isConsentAbort(options.signal) ? "drop_event" : "retry_same_batch");
196
- }
84
+ finally {
85
+ clearTimeout(timeout);
86
+ if (this.inFlight === active)
87
+ this.inFlight = null;
197
88
  }
198
- return this.failedSend(lastStatus, RETRY_CONFIG.maxAttempts, "retry_same_batch");
89
+ }
90
+ abortAttempt(cause) {
91
+ if (!this.inFlight)
92
+ return;
93
+ this.inFlight.cause = cause;
94
+ this.inFlight.controller.abort();
199
95
  }
200
96
  measurePayloadBytes(events, metadata = {
201
- batch_id: "batch_00000000-0000-4000-8000-000000000000",
202
- idempotency_key: "idem_00000000-0000-4000-8000-000000000000",
97
+ batch_id: `batch_${"0".repeat(32)}`,
98
+ idempotency_key: `idem_${"0".repeat(32)}`,
203
99
  sent_at: "2026-01-01T00:00:00.000Z",
204
100
  }) {
205
101
  return new TextEncoder().encode(this.stringifyPayload(events, metadata))
@@ -218,34 +114,28 @@ export class IngestClient {
218
114
  sdk_type: this.sdkType,
219
115
  sdk_version: this.sdkVersion,
220
116
  },
221
- events: events.map((event) => this.withSourceEvidence(event)),
117
+ events: events.map((event) => withSourceEvidence(event)),
222
118
  });
223
119
  }
224
- withSourceEvidence(event) {
225
- return {
226
- ...event,
227
- source_event_id: event.source_event_id ?? event.event_id,
228
- source_schema_version: String(this.schemaVersion),
229
- source_event_type: SDK_SOURCE_SIGNAL_EVENT_TYPE,
230
- source_event_kind: SDK_SOURCE_SIGNAL_EVENT_TYPE,
231
- event_name: SDK_SOURCE_SIGNAL_EVENT_NAME,
232
- event_category: "context",
233
- producer_id: this.producerId,
234
- service_name: event.service_name ?? this.sourceIdentifier,
235
- privacy_state: event.privacy_state ?? "masked",
236
- privacy_level: event.privacy_level ?? "default_masked",
237
- masking_state: event.masking_state ?? "masked",
238
- allowed_value_keys: event.allowed_value_keys ?? [],
239
- };
240
- }
241
120
  createBatchEnvelopeMetadata() {
242
- const id = this.createId();
121
+ const id = generateBatchId();
243
122
  return {
244
123
  batch_id: `batch_${id}`,
245
124
  idempotency_key: `idem_${id}`,
246
125
  sent_at: new Date().toISOString(),
247
126
  };
248
127
  }
128
+ diagnosticCanonicalPayload(payload) {
129
+ try {
130
+ return canonicalJsonStringify(JSON.parse(payload));
131
+ }
132
+ catch {
133
+ return "{}";
134
+ }
135
+ }
136
+ async ensureDeliveryToken(options = {}) {
137
+ return await this.safeEnsureBrowserToken(options);
138
+ }
249
139
  hasUsableBrowserToken(nowMs = Date.now()) {
250
140
  return this.getUsableCachedBrowserToken(nowMs) !== null;
251
141
  }
@@ -253,78 +143,6 @@ export class IngestClient {
253
143
  const result = await this.safeEnsureBrowserToken({ signal });
254
144
  return result.ok;
255
145
  }
256
- failedSend(status, attempts, retryStrategy, governor = null) {
257
- this.metrics?.increment("send_failure_count");
258
- this.metrics?.increment("transport_failed_count");
259
- return { ok: false, status, attempts, governor, retryStrategy };
260
- }
261
- recordDecision(decision, status, attempt, payload) {
262
- const diagnostics = this.diagnostics;
263
- if (!diagnostics)
264
- return;
265
- for (const code of decision.diagnosticCodes) {
266
- diagnostics.record({
267
- code,
268
- count: code === "intentional_drop" ? decision.droppedEventCount : 1,
269
- http_status: status ?? undefined,
270
- attempt,
271
- batch_fingerprint: diagnostics.batchFingerprint(diagnosticCanonicalPayload(payload)),
272
- });
273
- }
274
- }
275
- async postBatch(input) {
276
- try {
277
- const response = await fetchWithDeadline(this.endpoint, {
278
- method: "POST",
279
- headers: {
280
- "content-type": "application/json",
281
- "x-cluebase-project-key": this.projectKey,
282
- [CLUEBASE_BROWSER_TOKEN_HEADER]: input.browserToken,
283
- [CLUEBASE_SDK_REQUEST_HEADER]: this.sdkType,
284
- [CLUEBASE_SDK_VERSION_HEADER]: this.sdkVersion,
285
- [CLUEBASE_SOURCE_SCHEMA_VERSION_HEADER]: String(this.schemaVersion),
286
- },
287
- body: input.payload,
288
- keepalive: input.keepalive,
289
- credentials: "omit",
290
- redirect: "error",
291
- }, input.signal, input.deadlineAt);
292
- const decision = evaluateBrowserDeliveryResponse({
293
- status: response.status,
294
- bodyText: await response.text(),
295
- localSubmittedCount: input.localSubmittedCount,
296
- });
297
- if (decision.privacyPolicy !== undefined) {
298
- applyDeliveredPrivacyPolicy(decision.privacyPolicy);
299
- }
300
- return {
301
- status: response.status,
302
- decision,
303
- };
304
- }
305
- catch {
306
- return {
307
- status: null,
308
- decision: {
309
- ok: false,
310
- retryStrategy: isConsentAbort(input.signal)
311
- ? "drop_event"
312
- : "retry_same_batch",
313
- governor: null,
314
- droppedEventCount: 0,
315
- acceptedEventCount: 0,
316
- acceptedPartial: false,
317
- reason: isConsentAbort(input.signal)
318
- ? "consent_denied"
319
- : "transport_timeout",
320
- diagnosticCodes: [],
321
- },
322
- };
323
- }
324
- }
325
- createId() {
326
- return generateEventId();
327
- }
328
146
  getUsableCachedBrowserToken(nowMs = Date.now()) {
329
147
  if (this.cachedToken &&
330
148
  this.cachedToken.expiresAtMs > nowMs + BROWSER_TOKEN_REFRESH_LEEWAY_MS) {
@@ -332,7 +150,7 @@ export class IngestClient {
332
150
  }
333
151
  return null;
334
152
  }
335
- async ensureBrowserToken(signal, deadlineAt) {
153
+ async ensureBrowserToken(signal) {
336
154
  const nowMs = Date.now();
337
155
  const cachedToken = this.getUsableCachedBrowserToken(nowMs);
338
156
  if (cachedToken) {
@@ -342,18 +160,17 @@ export class IngestClient {
342
160
  return await awaitWithAbort(this.pendingTokenRequest, signal);
343
161
  }
344
162
  const request = (async () => {
345
- const response = await fetchWithDeadline(this.browserTokenEndpoint, {
163
+ const response = await fetch((await this.endpoints()).browserTokenUrl, {
346
164
  method: "POST",
347
165
  headers: {
348
166
  "content-type": "application/json",
349
167
  "x-cluebase-project-key": this.projectKey,
350
168
  },
351
- body: JSON.stringify({
352
- projectKey: this.projectKey,
353
- }),
169
+ body: JSON.stringify({ projectKey: this.projectKey }),
354
170
  credentials: "omit",
355
171
  redirect: "error",
356
- }, signal, deadlineAt);
172
+ ...(signal ? { signal } : {}),
173
+ });
357
174
  if (!response.ok) {
358
175
  throw new BrowserTokenRequestError(response.status);
359
176
  }
@@ -392,7 +209,7 @@ export class IngestClient {
392
209
  try {
393
210
  return {
394
211
  ok: true,
395
- token: await this.ensureBrowserToken(options.signal, options.deadlineAt),
212
+ token: await this.ensureBrowserToken(options.signal),
396
213
  };
397
214
  }
398
215
  catch (error) {
@@ -411,35 +228,25 @@ export class IngestClient {
411
228
  }
412
229
  }
413
230
  }
414
- const asObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value)
415
- ? value
416
- : null;
417
- const diagnosticCanonicalPayload = (payload) => {
418
- try {
419
- return canonicalJsonStringify(JSON.parse(payload));
420
- }
421
- catch {
422
- return "{}";
231
+ const readBoundedBody = async (response, maxBytes) => {
232
+ if (!response.body)
233
+ return { text: "", overLimit: false };
234
+ const reader = response.body.getReader();
235
+ const decoder = new TextDecoder();
236
+ let bytes = 0;
237
+ let text = "";
238
+ while (true) {
239
+ const chunk = await reader.read();
240
+ if (chunk.done)
241
+ return { text: text + decoder.decode(), overLimit: false };
242
+ bytes += chunk.value.byteLength;
243
+ if (bytes > maxBytes) {
244
+ await reader.cancel();
245
+ return { text: "", overLimit: true };
246
+ }
247
+ text += decoder.decode(chunk.value, { stream: true });
423
248
  }
424
249
  };
425
- const isConsentAbort = (signal) => signal?.aborted === true && signal.reason === CONSENT_DENIED_ABORT_REASON;
426
- const sleep = (milliseconds, signal, deadlineAt) => new Promise((resolve) => {
427
- const remaining = remainingDeadlineMs(deadlineAt);
428
- if (signal?.aborted || (remaining !== null && remaining <= 0)) {
429
- resolve(false);
430
- return;
431
- }
432
- const abort = () => {
433
- clearTimeout(timeout);
434
- resolve(false);
435
- };
436
- const timeoutMs = Math.min(milliseconds, remaining ?? milliseconds);
437
- const timeout = setTimeout(() => {
438
- signal?.removeEventListener("abort", abort);
439
- resolve(timeoutMs === milliseconds);
440
- }, timeoutMs);
441
- signal?.addEventListener("abort", abort, { once: true });
442
- });
443
250
  const awaitWithAbort = async (promise, signal) => {
444
251
  if (!signal)
445
252
  return await promise;
@@ -457,27 +264,7 @@ const awaitWithAbort = async (promise, signal) => {
457
264
  });
458
265
  });
459
266
  };
460
- const fetchWithDeadline = async (input, init, externalSignal, deadlineAt) => {
461
- const controller = new AbortController();
462
- const abort = () => controller.abort();
463
- if (externalSignal?.aborted)
464
- abort();
465
- externalSignal?.addEventListener("abort", abort, { once: true });
466
- const remaining = remainingDeadlineMs(deadlineAt);
467
- if (remaining !== null && remaining <= 0)
468
- abort();
469
- const timeout = setTimeout(abort, Math.min(NORMAL_ATTEMPT_TIMEOUT_MS, remaining ?? NORMAL_ATTEMPT_TIMEOUT_MS));
470
- try {
471
- return await fetch(input, { ...init, signal: controller.signal });
472
- }
473
- finally {
474
- clearTimeout(timeout);
475
- externalSignal?.removeEventListener("abort", abort);
476
- }
477
- };
478
- const monotonicNow = () => typeof performance !== "undefined" ? performance.now() : Date.now();
479
- const remainingDeadlineMs = (deadlineAt) => deadlineAt === undefined ? null : Math.max(0, deadlineAt - monotonicNow());
480
- const hasDeadlineRemaining = (deadlineAt) => {
481
- const remaining = remainingDeadlineMs(deadlineAt);
482
- return remaining === null || remaining > 0;
483
- };
267
+ const isConsentAbort = (signal) => signal?.aborted === true && signal.reason === CONSENT_DENIED_ABORT_REASON;
268
+ const asObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value)
269
+ ? value
270
+ : null;
@@ -0,0 +1,71 @@
1
+ import type { DiagnosticCode } from "./diagnostics.js";
2
+ import { type CollectionPermit, type ConsentReport, type ConsentState, type DeliveryProgress, type DiagnosticEntry, type EnqueueReport, type FenceReport, type QueueDepthReport, type QueueLimitsInput, type RestoredEvent, type RestoreReport, type TransportOutcome, type TransportRequest } from "./sdk-core-delivery.js";
3
+ export type DeliveryTransport = (request: TransportRequest) => Promise<TransportOutcome>;
4
+ export type DeliveryClock = {
5
+ nowMs: () => number;
6
+ sleep: (delayMs: number) => Promise<void>;
7
+ };
8
+ export type DeliveryPipelineOptions = {
9
+ readonly endpoint: string;
10
+ readonly projectKey: string;
11
+ readonly sdkVersion: string;
12
+ readonly sourceSchemaVersion: string;
13
+ readonly sdkRequest: string;
14
+ readonly queueLimits: QueueLimitsInput;
15
+ readonly requestTimeoutMs?: number;
16
+ readonly initialConsent: ConsentState;
17
+ readonly transport: DeliveryTransport;
18
+ readonly clock: DeliveryClock;
19
+ };
20
+ export type AdmittedEvent = {
21
+ readonly eventKey: string;
22
+ readonly byteLength: number;
23
+ readonly diagnosticCanonicalBytes?: string;
24
+ };
25
+ export type DeliveryObservation = {
26
+ readonly progress: DeliveryProgress;
27
+ readonly diagnostics: readonly DiagnosticEntry[];
28
+ readonly settledEventKeys: readonly string[];
29
+ readonly droppedEventKeys: readonly string[];
30
+ readonly droppedEventCount: number;
31
+ readonly httpStatus: number | null;
32
+ readonly reason: string | null;
33
+ readonly privacyPolicy?: unknown;
34
+ readonly directiveUnreadable: boolean;
35
+ readonly diagnosticCodes: readonly DiagnosticCode[];
36
+ };
37
+ export type SerializedBatch = {
38
+ readonly body: string;
39
+ readonly diagnosticCanonicalBytes?: string;
40
+ };
41
+ export type DeliveryRunStop = "queue_drained" | "awaiting_result" | "consent_revoked" | "deadline_exceeded" | "retries_exhausted" | "retry_not_due" | "batch_unavailable";
42
+ export type DeliveryRunSummary = {
43
+ readonly observations: readonly DeliveryObservation[];
44
+ readonly attempts: number;
45
+ readonly stop: DeliveryRunStop;
46
+ };
47
+ export declare class DeliveryPipeline {
48
+ private readonly consentHandle;
49
+ private readonly driverOptions;
50
+ private handle;
51
+ private readonly transport;
52
+ private readonly clock;
53
+ private released;
54
+ constructor(options: DeliveryPipelineOptions);
55
+ reset(): void;
56
+ permit(): CollectionPermit;
57
+ admit(event: AdmittedEvent, permit: CollectionPermit): EnqueueReport;
58
+ adoptRestoredBatch(ordinal: number, events: readonly RestoredEvent[]): RestoreReport;
59
+ depth(envelopeOverheadBytes: number): QueueDepthReport;
60
+ pendingCount(): number;
61
+ fence(envelopeOverheadBytes: number): FenceReport;
62
+ rememberBrowserToken(token: string | null): void;
63
+ declareConsent(consent: ConsentState): ConsentReport;
64
+ beginClose(): void;
65
+ dispose(): boolean;
66
+ run(input: {
67
+ readonly body: (batchId: number, eventKeys: readonly string[]) => SerializedBatch | null;
68
+ readonly onObservation?: (observation: DeliveryObservation) => void;
69
+ readonly deadlineMs?: number | (() => number | undefined);
70
+ }): Promise<DeliveryRunSummary>;
71
+ }