@tracelog/capture-web 1.0.0

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.
@@ -0,0 +1,695 @@
1
+ (function (global) {
2
+ const EVENT_ENVELOPE_VERSION = 1;
3
+ const MAX_BATCH_BYTES = 262144;
4
+ const MAX_BATCH_EVENTS = 50;
5
+ const MAX_CONTEXT_BYTES = 8192;
6
+ const MAX_ERROR_MESSAGE_BYTES = 1024;
7
+ const CONSENT_KEY = "__tl.c";
8
+ const SESSION_KEY = "__tl.s";
9
+ const ACTIVITY_KEY = "__tl.a";
10
+ const QUEUE_KEY = "__tl.q";
11
+ /**
12
+ * How many events wait in memory before consent. A platform artifact that
13
+ * buffers its own platform's events ahead of this engine holds the same line,
14
+ * so the number is exported rather than mirrored.
15
+ */
16
+ const PRE_CONSENT_CAP = 100;
17
+ const QUEUE_CAP = 200;
18
+ const SESSION_INACTIVITY_MS = 30 * 60 * 1000;
19
+ const FLUSH_INTERVAL_MS = 5 * 1000;
20
+ const RETRY_BASE_MS = 1000;
21
+ const RETRY_CAP_MS = 60 * 1000;
22
+ const CIRCUIT_FAILURE_THRESHOLD = 5;
23
+ const CIRCUIT_PROBE_MS = 60 * 1000;
24
+ const EVENT_NAME_PATTERN = /^[a-z][a-z0-9_]{0,63}$/;
25
+ const PUBLIC_KEY_PATTERN = /^tl_pk_[a-z2-7]{26}$/;
26
+ /**
27
+ * The contract states these inside its schemas (`conversionEventSchema`,
28
+ * `currencySchema`) and exports no constant for them; the schemas themselves
29
+ * are not imported because zod is not part of the runtime a visitor loads.
30
+ */
31
+ const MAX_IDENTIFIER_LENGTH = 256;
32
+ const CURRENCY_PATTERN = /^[A-Z]{3}$/;
33
+ const textEncoder = new TextEncoder();
34
+ function safeGet(storage, key) {
35
+ try {
36
+ return storage.getItem(key);
37
+ }
38
+ catch {
39
+ return null;
40
+ }
41
+ }
42
+ function safeSet(storage, key, value) {
43
+ try {
44
+ storage.setItem(key, value);
45
+ }
46
+ catch {
47
+ // Capture must never break the host page when storage is unavailable.
48
+ }
49
+ }
50
+ function safeRemove(storage, key) {
51
+ try {
52
+ storage.removeItem(key);
53
+ }
54
+ catch {
55
+ // Capture must never break the host page when storage is unavailable.
56
+ }
57
+ }
58
+ function emptyQueue() {
59
+ return { events: [], drops: {} };
60
+ }
61
+ function readQueue(storage) {
62
+ const serialized = safeGet(storage, QUEUE_KEY);
63
+ if (serialized === null)
64
+ return emptyQueue();
65
+ try {
66
+ const value = JSON.parse(serialized);
67
+ if (typeof value !== "object" || value === null)
68
+ return emptyQueue();
69
+ const candidate = value;
70
+ if (!Array.isArray(candidate.events))
71
+ return emptyQueue();
72
+ return {
73
+ events: candidate.events,
74
+ drops: typeof candidate.drops === "object" && candidate.drops !== null
75
+ ? candidate.drops
76
+ : {},
77
+ };
78
+ }
79
+ catch {
80
+ return emptyQueue();
81
+ }
82
+ }
83
+ function writeQueue(storage, queue) {
84
+ safeSet(storage, QUEUE_KEY, JSON.stringify(queue));
85
+ }
86
+ function addDrop(queue, reason, count = 1) {
87
+ queue.drops[reason] = (queue.drops[reason] ?? 0) + count;
88
+ }
89
+ function randomBytes(length) {
90
+ const bytes = new Uint8Array(length);
91
+ const cryptoSource = globalThis.crypto;
92
+ if (cryptoSource !== undefined)
93
+ return cryptoSource.getRandomValues(bytes);
94
+ for (let index = 0; index < bytes.length; index += 1) {
95
+ bytes[index] = Math.floor(Math.random() * 256);
96
+ }
97
+ return bytes;
98
+ }
99
+ function uuidv7(at) {
100
+ const bytes = randomBytes(16);
101
+ let timestamp = at.getTime();
102
+ for (let index = 5; index >= 0; index -= 1) {
103
+ bytes[index] = timestamp % 256;
104
+ timestamp = Math.floor(timestamp / 256);
105
+ }
106
+ bytes[6] = 0x70 | (bytes[6] & 0x0f);
107
+ bytes[8] = 0x80 | (bytes[8] & 0x3f);
108
+ const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0"));
109
+ return `${hex.slice(0, 4).join("")}-${hex.slice(4, 6).join("")}-${hex.slice(6, 8).join("")}-${hex.slice(8, 10).join("")}-${hex.slice(10).join("")}`;
110
+ }
111
+ function jsonContext(value) {
112
+ if (value === undefined)
113
+ return undefined;
114
+ try {
115
+ const serialized = JSON.stringify(value);
116
+ if (textEncoder.encode(serialized).byteLength > MAX_CONTEXT_BYTES)
117
+ return undefined;
118
+ const parsed = JSON.parse(serialized);
119
+ if (typeof parsed !== "object" ||
120
+ parsed === null ||
121
+ Array.isArray(parsed)) {
122
+ return undefined;
123
+ }
124
+ return parsed;
125
+ }
126
+ catch {
127
+ return undefined;
128
+ }
129
+ }
130
+ function truncateUtf8(value, maxBytes) {
131
+ if (textEncoder.encode(value).byteLength <= maxBytes)
132
+ return value;
133
+ let low = 0;
134
+ let high = value.length;
135
+ while (low < high) {
136
+ const middle = Math.ceil((low + high) / 2);
137
+ if (textEncoder.encode(value.slice(0, middle)).byteLength <= maxBytes) {
138
+ low = middle;
139
+ }
140
+ else {
141
+ high = middle - 1;
142
+ }
143
+ }
144
+ return value.slice(0, low);
145
+ }
146
+ function isConversionValid(options) {
147
+ return (options.identifier.length >= 1 &&
148
+ options.identifier.length <= MAX_IDENTIFIER_LENGTH &&
149
+ (options.value === undefined ||
150
+ (Number.isFinite(options.value) && options.value >= 0)) &&
151
+ (options.currency === undefined || CURRENCY_PATTERN.test(options.currency)));
152
+ }
153
+ function batchFrom(events) {
154
+ const selected = [];
155
+ const unsendable = [];
156
+ for (const event of events) {
157
+ if (selected.length >= MAX_BATCH_EVENTS)
158
+ break;
159
+ const candidate = {
160
+ v: EVENT_ENVELOPE_VERSION,
161
+ events: [...selected, event],
162
+ };
163
+ if (textEncoder.encode(JSON.stringify(candidate)).byteLength > MAX_BATCH_BYTES) {
164
+ if (selected.length === 0) {
165
+ unsendable.push(event);
166
+ continue;
167
+ }
168
+ break;
169
+ }
170
+ selected.push(event);
171
+ }
172
+ return { batch: { v: EVENT_ENVELOPE_VERSION, events: selected }, unsendable };
173
+ }
174
+ function createCaptureEngine(ports, acquisition) {
175
+ let consentState = "unknown";
176
+ let initialized = false;
177
+ let config = { key: "", endpoint: "/v1/events" };
178
+ let pending = [];
179
+ let lastDeclaredName;
180
+ let sending = false;
181
+ let flushTimer;
182
+ let consecutiveFailures = 0;
183
+ let circuit = "closed";
184
+ let nextProbeAt = 0;
185
+ function scheduleFlush(delay) {
186
+ if (flushTimer !== undefined)
187
+ clearTimeout(flushTimer);
188
+ flushTimer = setTimeout(() => {
189
+ flushTimer = undefined;
190
+ void flush(false);
191
+ }, delay);
192
+ }
193
+ function persistEvent(event) {
194
+ const queue = readQueue(ports.storage);
195
+ queue.events.push(event);
196
+ while (queue.events.length > QUEUE_CAP) {
197
+ queue.events.shift();
198
+ addDrop(queue, "queue_overflow");
199
+ }
200
+ writeQueue(ports.storage, queue);
201
+ }
202
+ function sessionFor(now) {
203
+ const storedId = safeGet(ports.storage, SESSION_KEY);
204
+ const activityValue = safeGet(ports.storage, ACTIVITY_KEY);
205
+ const lastActivity = activityValue === null ? Number.NaN : Number(activityValue);
206
+ const rotated = storedId === null ||
207
+ !Number.isFinite(lastActivity) ||
208
+ now.getTime() - lastActivity >= SESSION_INACTIVITY_MS;
209
+ const id = rotated ? uuidv7(now) : storedId;
210
+ if (rotated)
211
+ safeSet(ports.storage, SESSION_KEY, id);
212
+ safeSet(ports.storage, ACTIVITY_KEY, String(now.getTime()));
213
+ return { id, started: rotated };
214
+ }
215
+ function baseEvent(now, sessionId) {
216
+ return {
217
+ eventId: uuidv7(now),
218
+ sessionId,
219
+ occurredAt: now.toISOString(),
220
+ ...(acquisition.mode === undefined ? {} : { mode: acquisition.mode }),
221
+ };
222
+ }
223
+ function recordInvalidEvent() {
224
+ const queue = readQueue(ports.storage);
225
+ addDrop(queue, "invalid_event");
226
+ writeQueue(ports.storage, queue);
227
+ }
228
+ function emit(pendingEvent) {
229
+ if (!EVENT_NAME_PATTERN.test(pendingEvent.name)) {
230
+ recordInvalidEvent();
231
+ return;
232
+ }
233
+ if (pendingEvent.kind === "conversion" &&
234
+ !isConversionValid(pendingEvent.options)) {
235
+ recordInvalidEvent();
236
+ return;
237
+ }
238
+ const now = ports.clock.now();
239
+ const session = sessionFor(now);
240
+ if (session.started) {
241
+ persistEvent({
242
+ ...baseEvent(now, session.id),
243
+ kind: "session_start",
244
+ name: "session_started",
245
+ ...(acquisition.referrer === undefined
246
+ ? {}
247
+ : { referrer: acquisition.referrer }),
248
+ utm: acquisition.utm,
249
+ landingPage: acquisition.landingPage,
250
+ device: acquisition.device,
251
+ });
252
+ }
253
+ if (pendingEvent.kind === "step") {
254
+ const context = jsonContext(pendingEvent.context);
255
+ persistEvent({
256
+ ...baseEvent(now, session.id),
257
+ kind: "step",
258
+ name: pendingEvent.name,
259
+ ...(context === undefined ? {} : { context }),
260
+ });
261
+ }
262
+ else {
263
+ const options = pendingEvent.options;
264
+ const context = jsonContext(options.context);
265
+ persistEvent({
266
+ ...baseEvent(now, session.id),
267
+ kind: "conversion",
268
+ name: pendingEvent.name,
269
+ identifier: options.identifier,
270
+ ...(options.value === undefined ? {} : { value: options.value }),
271
+ ...(options.currency === undefined
272
+ ? {}
273
+ : { currency: options.currency }),
274
+ ...(context === undefined ? {} : { context }),
275
+ });
276
+ }
277
+ lastDeclaredName = pendingEvent.name;
278
+ }
279
+ function capture(pendingEvent) {
280
+ if (!initialized || consentState === "denied")
281
+ return;
282
+ if (consentState === "unknown") {
283
+ if (pending.length < PRE_CONSENT_CAP)
284
+ pending.push(pendingEvent);
285
+ return;
286
+ }
287
+ emit(pendingEvent);
288
+ }
289
+ async function flush(keepalive = false) {
290
+ if (!initialized || consentState !== "granted" || sending)
291
+ return;
292
+ const queue = readQueue(ports.storage);
293
+ if (queue.events.length === 0 || !PUBLIC_KEY_PATTERN.test(config.key)) {
294
+ scheduleFlush(FLUSH_INTERVAL_MS);
295
+ return;
296
+ }
297
+ const now = ports.clock.now().getTime();
298
+ if (circuit === "open") {
299
+ if (now < nextProbeAt) {
300
+ scheduleFlush(nextProbeAt - now);
301
+ return;
302
+ }
303
+ circuit = "half_open";
304
+ }
305
+ const { batch, unsendable } = batchFrom(queue.events);
306
+ if (unsendable.length > 0) {
307
+ const dropped = new Set(unsendable.map((event) => event.eventId));
308
+ queue.events = queue.events.filter((event) => !dropped.has(event.eventId));
309
+ addDrop(queue, "invalid_event", unsendable.length);
310
+ writeQueue(ports.storage, queue);
311
+ }
312
+ if (batch.events.length === 0) {
313
+ scheduleFlush(FLUSH_INTERVAL_MS);
314
+ return;
315
+ }
316
+ sending = true;
317
+ try {
318
+ const response = await ports.transport.send({
319
+ endpoint: config.endpoint,
320
+ key: config.key,
321
+ batch,
322
+ keepalive,
323
+ });
324
+ if (response.status >= 200 && response.status < 300) {
325
+ const currentQueue = readQueue(ports.storage);
326
+ const delivered = new Set(batch.events.map((event) => event.eventId));
327
+ currentQueue.events = currentQueue.events.filter((event) => !delivered.has(event.eventId));
328
+ if ((response.rejected ?? 0) > 0) {
329
+ addDrop(currentQueue, "server_rejected", response.rejected);
330
+ }
331
+ writeQueue(ports.storage, currentQueue);
332
+ consecutiveFailures = 0;
333
+ circuit = "closed";
334
+ scheduleFlush(currentQueue.events.length === 0 ? FLUSH_INTERVAL_MS : 0);
335
+ return;
336
+ }
337
+ /**
338
+ * A 4xx is the server's word that this batch will never be accepted —
339
+ * except a 429, which is the server's word that it will be accepted
340
+ * later: the ingestion limit is per public key, shared by every visitor
341
+ * of one site, so a busy minute answers 429 to visitors whose events
342
+ * are sound. Dropping those is silent loss ([spec/capture.md]
343
+ * § Delivery); they take the retry path with everything else transient.
344
+ */
345
+ if (response.status >= 400 &&
346
+ response.status < 500 &&
347
+ response.status !== 429) {
348
+ const currentQueue = readQueue(ports.storage);
349
+ const rejected = new Set(batch.events.map((event) => event.eventId));
350
+ currentQueue.events = currentQueue.events.filter((event) => !rejected.has(event.eventId));
351
+ addDrop(currentQueue, "server_rejected", batch.events.length);
352
+ writeQueue(ports.storage, currentQueue);
353
+ consecutiveFailures = 0;
354
+ circuit = "closed";
355
+ scheduleFlush(currentQueue.events.length === 0 ? FLUSH_INTERVAL_MS : 0);
356
+ return;
357
+ }
358
+ throw new Error("retryable response");
359
+ }
360
+ catch {
361
+ consecutiveFailures += 1;
362
+ if (circuit === "half_open" ||
363
+ consecutiveFailures >= CIRCUIT_FAILURE_THRESHOLD) {
364
+ circuit = "open";
365
+ nextProbeAt = ports.clock.now().getTime() + CIRCUIT_PROBE_MS;
366
+ scheduleFlush(CIRCUIT_PROBE_MS);
367
+ }
368
+ else {
369
+ scheduleFlush(Math.min(RETRY_BASE_MS * 2 ** (consecutiveFailures - 1), RETRY_CAP_MS));
370
+ }
371
+ }
372
+ finally {
373
+ sending = false;
374
+ }
375
+ }
376
+ const engine = {
377
+ init(options) {
378
+ config = {
379
+ key: options.key,
380
+ endpoint: options.endpoint ?? "/v1/events",
381
+ };
382
+ initialized = true;
383
+ const storedConsent = safeGet(ports.storage, CONSENT_KEY);
384
+ consentState =
385
+ storedConsent === "granted" || storedConsent === "denied"
386
+ ? storedConsent
387
+ : "unknown";
388
+ if (consentState === "granted")
389
+ scheduleFlush(FLUSH_INTERVAL_MS);
390
+ },
391
+ consent: {
392
+ grant() {
393
+ if (!initialized)
394
+ return;
395
+ consentState = "granted";
396
+ safeSet(ports.storage, CONSENT_KEY, "granted");
397
+ const buffered = pending;
398
+ pending = [];
399
+ for (const pendingEvent of buffered)
400
+ emit(pendingEvent);
401
+ scheduleFlush(FLUSH_INTERVAL_MS);
402
+ },
403
+ deny() {
404
+ if (!initialized)
405
+ return;
406
+ consentState = "denied";
407
+ pending = [];
408
+ lastDeclaredName = undefined;
409
+ safeSet(ports.storage, CONSENT_KEY, "denied");
410
+ safeRemove(ports.storage, SESSION_KEY);
411
+ safeRemove(ports.storage, ACTIVITY_KEY);
412
+ safeRemove(ports.storage, QUEUE_KEY);
413
+ if (flushTimer !== undefined)
414
+ clearTimeout(flushTimer);
415
+ flushTimer = undefined;
416
+ },
417
+ state() {
418
+ return consentState;
419
+ },
420
+ },
421
+ step(name, context) {
422
+ capture({
423
+ kind: "step",
424
+ name,
425
+ ...(context === undefined ? {} : { context }),
426
+ });
427
+ },
428
+ conversion(name, options) {
429
+ capture({ kind: "conversion", name, options });
430
+ },
431
+ };
432
+ return {
433
+ engine,
434
+ flush,
435
+ captureError(message) {
436
+ if (!initialized ||
437
+ consentState !== "granted" ||
438
+ lastDeclaredName === undefined) {
439
+ return;
440
+ }
441
+ const now = ports.clock.now();
442
+ const session = sessionFor(now);
443
+ if (session.started) {
444
+ persistEvent({
445
+ ...baseEvent(now, session.id),
446
+ kind: "session_start",
447
+ name: "session_started",
448
+ ...(acquisition.referrer === undefined
449
+ ? {}
450
+ : { referrer: acquisition.referrer }),
451
+ utm: acquisition.utm,
452
+ landingPage: acquisition.landingPage,
453
+ device: acquisition.device,
454
+ });
455
+ }
456
+ persistEvent({
457
+ ...baseEvent(now, session.id),
458
+ kind: "error",
459
+ name: "capture_error",
460
+ message: truncateUtf8(message, MAX_ERROR_MESSAGE_BYTES),
461
+ stepName: lastDeclaredName,
462
+ });
463
+ },
464
+ drops() {
465
+ const queue = readQueue(ports.storage);
466
+ return ["queue_overflow", "server_rejected", "invalid_event"]
467
+ .map((reason) => ({ reason, count: queue.drops[reason] ?? 0 }))
468
+ .filter((drop) => drop.count > 0);
469
+ },
470
+ circuitState() {
471
+ return circuit;
472
+ },
473
+ };
474
+ }
475
+ /**
476
+ * The browser's clock, and the one file in this package allowed to read it
477
+ * ([CLAUDE.md] § Invariants: time is injected). `capture-core` declares the
478
+ * port and reads no clock at all — it runs inside sandboxes that have none —
479
+ * so the implementation lives here, with the runtime that has a `Date`.
480
+ */
481
+ const systemClock = {
482
+ now: () => new Date(Date.now()),
483
+ };
484
+ const WEB_PUBLIC_KEY_PATTERN = /^tl_pk_[a-z2-7]{26}$/;
485
+ class MemoryStorage {
486
+ constructor() {
487
+ this.values = new Map();
488
+ }
489
+ getItem(key) {
490
+ return this.values.get(key) ?? null;
491
+ }
492
+ setItem(key, value) {
493
+ this.values.set(key, value);
494
+ }
495
+ removeItem(key) {
496
+ this.values.delete(key);
497
+ }
498
+ }
499
+ let declaredMode;
500
+ let runtime;
501
+ let engine;
502
+ let verification;
503
+ let configValid = false;
504
+ let listenersInstalled = false;
505
+ function browserStorage() {
506
+ try {
507
+ return window.localStorage;
508
+ }
509
+ catch {
510
+ return new MemoryStorage();
511
+ }
512
+ }
513
+ function verificationMode() {
514
+ const nonce = new URL(window.location.href).searchParams.get("__tl_verify");
515
+ return nonce !== null && nonce.length > 0 && window.opener !== null
516
+ ? { nonce, opener: window.opener }
517
+ : undefined;
518
+ }
519
+ function landingPage() {
520
+ const url = new URL(window.location.href);
521
+ url.searchParams.delete("__tl_verify");
522
+ url.hash = "";
523
+ return url.toString();
524
+ }
525
+ function utm() {
526
+ const search = new URL(window.location.href).searchParams;
527
+ const result = {};
528
+ const mappings = [
529
+ ["utm_source", "source"],
530
+ ["utm_medium", "medium"],
531
+ ["utm_campaign", "campaign"],
532
+ ["utm_term", "term"],
533
+ ["utm_content", "content"],
534
+ ];
535
+ for (const [parameter, field] of mappings) {
536
+ const value = search.get(parameter);
537
+ if (value !== null)
538
+ result[field] = value;
539
+ }
540
+ return result;
541
+ }
542
+ function device() {
543
+ const agent = window.navigator.userAgent;
544
+ if (/iPad|Tablet|Android(?!.*Mobile)/i.test(agent))
545
+ return "tablet";
546
+ if (/Mobi|Android/i.test(agent))
547
+ return "mobile";
548
+ return "desktop";
549
+ }
550
+ function acquisition() {
551
+ const referrer = window.document.referrer;
552
+ return {
553
+ ...(referrer.length === 0 ? {} : { referrer }),
554
+ utm: utm(),
555
+ landingPage: landingPage(),
556
+ device: device(),
557
+ ...(verification === undefined && declaredMode === undefined
558
+ ? {}
559
+ : { mode: "verification" }),
560
+ };
561
+ }
562
+ function isEndpointValid(endpoint) {
563
+ try {
564
+ const url = new URL(endpoint, window.location.href);
565
+ return url.protocol === "https:" || url.protocol === "http:";
566
+ }
567
+ catch {
568
+ return false;
569
+ }
570
+ }
571
+ function reportDiagnostic() {
572
+ if (verification === undefined)
573
+ return;
574
+ verification.opener.postMessage({
575
+ type: "tracelog:diag",
576
+ nonce: verification.nonce,
577
+ present: true,
578
+ consent: engine?.consent.state() ?? "unknown",
579
+ configValid,
580
+ drops: runtime?.drops() ?? [],
581
+ }, "*");
582
+ }
583
+ async function responseSummary(response) {
584
+ if (response.status < 200 || response.status >= 300) {
585
+ return { status: response.status };
586
+ }
587
+ try {
588
+ const body = (await response.json());
589
+ const rejected = typeof body === "object" &&
590
+ body !== null &&
591
+ "rejected" in body &&
592
+ Array.isArray(body.rejected)
593
+ ? body.rejected.length
594
+ : 0;
595
+ return rejected === 0
596
+ ? { status: response.status }
597
+ : { status: response.status, rejected };
598
+ }
599
+ catch {
600
+ return { status: response.status };
601
+ }
602
+ }
603
+ function errorMessage(reason) {
604
+ if (reason instanceof Error)
605
+ return reason.message;
606
+ if (typeof reason === "string")
607
+ return reason;
608
+ return "Unhandled promise rejection";
609
+ }
610
+ function installListeners() {
611
+ if (listenersInstalled)
612
+ return;
613
+ listenersInstalled = true;
614
+ window.addEventListener("error", (event) => {
615
+ runtime?.captureError(event.message || "Unknown error");
616
+ reportDiagnostic();
617
+ });
618
+ window.addEventListener("unhandledrejection", (event) => {
619
+ runtime?.captureError(errorMessage(event.reason));
620
+ reportDiagnostic();
621
+ });
622
+ window.document.addEventListener("visibilitychange", () => {
623
+ if (window.document.visibilityState === "hidden") {
624
+ void runtime?.flush(true).finally(reportDiagnostic);
625
+ }
626
+ });
627
+ }
628
+ function init(options) {
629
+ verification = verificationMode();
630
+ declaredMode = options.mode;
631
+ const endpoint = options.endpoint ?? "/v1/events";
632
+ configValid =
633
+ WEB_PUBLIC_KEY_PATTERN.test(options.key) && isEndpointValid(endpoint);
634
+ if (runtime === undefined) {
635
+ runtime = createCaptureEngine({
636
+ clock: systemClock,
637
+ storage: browserStorage(),
638
+ transport: {
639
+ async send(request) {
640
+ try {
641
+ const response = await window.fetch(request.endpoint, {
642
+ method: "POST",
643
+ headers: {
644
+ Authorization: `Bearer ${request.key}`,
645
+ "Content-Type": "application/json",
646
+ },
647
+ body: JSON.stringify(request.batch),
648
+ keepalive: request.keepalive,
649
+ });
650
+ const summary = await responseSummary(response);
651
+ reportDiagnostic();
652
+ return summary;
653
+ }
654
+ catch (error) {
655
+ reportDiagnostic();
656
+ throw error;
657
+ }
658
+ },
659
+ },
660
+ }, acquisition());
661
+ engine = runtime.engine;
662
+ }
663
+ engine.init({
664
+ key: configValid ? options.key : "",
665
+ ...(options.endpoint === undefined ? {} : { endpoint }),
666
+ });
667
+ installListeners();
668
+ reportDiagnostic();
669
+ }
670
+ const TraceLog = {
671
+ init,
672
+ consent: {
673
+ grant() {
674
+ engine?.consent.grant();
675
+ reportDiagnostic();
676
+ },
677
+ deny() {
678
+ engine?.consent.deny();
679
+ reportDiagnostic();
680
+ },
681
+ state() {
682
+ return engine?.consent.state() ?? "unknown";
683
+ },
684
+ },
685
+ step(name, context) {
686
+ engine?.step(name, context);
687
+ reportDiagnostic();
688
+ },
689
+ conversion(name, options) {
690
+ engine?.conversion(name, options);
691
+ reportDiagnostic();
692
+ },
693
+ };
694
+ global.TraceLog = TraceLog;
695
+ })(globalThis);