@loggerjs/browser 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +93 -0
  3. package/dist/broadcast-channel-transport.d.ts +38 -0
  4. package/dist/broadcast-channel-transport.d.ts.map +1 -0
  5. package/dist/console-integration.d.ts +11 -0
  6. package/dist/console-integration.d.ts.map +1 -0
  7. package/dist/error-integration.d.ts +10 -0
  8. package/dist/error-integration.d.ts.map +1 -0
  9. package/dist/fetch-integration.d.ts +13 -0
  10. package/dist/fetch-integration.d.ts.map +1 -0
  11. package/dist/framework-error-integration.d.ts +29 -0
  12. package/dist/framework-error-integration.d.ts.map +1 -0
  13. package/dist/http-capture-utils.d.ts +7 -0
  14. package/dist/http-capture-utils.d.ts.map +1 -0
  15. package/dist/http-transport.d.ts +49 -0
  16. package/dist/http-transport.d.ts.map +1 -0
  17. package/dist/index.cjs +3151 -0
  18. package/dist/index.cjs.map +1 -0
  19. package/dist/index.d.ts +23 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +3119 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/indexeddb-offline-queue.d.ts +16 -0
  24. package/dist/indexeddb-offline-queue.d.ts.map +1 -0
  25. package/dist/indexeddb-transport.d.ts +80 -0
  26. package/dist/indexeddb-transport.d.ts.map +1 -0
  27. package/dist/page-lifecycle.d.ts +8 -0
  28. package/dist/page-lifecycle.d.ts.map +1 -0
  29. package/dist/performance-integration.d.ts +45 -0
  30. package/dist/performance-integration.d.ts.map +1 -0
  31. package/dist/reporting-integration.d.ts +49 -0
  32. package/dist/reporting-integration.d.ts.map +1 -0
  33. package/dist/router-integration.d.ts +34 -0
  34. package/dist/router-integration.d.ts.map +1 -0
  35. package/dist/runtime-host-integration.d.ts +46 -0
  36. package/dist/runtime-host-integration.d.ts.map +1 -0
  37. package/dist/service-worker-integration.d.ts +28 -0
  38. package/dist/service-worker-integration.d.ts.map +1 -0
  39. package/dist/service-worker-transport.d.ts +51 -0
  40. package/dist/service-worker-transport.d.ts.map +1 -0
  41. package/dist/user-action-integration.d.ts +37 -0
  42. package/dist/user-action-integration.d.ts.map +1 -0
  43. package/dist/web-vitals-integration.d.ts +23 -0
  44. package/dist/web-vitals-integration.d.ts.map +1 -0
  45. package/dist/websocket-integration.d.ts +41 -0
  46. package/dist/websocket-integration.d.ts.map +1 -0
  47. package/dist/websocket-transport.d.ts +37 -0
  48. package/dist/websocket-transport.d.ts.map +1 -0
  49. package/dist/xhr-integration.d.ts +11 -0
  50. package/dist/xhr-integration.d.ts.map +1 -0
  51. package/dist/zip-export.d.ts +53 -0
  52. package/dist/zip-export.d.ts.map +1 -0
  53. package/package.json +152 -0
package/dist/index.js ADDED
@@ -0,0 +1,3119 @@
1
+ import { incrementLoggerMetaCounter, normalizeValue, safeJsonCodec, safeJsonStringify, toLevelValue } from "@loggerjs/core";
2
+ export * from "@loggerjs/core";
3
+ //#region src/broadcast-channel-transport.ts
4
+ let sourceSequence$1 = 0;
5
+ function createSourceId$1() {
6
+ sourceSequence$1 += 1;
7
+ return `loggerjs-${Date.now().toString(36)}-${sourceSequence$1.toString(36)}`;
8
+ }
9
+ function defaultChannelFactory(channelName) {
10
+ if (typeof BroadcastChannel === "undefined") throw new Error("BroadcastChannel is not available");
11
+ return new BroadcastChannel(channelName);
12
+ }
13
+ function defaultMapEvent$1(event, context) {
14
+ return {
15
+ type: "loggerjs.event",
16
+ source: context.source,
17
+ event
18
+ };
19
+ }
20
+ function defaultMapBatch$1(events, context) {
21
+ return {
22
+ type: "loggerjs.batch",
23
+ source: context.source,
24
+ events
25
+ };
26
+ }
27
+ function browserBroadcastChannelTransport(options) {
28
+ const transportName = options.name ?? "browser-broadcast-channel";
29
+ const source = options.source ?? createSourceId$1();
30
+ const mapContext = {
31
+ channelName: options.channelName,
32
+ source
33
+ };
34
+ const mapEvent = options.mapEvent ?? defaultMapEvent$1;
35
+ const mapBatch = options.mapBatch ?? defaultMapBatch$1;
36
+ const channelFactory = options.channelFactory ?? defaultChannelFactory;
37
+ const closeChannelOnClose = options.closeChannelOnClose ?? true;
38
+ let channel;
39
+ let closed = false;
40
+ const reportError = (error, context, detail) => {
41
+ if (detail.droppedEvents > 0) {
42
+ incrementLoggerMetaCounter("transport.dropped", detail.droppedEvents);
43
+ incrementLoggerMetaCounter(`transport.dropped.${detail.operation}`, detail.droppedEvents);
44
+ }
45
+ try {
46
+ options.onError?.(error, detail);
47
+ } catch (onErrorError) {
48
+ context?.reportInternalError(onErrorError, {
49
+ operation: "on-error",
50
+ phase: "transport",
51
+ transport: transportName
52
+ });
53
+ }
54
+ context?.reportInternalError(error, {
55
+ operation: detail.operation,
56
+ phase: "transport",
57
+ transport: transportName
58
+ });
59
+ };
60
+ const getChannel = (context) => {
61
+ if (closed) return void 0;
62
+ if (channel) return channel;
63
+ try {
64
+ channel = channelFactory(options.channelName);
65
+ return channel;
66
+ } catch (error) {
67
+ reportError(error, context, {
68
+ droppedEvents: 1,
69
+ operation: "create-channel"
70
+ });
71
+ return;
72
+ }
73
+ };
74
+ const postMessage = (message, context, droppedEvents) => {
75
+ const currentChannel = getChannel(context);
76
+ if (!currentChannel) return;
77
+ try {
78
+ currentChannel.postMessage(message);
79
+ } catch (error) {
80
+ reportError(error, context, {
81
+ droppedEvents,
82
+ operation: "post-message"
83
+ });
84
+ }
85
+ };
86
+ return {
87
+ name: transportName,
88
+ minLevel: options.minLevel,
89
+ log(event, context) {
90
+ postMessage(mapEvent(event, mapContext), context, 1);
91
+ },
92
+ logBatch(events, context) {
93
+ if (events.length === 0) return;
94
+ postMessage(mapBatch(events, mapContext), context, events.length);
95
+ },
96
+ async close() {
97
+ closed = true;
98
+ if (!channel || !closeChannelOnClose) return;
99
+ try {
100
+ channel.close?.();
101
+ } catch (error) {
102
+ reportError(error, void 0, {
103
+ droppedEvents: 0,
104
+ operation: "close-channel"
105
+ });
106
+ } finally {
107
+ channel = void 0;
108
+ }
109
+ }
110
+ };
111
+ }
112
+ //#endregion
113
+ //#region src/http-transport.ts
114
+ const DEFAULT_BEACON_MAX_BYTES = 60 * 1024;
115
+ const sleep = (delayMs) => delayMs <= 0 ? Promise.resolve() : new Promise((resolve) => setTimeout(resolve, delayMs));
116
+ function memoryBrowserHttpOfflineQueue(options = {}) {
117
+ const entries = [];
118
+ const maxEntries = options.maxEntries ?? 1e3;
119
+ const dropPolicy = options.dropPolicy ?? "drop-oldest";
120
+ const drop = (entry, reason) => {
121
+ incrementLoggerMetaCounter("transport.offline.dropped");
122
+ incrementLoggerMetaCounter(`transport.offline.dropped.${reason}`);
123
+ options.onDrop?.(entry, reason);
124
+ };
125
+ return {
126
+ enqueue(entry) {
127
+ if (entries.length >= maxEntries) {
128
+ if (dropPolicy === "drop-newest") {
129
+ drop(entry, "queue-full");
130
+ return;
131
+ }
132
+ const dropped = entries.shift();
133
+ if (dropped) drop(dropped, "queue-full");
134
+ }
135
+ entries.push(entry);
136
+ },
137
+ async replay(send) {
138
+ while (entries.length > 0) {
139
+ const entry = entries[0];
140
+ if (!entry) return;
141
+ await send(entry);
142
+ entries.shift();
143
+ }
144
+ },
145
+ size() {
146
+ return entries.length;
147
+ }
148
+ };
149
+ }
150
+ function payloadToBody(payload) {
151
+ if (typeof payload === "string") return payload;
152
+ return Uint8Array.from(payload);
153
+ }
154
+ function payloadByteLength$1(payload) {
155
+ if (typeof payload !== "string") return payload.byteLength;
156
+ if (typeof TextEncoder !== "undefined") return new TextEncoder().encode(payload).byteLength;
157
+ return new Blob([payload]).size;
158
+ }
159
+ function payloadToBeaconBody(payload, contentType) {
160
+ return new Blob([typeof payload === "string" ? payload : Uint8Array.from(payload)], { type: contentType });
161
+ }
162
+ function remainingEvents(chunks, startIndex) {
163
+ const events = [];
164
+ for (let index = startIndex; index < chunks.length; index++) {
165
+ const chunk = chunks[index];
166
+ if (chunk) events.push(...chunk.events);
167
+ }
168
+ return events;
169
+ }
170
+ function browserHttpTransport(options) {
171
+ const codec = options.codec ?? safeJsonCodec();
172
+ const queue = [];
173
+ const maxBatchSize = options.maxBatchSize ?? 50;
174
+ const flushIntervalMs = options.flushIntervalMs ?? 2e3;
175
+ const maxQueueSize = options.maxQueueSize ?? 1e3;
176
+ const dropPolicy = options.dropPolicy ?? "drop-oldest";
177
+ const beaconMaxBytes = options.beaconMaxBytes ?? DEFAULT_BEACON_MAX_BYTES;
178
+ const offlineQueue = options.offlineQueue;
179
+ const offlineReplayMaxRetries = options.offlineReplayMaxRetries ?? 3;
180
+ const offlineReplayBaseDelayMs = options.offlineReplayBaseDelayMs ?? 250;
181
+ const offlineReplayMaxDelayMs = options.offlineReplayMaxDelayMs ?? 5e3;
182
+ const random = options.random ?? Math.random;
183
+ const fetchFn = options.fetchFn ?? globalThis.fetch?.bind(globalThis);
184
+ let offlineEntrySeq = 0;
185
+ let timer;
186
+ let flushing = false;
187
+ let replayingOffline = false;
188
+ let lastContext;
189
+ const headers = () => ({
190
+ "content-type": codec.contentType,
191
+ ...options.headers
192
+ });
193
+ const clearTimer = () => {
194
+ if (timer) clearTimeout(timer);
195
+ timer = void 0;
196
+ };
197
+ const reportDrop = (event, reason) => {
198
+ incrementLoggerMetaCounter("transport.dropped");
199
+ incrementLoggerMetaCounter(`transport.dropped.${reason}`);
200
+ options.onDrop?.(event, reason);
201
+ };
202
+ const reportInternalError = (error, operation) => {
203
+ lastContext?.reportInternalError(error, {
204
+ phase: "transport",
205
+ transport: options.name ?? "browser-http",
206
+ operation
207
+ });
208
+ };
209
+ const createBeaconChunks = (batch) => {
210
+ const chunks = [];
211
+ let currentEvents = [];
212
+ let currentPayload;
213
+ for (const event of batch) {
214
+ const candidateEvents = [...currentEvents, event];
215
+ const candidatePayload = codec.encode(candidateEvents);
216
+ if (payloadByteLength$1(candidatePayload) <= beaconMaxBytes) {
217
+ currentEvents = candidateEvents;
218
+ currentPayload = candidatePayload;
219
+ continue;
220
+ }
221
+ if (currentEvents.length > 0 && currentPayload) chunks.push({
222
+ events: currentEvents,
223
+ payload: currentPayload
224
+ });
225
+ const singlePayload = codec.encode([event]);
226
+ if (payloadByteLength$1(singlePayload) <= beaconMaxBytes) {
227
+ currentEvents = [event];
228
+ currentPayload = singlePayload;
229
+ } else {
230
+ currentEvents = [];
231
+ currentPayload = void 0;
232
+ reportDrop(event, "beacon-too-large");
233
+ }
234
+ }
235
+ if (currentEvents.length > 0 && currentPayload) chunks.push({
236
+ events: currentEvents,
237
+ payload: currentPayload
238
+ });
239
+ return chunks;
240
+ };
241
+ const sendBeaconBatch = (batch) => {
242
+ if (typeof navigator === "undefined" || !navigator.sendBeacon) return {
243
+ ok: false,
244
+ remaining: batch
245
+ };
246
+ const chunks = createBeaconChunks(batch);
247
+ for (let index = 0; index < chunks.length; index++) {
248
+ const chunk = chunks[index];
249
+ if (!chunk) continue;
250
+ if (!navigator.sendBeacon(options.url, payloadToBeaconBody(chunk.payload, codec.contentType))) return {
251
+ ok: false,
252
+ remaining: remainingEvents(chunks, index)
253
+ };
254
+ }
255
+ return {
256
+ ok: true,
257
+ remaining: []
258
+ };
259
+ };
260
+ const sendPayload = async (entry) => {
261
+ if (!fetchFn) throw new Error("fetch is not available for browserHttpTransport");
262
+ const response = await fetchFn(entry.url, {
263
+ method: entry.method,
264
+ headers: entry.headers,
265
+ body: payloadToBody(entry.body),
266
+ credentials: entry.credentials,
267
+ keepalive: entry.keepalive
268
+ });
269
+ if (!response.ok) throw new Error(`browserHttpTransport failed with status ${response.status}`);
270
+ };
271
+ const createOfflineEntry = (payload) => ({
272
+ id: `${Date.now().toString(36)}-${(offlineEntrySeq++).toString(36)}`,
273
+ url: options.url,
274
+ method: options.method ?? "POST",
275
+ headers: headers(),
276
+ body: payload,
277
+ credentials: options.credentials,
278
+ keepalive: options.keepalive ?? true,
279
+ createdAt: Date.now()
280
+ });
281
+ const enqueueOfflinePayload = async (payload) => {
282
+ if (!offlineQueue) return false;
283
+ await offlineQueue.enqueue(createOfflineEntry(payload));
284
+ incrementLoggerMetaCounter("transport.offline.queued");
285
+ return true;
286
+ };
287
+ const sendFetchBatch = async (batch) => {
288
+ if (batch.length === 0) return;
289
+ const payload = codec.encode(batch);
290
+ if (offlineQueue && typeof navigator !== "undefined" && navigator.onLine === false) {
291
+ await enqueueOfflinePayload(payload);
292
+ return;
293
+ }
294
+ try {
295
+ await sendPayload(createOfflineEntry(payload));
296
+ } catch (error) {
297
+ if (await enqueueOfflinePayload(payload)) return;
298
+ throw error;
299
+ }
300
+ };
301
+ const replayRetryDelay = (attempt) => {
302
+ const cap = Math.min(offlineReplayMaxDelayMs, offlineReplayBaseDelayMs * 2 ** attempt);
303
+ return cap <= 0 ? 0 : random() * cap;
304
+ };
305
+ const sendOfflineEntryWithRetry = async (entry) => {
306
+ for (let attempt = 0;; attempt++) try {
307
+ await sendPayload(entry);
308
+ incrementLoggerMetaCounter("transport.offline.replayed");
309
+ return;
310
+ } catch (error) {
311
+ if (attempt >= offlineReplayMaxRetries) {
312
+ incrementLoggerMetaCounter("transport.offline.replay.failed");
313
+ throw error;
314
+ }
315
+ incrementLoggerMetaCounter("transport.offline.retry");
316
+ await sleep(replayRetryDelay(attempt));
317
+ }
318
+ };
319
+ const replayOfflineQueue = async () => {
320
+ if (!offlineQueue || replayingOffline) return;
321
+ replayingOffline = true;
322
+ try {
323
+ await offlineQueue.replay(sendOfflineEntryWithRetry);
324
+ } finally {
325
+ replayingOffline = false;
326
+ }
327
+ };
328
+ const flush = async (preferBeacon = false) => {
329
+ if (flushing || queue.length === 0) return;
330
+ flushing = true;
331
+ clearTimer();
332
+ const batch = queue.splice(0, queue.length);
333
+ try {
334
+ let fetchBatch = batch;
335
+ if (preferBeacon) {
336
+ const beaconResult = sendBeaconBatch(batch);
337
+ if (beaconResult.ok || beaconResult.remaining.length === 0) return;
338
+ fetchBatch = beaconResult.remaining;
339
+ }
340
+ await sendFetchBatch(fetchBatch);
341
+ } finally {
342
+ flushing = false;
343
+ if (queue.length > 0) schedule();
344
+ }
345
+ };
346
+ const schedule = () => {
347
+ if (timer || flushIntervalMs <= 0) return;
348
+ timer = setTimeout(() => {
349
+ flush(false).catch((error) => reportInternalError(error, "flush"));
350
+ }, flushIntervalMs);
351
+ };
352
+ const onPageHide = () => {
353
+ flush(true).catch((error) => reportInternalError(error, "pagehide-flush"));
354
+ };
355
+ const onVisibilityChange = () => {
356
+ if (typeof document !== "undefined" && document.visibilityState === "hidden") flush(true).catch((error) => reportInternalError(error, "visibility-flush"));
357
+ };
358
+ const onOnline = () => {
359
+ replayOfflineQueue().catch((error) => reportInternalError(error, "replay"));
360
+ };
361
+ if (options.useBeaconOnPageHide ?? true) {
362
+ globalThis.addEventListener?.("pagehide", onPageHide);
363
+ globalThis.addEventListener?.("visibilitychange", onVisibilityChange);
364
+ }
365
+ if (offlineQueue) globalThis.addEventListener?.("online", onOnline);
366
+ return {
367
+ name: options.name ?? "browser-http",
368
+ minLevel: options.minLevel,
369
+ log(event, context) {
370
+ if (options.minLevel !== void 0 && event.level < toLevelValue(options.minLevel)) return;
371
+ lastContext = context;
372
+ if (queue.length >= maxQueueSize) {
373
+ if (dropPolicy === "drop-newest") {
374
+ reportDrop(event, "queue-full");
375
+ return;
376
+ }
377
+ const dropped = queue.shift();
378
+ if (dropped) reportDrop(dropped, "queue-full");
379
+ }
380
+ queue.push(event);
381
+ if (queue.length >= maxBatchSize) flush(false).catch((error) => reportInternalError(error, "flush"));
382
+ else schedule();
383
+ },
384
+ flush() {
385
+ return flush(false);
386
+ },
387
+ close() {
388
+ globalThis.removeEventListener?.("pagehide", onPageHide);
389
+ globalThis.removeEventListener?.("visibilitychange", onVisibilityChange);
390
+ globalThis.removeEventListener?.("online", onOnline);
391
+ return flush(true);
392
+ }
393
+ };
394
+ }
395
+ //#endregion
396
+ //#region src/service-worker-transport.ts
397
+ let sourceSequence = 0;
398
+ function createSourceId() {
399
+ sourceSequence += 1;
400
+ return `loggerjs-${Date.now().toString(36)}-${sourceSequence.toString(36)}`;
401
+ }
402
+ function defaultMapEvent(event, context) {
403
+ return {
404
+ type: "loggerjs.event",
405
+ source: context.source,
406
+ event
407
+ };
408
+ }
409
+ function defaultMapBatch(events, context) {
410
+ return {
411
+ type: "loggerjs.batch",
412
+ source: context.source,
413
+ events
414
+ };
415
+ }
416
+ function serviceWorkerContainer() {
417
+ return typeof navigator === "undefined" ? void 0 : navigator.serviceWorker;
418
+ }
419
+ function workerFromRegistration(registration) {
420
+ return registration.active ?? registration.waiting ?? registration.installing ?? void 0;
421
+ }
422
+ function browserServiceWorkerTransport(options = {}) {
423
+ const transportName = options.name ?? "browser-service-worker";
424
+ const target = options.target ?? "controller";
425
+ const source = options.source ?? createSourceId();
426
+ const maxQueueSize = options.maxQueueSize ?? 1e3;
427
+ const dropPolicy = options.dropPolicy ?? "drop-oldest";
428
+ const mapContext = {
429
+ source,
430
+ target
431
+ };
432
+ const mapEvent = options.mapEvent ?? defaultMapEvent;
433
+ const mapBatch = options.mapBatch ?? defaultMapBatch;
434
+ const queue = [];
435
+ let readyWorker;
436
+ let readyStarted = false;
437
+ let lastContext;
438
+ const reportError = (error, context, operation, droppedEvents = 0) => {
439
+ try {
440
+ options.onError?.(error, {
441
+ droppedEvents,
442
+ operation
443
+ });
444
+ } catch (onErrorError) {
445
+ context?.reportInternalError(onErrorError, {
446
+ operation: "on-error",
447
+ phase: "transport",
448
+ transport: transportName
449
+ });
450
+ }
451
+ context?.reportInternalError(error, {
452
+ operation,
453
+ phase: "transport",
454
+ transport: transportName
455
+ });
456
+ };
457
+ const reportDrop = (item, reason) => {
458
+ incrementLoggerMetaCounter("transport.dropped", item.events.length);
459
+ incrementLoggerMetaCounter(`transport.dropped.${reason}`, item.events.length);
460
+ for (const event of item.events) options.onDrop?.(event, reason);
461
+ };
462
+ const currentWorker = () => {
463
+ const container = options.serviceWorker ?? serviceWorkerContainer();
464
+ if (!container) return void 0;
465
+ if (target === "controller") return container.controller ?? void 0;
466
+ return readyWorker;
467
+ };
468
+ const post = (worker, item, context) => {
469
+ try {
470
+ worker.postMessage(item.message, options.transfer?.(item.message));
471
+ return true;
472
+ } catch (error) {
473
+ reportDrop(item, "post-message");
474
+ reportError(error, context, "post-message", item.events.length);
475
+ return false;
476
+ }
477
+ };
478
+ const drain = (context = lastContext) => {
479
+ const worker = currentWorker();
480
+ if (!worker || !context) return;
481
+ while (queue.length > 0) {
482
+ const item = queue.shift();
483
+ if (!item) return;
484
+ post(worker, item, context);
485
+ }
486
+ };
487
+ const startReady = (context) => {
488
+ if (target !== "ready" || readyStarted) return;
489
+ readyStarted = true;
490
+ const container = options.serviceWorker ?? serviceWorkerContainer();
491
+ if (!container?.ready) {
492
+ reportError(/* @__PURE__ */ new Error("serviceWorker.ready is not available"), context, "ready");
493
+ return;
494
+ }
495
+ container.ready.then((registration) => {
496
+ readyWorker = workerFromRegistration(registration);
497
+ if (!readyWorker) {
498
+ const pending = queue.splice(0);
499
+ for (const item of pending) reportDrop(item, "unavailable");
500
+ return;
501
+ }
502
+ drain(context);
503
+ }).catch((error) => {
504
+ const pending = queue.splice(0);
505
+ for (const item of pending) reportDrop(item, "ready");
506
+ reportError(error, context, "ready", pending.reduce((total, item) => total + item.events.length, 0));
507
+ });
508
+ };
509
+ const enqueue = (item) => {
510
+ if (queue.length >= maxQueueSize) {
511
+ if (dropPolicy === "drop-newest") {
512
+ reportDrop(item, "queue-full");
513
+ return false;
514
+ }
515
+ const dropped = queue.shift();
516
+ if (dropped) reportDrop(dropped, "queue-full");
517
+ }
518
+ queue.push(item);
519
+ return true;
520
+ };
521
+ const send = (item, context) => {
522
+ lastContext = context;
523
+ const worker = currentWorker();
524
+ if (worker) {
525
+ post(worker, item, context);
526
+ return;
527
+ }
528
+ if (target === "ready") {
529
+ if (enqueue(item)) startReady(context);
530
+ return;
531
+ }
532
+ reportDrop(item, "unavailable");
533
+ reportError(/* @__PURE__ */ new Error("service worker controller is not available"), context, "unavailable", item.events.length);
534
+ };
535
+ return {
536
+ name: transportName,
537
+ minLevel: options.minLevel,
538
+ queueSize() {
539
+ return queue.length;
540
+ },
541
+ log(event, context) {
542
+ send({
543
+ events: [event],
544
+ message: mapEvent(event, mapContext)
545
+ }, context);
546
+ },
547
+ logBatch(events, context) {
548
+ if (events.length === 0) return;
549
+ send({
550
+ events,
551
+ message: mapBatch(events, mapContext)
552
+ }, context);
553
+ },
554
+ flush() {
555
+ drain();
556
+ },
557
+ close() {
558
+ const pending = queue.splice(0);
559
+ for (const item of pending) reportDrop(item, "closed");
560
+ }
561
+ };
562
+ }
563
+ //#endregion
564
+ //#region src/websocket-transport.ts
565
+ const WS_CONNECTING = 0;
566
+ const WS_OPEN = 1;
567
+ function defaultWebSocketFactory(url, protocols) {
568
+ if (typeof WebSocket === "undefined") throw new Error("WebSocket is not available");
569
+ return new WebSocket(url, protocols);
570
+ }
571
+ function toSendPayload(payload) {
572
+ if (typeof payload === "string") return payload;
573
+ return Uint8Array.from(payload);
574
+ }
575
+ function browserWebSocketTransport(options) {
576
+ const transportName = options.name ?? "browser-websocket";
577
+ const codec = options.codec ?? safeJsonCodec();
578
+ const maxQueueSize = options.maxQueueSize ?? 1e3;
579
+ const dropPolicy = options.dropPolicy ?? "drop-oldest";
580
+ const webSocketFactory = options.webSocketFactory ?? defaultWebSocketFactory;
581
+ const queue = [];
582
+ let active;
583
+ let closed = false;
584
+ let lastContext;
585
+ const reportDrop = (item, reason) => {
586
+ incrementLoggerMetaCounter("transport.dropped", item.events.length);
587
+ incrementLoggerMetaCounter(`transport.dropped.${reason}`, item.events.length);
588
+ for (const event of item.events) options.onDrop?.(event, reason);
589
+ };
590
+ const reportError = (error, context, detail) => {
591
+ try {
592
+ options.onError?.(error, detail);
593
+ } catch (onErrorError) {
594
+ context?.reportInternalError(onErrorError, {
595
+ operation: "on-error",
596
+ phase: "transport",
597
+ transport: transportName
598
+ });
599
+ }
600
+ context?.reportInternalError(error, {
601
+ operation: detail.operation,
602
+ phase: "transport",
603
+ transport: transportName
604
+ });
605
+ };
606
+ const detachSocket = () => {
607
+ if (!active) return;
608
+ const current = active;
609
+ current.socket.removeEventListener("open", current.onOpen);
610
+ current.socket.removeEventListener("close", current.onClose);
611
+ current.socket.removeEventListener("error", current.onError);
612
+ active = void 0;
613
+ };
614
+ const drainQueue = (context = lastContext) => {
615
+ const socket = active?.socket;
616
+ if (!socket || socket.readyState !== WS_OPEN) return;
617
+ while (queue.length > 0) {
618
+ const item = queue.shift();
619
+ if (!item) return;
620
+ try {
621
+ socket.send(toSendPayload(item.payload));
622
+ } catch (error) {
623
+ reportDrop(item, "send");
624
+ reportError(error, context, {
625
+ droppedEvents: item.events.length,
626
+ operation: "send"
627
+ });
628
+ return;
629
+ }
630
+ }
631
+ };
632
+ const createSocket = (context, droppedEvents) => {
633
+ if (closed) return void 0;
634
+ if (active && (active.socket.readyState === WS_CONNECTING || active.socket.readyState === WS_OPEN)) return active.socket;
635
+ detachSocket();
636
+ try {
637
+ const socket = webSocketFactory(options.url, options.protocols);
638
+ const nextActive = {
639
+ socket,
640
+ onOpen() {
641
+ drainQueue(context);
642
+ },
643
+ onClose() {
644
+ if (active?.socket === socket) detachSocket();
645
+ },
646
+ onError(errorEvent) {
647
+ reportError(errorEvent, context, {
648
+ droppedEvents: 0,
649
+ operation: "socket-error"
650
+ });
651
+ }
652
+ };
653
+ active = nextActive;
654
+ socket.addEventListener("open", nextActive.onOpen);
655
+ socket.addEventListener("close", nextActive.onClose);
656
+ socket.addEventListener("error", nextActive.onError);
657
+ return socket;
658
+ } catch (error) {
659
+ reportError(error, context, {
660
+ droppedEvents,
661
+ operation: "create-socket"
662
+ });
663
+ return;
664
+ }
665
+ };
666
+ const enqueue = (item) => {
667
+ if (queue.length >= maxQueueSize) {
668
+ if (dropPolicy === "drop-newest") {
669
+ reportDrop(item, "queue-full");
670
+ return false;
671
+ }
672
+ const dropped = queue.shift();
673
+ if (dropped) reportDrop(dropped, "queue-full");
674
+ }
675
+ queue.push(item);
676
+ return true;
677
+ };
678
+ const sendOrQueue = (item, context) => {
679
+ lastContext = context;
680
+ if (closed) {
681
+ reportDrop(item, "closed");
682
+ return;
683
+ }
684
+ const socket = createSocket(context, item.events.length);
685
+ if (!socket) {
686
+ reportDrop(item, "create-socket");
687
+ return;
688
+ }
689
+ if (socket.readyState === WS_OPEN) {
690
+ try {
691
+ socket.send(toSendPayload(item.payload));
692
+ } catch (error) {
693
+ reportDrop(item, "send");
694
+ reportError(error, context, {
695
+ droppedEvents: item.events.length,
696
+ operation: "send"
697
+ });
698
+ }
699
+ return;
700
+ }
701
+ enqueue(item);
702
+ };
703
+ return {
704
+ name: transportName,
705
+ minLevel: options.minLevel,
706
+ queueSize() {
707
+ return queue.length;
708
+ },
709
+ log(event, context) {
710
+ sendOrQueue({
711
+ events: [event],
712
+ payload: codec.encode(event)
713
+ }, context);
714
+ },
715
+ logBatch(events, context) {
716
+ if (events.length === 0) return;
717
+ sendOrQueue({
718
+ events,
719
+ payload: codec.encode(events)
720
+ }, context);
721
+ },
722
+ flush() {
723
+ drainQueue();
724
+ },
725
+ async close() {
726
+ closed = true;
727
+ while (queue.length > 0) {
728
+ const item = queue.shift();
729
+ if (item) reportDrop(item, "closed");
730
+ }
731
+ const socket = active?.socket;
732
+ detachSocket();
733
+ if (!socket) return;
734
+ try {
735
+ socket.close(options.closeCode, options.closeReason);
736
+ } catch (error) {
737
+ reportError(error, lastContext, {
738
+ droppedEvents: 0,
739
+ operation: "close-socket"
740
+ });
741
+ }
742
+ }
743
+ };
744
+ }
745
+ //#endregion
746
+ //#region src/indexeddb-offline-queue.ts
747
+ const DEFAULT_DB_NAME$1 = "loggerjs";
748
+ const DEFAULT_STORE_NAME$1 = "browser-http-offline";
749
+ function normalizeMaxEntries(value) {
750
+ if (value === void 0 || !Number.isFinite(value)) return 1e3;
751
+ return Math.max(0, Math.floor(value));
752
+ }
753
+ function requestToPromise$1(request) {
754
+ return new Promise((resolve, reject) => {
755
+ request.addEventListener("success", () => resolve(request.result), { once: true });
756
+ request.addEventListener("error", () => reject(request.error ?? /* @__PURE__ */ new Error("IndexedDB request failed")), { once: true });
757
+ });
758
+ }
759
+ function compareEntries$1(a, b) {
760
+ if (a.createdAt !== b.createdAt) return a.createdAt - b.createdAt;
761
+ return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
762
+ }
763
+ function orderEntries$1(entries) {
764
+ const ordered = [];
765
+ for (const entry of entries) {
766
+ let index = 0;
767
+ while (index < ordered.length && compareEntries$1(ordered[index], entry) <= 0) index += 1;
768
+ ordered.splice(index, 0, entry);
769
+ }
770
+ return ordered;
771
+ }
772
+ function indexedDbBrowserHttpOfflineQueue(options = {}) {
773
+ const dbName = options.dbName ?? DEFAULT_DB_NAME$1;
774
+ const storeName = options.storeName ?? DEFAULT_STORE_NAME$1;
775
+ const maxEntries = normalizeMaxEntries(options.maxEntries);
776
+ const dropPolicy = options.dropPolicy ?? "drop-oldest";
777
+ const idb = options.indexedDB ?? globalThis.indexedDB;
778
+ let dbPromise;
779
+ const drop = (entry, reason) => {
780
+ incrementLoggerMetaCounter("transport.offline.dropped");
781
+ incrementLoggerMetaCounter(`transport.offline.dropped.${reason}`);
782
+ options.onDrop?.(entry, reason);
783
+ };
784
+ const openDb = () => {
785
+ if (!idb) throw new Error("IndexedDB is not available for loggerjs offline queue");
786
+ dbPromise ??= new Promise((resolve, reject) => {
787
+ const request = idb.open(dbName, 1);
788
+ request.addEventListener("upgradeneeded", () => {
789
+ const db = request.result;
790
+ if (!db.objectStoreNames.contains(storeName)) db.createObjectStore(storeName, { keyPath: "id" });
791
+ });
792
+ request.addEventListener("success", () => resolve(request.result), { once: true });
793
+ request.addEventListener("error", () => reject(request.error ?? /* @__PURE__ */ new Error("IndexedDB open failed")), { once: true });
794
+ });
795
+ return dbPromise;
796
+ };
797
+ const withStore = async (mode, run) => {
798
+ return run((await openDb()).transaction(storeName, mode).objectStore(storeName));
799
+ };
800
+ const getEntries = async () => orderEntries$1(await withStore("readonly", (store) => requestToPromise$1(store.getAll())));
801
+ const putEntry = (entry) => withStore("readwrite", (store) => requestToPromise$1(store.put(entry)));
802
+ const deleteEntry = (id) => withStore("readwrite", (store) => requestToPromise$1(store.delete(id)));
803
+ return {
804
+ async enqueue(entry) {
805
+ if (maxEntries === 0) {
806
+ drop(entry, "queue-full");
807
+ return;
808
+ }
809
+ const entries = await getEntries();
810
+ if (entries.length >= maxEntries) {
811
+ if (dropPolicy === "drop-newest") {
812
+ drop(entry, "queue-full");
813
+ return;
814
+ }
815
+ const dropCount = entries.length - maxEntries + 1;
816
+ for (let index = 0; index < dropCount; index += 1) {
817
+ const dropped = entries[index];
818
+ if (!dropped) continue;
819
+ await deleteEntry(dropped.id);
820
+ drop(dropped, "queue-full");
821
+ }
822
+ }
823
+ await putEntry(entry);
824
+ },
825
+ async replay(send) {
826
+ const entries = await getEntries();
827
+ for (const entry of entries) {
828
+ await send(entry);
829
+ await deleteEntry(entry.id);
830
+ }
831
+ },
832
+ async size() {
833
+ return (await getEntries()).length;
834
+ },
835
+ clear() {
836
+ return withStore("readwrite", (store) => requestToPromise$1(store.clear()));
837
+ },
838
+ close() {
839
+ const promise = dbPromise;
840
+ dbPromise = void 0;
841
+ promise?.then((db) => db.close());
842
+ }
843
+ };
844
+ }
845
+ //#endregion
846
+ //#region src/indexeddb-transport.ts
847
+ const DEFAULT_DB_NAME = "loggerjs";
848
+ const DEFAULT_STORE_NAME = "logs";
849
+ const DB_VERSION = 2;
850
+ const ORDER_INDEX_NAME = "createdAtSeq";
851
+ function normalizePositiveInteger(value, fallback) {
852
+ if (value === void 0 || !Number.isFinite(value)) return fallback;
853
+ return Math.max(0, Math.floor(value));
854
+ }
855
+ function nowMs$1() {
856
+ return typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
857
+ }
858
+ function incrementRecord(record, key, amount = 1) {
859
+ record[key] = (record[key] ?? 0) + amount;
860
+ }
861
+ function requestToPromise(request) {
862
+ return new Promise((resolve, reject) => {
863
+ request.addEventListener("success", () => resolve(request.result), { once: true });
864
+ request.addEventListener("error", () => reject(request.error ?? /* @__PURE__ */ new Error("IndexedDB request failed")), { once: true });
865
+ });
866
+ }
867
+ function transactionToPromise(transaction) {
868
+ return new Promise((resolve, reject) => {
869
+ transaction.addEventListener("complete", () => resolve(), { once: true });
870
+ transaction.addEventListener("abort", () => reject(transaction.error ?? /* @__PURE__ */ new Error("IndexedDB transaction aborted")), { once: true });
871
+ transaction.addEventListener("error", () => reject(transaction.error ?? /* @__PURE__ */ new Error("IndexedDB transaction failed")), { once: true });
872
+ });
873
+ }
874
+ function payloadByteLength(payload) {
875
+ if (typeof payload !== "string") return payload.byteLength;
876
+ if (typeof TextEncoder !== "undefined") return new TextEncoder().encode(payload).byteLength;
877
+ return new Blob([payload]).size;
878
+ }
879
+ function compareEntries(a, b) {
880
+ if (a.createdAt !== b.createdAt) return a.createdAt - b.createdAt;
881
+ if (a.seq !== b.seq) return a.seq - b.seq;
882
+ return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
883
+ }
884
+ function orderEntries(entries, order = "asc") {
885
+ const sorted = [...entries].sort(compareEntries);
886
+ if (order === "asc") return sorted;
887
+ const reversed = [];
888
+ for (let index = sorted.length - 1; index >= 0; index -= 1) reversed.push(sorted[index]);
889
+ return reversed;
890
+ }
891
+ function indexExists(store, name) {
892
+ return store.indexNames.contains(name);
893
+ }
894
+ function ensureIndex(store, name, keyPath) {
895
+ if (!indexExists(store, name)) store.createIndex(name, keyPath);
896
+ }
897
+ function getStoreIndex(store, name) {
898
+ const maybeStore = store;
899
+ if (typeof maybeStore.index !== "function" || !indexExists(store, name)) return void 0;
900
+ return maybeStore.index(name);
901
+ }
902
+ function getStorageBucketManager() {
903
+ return globalThis.navigator?.storageBuckets;
904
+ }
905
+ function storageBucketOpenOptions(options) {
906
+ if (options.storageBucketDurability === void 0 && options.storageBucketPersisted === void 0) return;
907
+ return {
908
+ durability: options.storageBucketDurability,
909
+ persisted: options.storageBucketPersisted
910
+ };
911
+ }
912
+ function decodeEntry(entry, codec) {
913
+ if (!codec.decode) throw new Error("indexedDbTransport query requires a codec with decode");
914
+ const decoded = codec.decode(entry.payload);
915
+ return Array.isArray(decoded) ? decoded[0] : decoded;
916
+ }
917
+ function shouldKeepEntry(entry, options) {
918
+ if (options.from !== void 0 && entry.createdAt < options.from) return false;
919
+ if (options.to !== void 0 && entry.createdAt > options.to) return false;
920
+ if (options.minLevel !== void 0 && entry.level < toLevelValue(options.minLevel)) return false;
921
+ if (options.logger !== void 0 && entry.logger !== options.logger) return false;
922
+ if (options.type !== void 0 && entry.type !== options.type) return false;
923
+ return true;
924
+ }
925
+ function orderRangeForQuery(options) {
926
+ if (typeof IDBKeyRange === "undefined") return void 0;
927
+ const lower = options.from === void 0 ? void 0 : [
928
+ options.from,
929
+ Number.MIN_SAFE_INTEGER,
930
+ ""
931
+ ];
932
+ const upper = options.to === void 0 ? void 0 : [
933
+ options.to,
934
+ Number.MAX_SAFE_INTEGER,
935
+ "￿"
936
+ ];
937
+ if (lower && upper) return IDBKeyRange.bound(lower, upper);
938
+ if (lower) return IDBKeyRange.lowerBound(lower);
939
+ if (upper) return IDBKeyRange.upperBound(upper);
940
+ }
941
+ function iterateCursor(request, visit) {
942
+ return new Promise((resolve, reject) => {
943
+ request.addEventListener("success", () => {
944
+ const cursor = request.result;
945
+ if (!cursor) {
946
+ resolve();
947
+ return;
948
+ }
949
+ try {
950
+ if (visit(cursor) === false) {
951
+ resolve();
952
+ return;
953
+ }
954
+ cursor.continue();
955
+ } catch (error) {
956
+ reject(error);
957
+ }
958
+ }, false);
959
+ request.addEventListener("error", () => reject(request.error ?? /* @__PURE__ */ new Error("IndexedDB cursor failed")), { once: true });
960
+ });
961
+ }
962
+ function indexedDbTransport(options = {}) {
963
+ const dbName = options.dbName ?? DEFAULT_DB_NAME;
964
+ const storeName = options.storeName ?? DEFAULT_STORE_NAME;
965
+ const codec = options.codec ?? safeJsonCodec();
966
+ const batchSize = normalizePositiveInteger(options.batchSize, 100);
967
+ const flushIntervalMs = normalizePositiveInteger(options.flushIntervalMs, 1e3);
968
+ const maxBufferSize = normalizePositiveInteger(options.maxBufferSize, 1e3);
969
+ const maxEntries = normalizePositiveInteger(options.maxEntries, 5e4);
970
+ const maxBytes = normalizePositiveInteger(options.maxBytes, 50 * 1024 * 1024);
971
+ const dropPolicy = options.dropPolicy ?? "drop-oldest";
972
+ const flushOnPageHide = options.flushOnPageHide ?? true;
973
+ const durability = options.durability ?? "default";
974
+ const rootIndexedDB = options.indexedDB ?? globalThis.indexedDB;
975
+ const transactionOptions = durability === "default" ? void 0 : { durability };
976
+ const buffer = [];
977
+ let dbPromise;
978
+ let indexedDBFactoryPromise;
979
+ let timer;
980
+ let flushPromise;
981
+ let lastContext;
982
+ let closed = false;
983
+ const statsState = {
984
+ bufferDepth: 0,
985
+ maxBufferDepth: 0,
986
+ pendingFlush: false,
987
+ enqueued: 0,
988
+ persisted: 0,
989
+ dropped: 0,
990
+ droppedByReason: {},
991
+ persistedDropped: 0,
992
+ persistedDroppedByReason: {},
993
+ flushes: 0,
994
+ flushErrors: 0,
995
+ lastFlushBatchSize: 0,
996
+ lastFlushDurationMs: 0,
997
+ prunes: 0,
998
+ pruneFallbacks: 0,
999
+ lastPruneDurationMs: 0,
1000
+ queries: 0,
1001
+ queryFallbacks: 0,
1002
+ lastQueryDurationMs: 0,
1003
+ databaseOpenCount: 0,
1004
+ storageBucketFallbacks: 0,
1005
+ transactionOptionFallbacks: 0,
1006
+ errors: 0,
1007
+ errorsByOperation: {}
1008
+ };
1009
+ const snapshotStats = () => ({
1010
+ ...statsState,
1011
+ bufferDepth: buffer.length,
1012
+ droppedByReason: { ...statsState.droppedByReason },
1013
+ errorsByOperation: { ...statsState.errorsByOperation },
1014
+ pendingFlush: flushPromise !== void 0,
1015
+ persistedDroppedByReason: { ...statsState.persistedDroppedByReason }
1016
+ });
1017
+ const reportInternalError = (error, operation) => {
1018
+ statsState.errors += 1;
1019
+ incrementRecord(statsState.errorsByOperation, operation);
1020
+ lastContext?.reportInternalError(error, {
1021
+ operation,
1022
+ phase: "transport",
1023
+ transport: options.name ?? "indexeddb"
1024
+ });
1025
+ };
1026
+ const dropEvent = (event, reason) => {
1027
+ statsState.dropped += 1;
1028
+ incrementRecord(statsState.droppedByReason, reason);
1029
+ incrementLoggerMetaCounter("transport.indexeddb.dropped");
1030
+ incrementLoggerMetaCounter(`transport.indexeddb.dropped.${reason}`);
1031
+ options.onDrop?.(event, reason);
1032
+ };
1033
+ const dropPersistedEntry = (entry, reason) => {
1034
+ statsState.persistedDropped += 1;
1035
+ incrementRecord(statsState.persistedDroppedByReason, reason);
1036
+ incrementLoggerMetaCounter("transport.indexeddb.persisted.dropped");
1037
+ incrementLoggerMetaCounter(`transport.indexeddb.persisted.dropped.${reason}`);
1038
+ options.onPersistedDrop?.(entry, reason);
1039
+ };
1040
+ const resolveIndexedDBFactory = async () => {
1041
+ if (options.indexedDB || !options.storageBucketName) return rootIndexedDB;
1042
+ indexedDBFactoryPromise ??= (async () => {
1043
+ const manager = getStorageBucketManager();
1044
+ if (!manager) {
1045
+ statsState.storageBucketFallbacks += 1;
1046
+ return rootIndexedDB;
1047
+ }
1048
+ try {
1049
+ const bucket = await manager.open(options.storageBucketName, storageBucketOpenOptions(options));
1050
+ if (bucket.indexedDB) return bucket.indexedDB;
1051
+ statsState.storageBucketFallbacks += 1;
1052
+ return rootIndexedDB;
1053
+ } catch (error) {
1054
+ statsState.storageBucketFallbacks += 1;
1055
+ reportInternalError(error, "storage-bucket-open");
1056
+ return rootIndexedDB;
1057
+ }
1058
+ })();
1059
+ return indexedDBFactoryPromise;
1060
+ };
1061
+ const openDb = () => {
1062
+ dbPromise ??= resolveIndexedDBFactory().then((idb) => {
1063
+ if (!idb) throw new Error("IndexedDB is not available for indexedDbTransport");
1064
+ return new Promise((resolve, reject) => {
1065
+ const request = idb.open(dbName, DB_VERSION);
1066
+ request.addEventListener("upgradeneeded", () => {
1067
+ const db = request.result;
1068
+ const store = db.objectStoreNames.contains(storeName) ? request.transaction?.objectStore(storeName) : db.createObjectStore(storeName, { keyPath: "id" });
1069
+ if (!store) return;
1070
+ ensureIndex(store, "createdAt", "createdAt");
1071
+ ensureIndex(store, "level", "level");
1072
+ ensureIndex(store, "logger", "logger");
1073
+ ensureIndex(store, "type", "type");
1074
+ ensureIndex(store, ORDER_INDEX_NAME, [
1075
+ "createdAt",
1076
+ "seq",
1077
+ "id"
1078
+ ]);
1079
+ });
1080
+ request.addEventListener("success", () => {
1081
+ statsState.databaseOpenCount += 1;
1082
+ resolve(request.result);
1083
+ }, { once: true });
1084
+ request.addEventListener("error", () => reject(request.error ?? /* @__PURE__ */ new Error("IndexedDB open failed")), { once: true });
1085
+ });
1086
+ });
1087
+ return dbPromise;
1088
+ };
1089
+ const createTransaction = (db, mode) => {
1090
+ if (mode !== "readwrite" || !transactionOptions) return db.transaction(storeName, mode);
1091
+ try {
1092
+ return db.transaction(storeName, mode, transactionOptions);
1093
+ } catch {
1094
+ statsState.transactionOptionFallbacks += 1;
1095
+ return db.transaction(storeName, mode);
1096
+ }
1097
+ };
1098
+ const withStore = async (mode, run) => {
1099
+ const tx = createTransaction(await openDb(), mode);
1100
+ const settled = transactionToPromise(tx);
1101
+ try {
1102
+ const result = await run(tx.objectStore(storeName), tx);
1103
+ await settled;
1104
+ return result;
1105
+ } catch (error) {
1106
+ if (tx.error === null) try {
1107
+ tx.abort();
1108
+ } catch {}
1109
+ throw error;
1110
+ }
1111
+ };
1112
+ const getEntries = async () => orderEntries(await withStore("readonly", (store) => requestToPromise(store.getAll())));
1113
+ const countEntries = async () => withStore("readonly", (store) => {
1114
+ const maybeStore = store;
1115
+ if (typeof maybeStore.count !== "function") return void 0;
1116
+ return requestToPromise(maybeStore.count.call(store));
1117
+ });
1118
+ const deleteEntries = async (entries, reason) => {
1119
+ if (entries.length === 0) return;
1120
+ await withStore("readwrite", async (store) => {
1121
+ for (const entry of entries) {
1122
+ await requestToPromise(store.delete(entry.id));
1123
+ dropPersistedEntry(entry, reason);
1124
+ }
1125
+ });
1126
+ };
1127
+ const deleteByCursor = async (reason, openCursor, shouldDelete) => withStore("readwrite", async (store) => {
1128
+ const request = openCursor(store);
1129
+ if (!request) return void 0;
1130
+ let deleted = 0;
1131
+ await iterateCursor(request, (cursor) => {
1132
+ const entry = cursor.value;
1133
+ if (!shouldDelete(entry, deleted)) return false;
1134
+ cursor.delete();
1135
+ deleted += 1;
1136
+ dropPersistedEntry(entry, reason);
1137
+ return true;
1138
+ });
1139
+ return deleted;
1140
+ });
1141
+ const deleteExpiredByCursor = async (cutoff) => {
1142
+ if (typeof IDBKeyRange === "undefined") return void 0;
1143
+ const range = IDBKeyRange.upperBound(cutoff, true);
1144
+ return deleteByCursor("ttl", (store) => getStoreIndex(store, "createdAt")?.openCursor(range), () => true);
1145
+ };
1146
+ const deleteOldestByCursor = async (reason, count) => {
1147
+ if (count <= 0) return 0;
1148
+ return deleteByCursor(reason, (store) => getStoreIndex(store, ORDER_INDEX_NAME)?.openCursor(), (_entry, deleted) => deleted < count);
1149
+ };
1150
+ const deleteOldestBytesByCursor = async (bytesToFree) => {
1151
+ if (bytesToFree <= 0) return 0;
1152
+ let freed = 0;
1153
+ return deleteByCursor("max-bytes", (store) => getStoreIndex(store, ORDER_INDEX_NAME)?.openCursor(), (entry) => {
1154
+ if (freed >= bytesToFree) return false;
1155
+ freed += entry.byteLength;
1156
+ return true;
1157
+ });
1158
+ };
1159
+ const sumBytesByCursor = async () => withStore("readonly", async (store) => {
1160
+ const index = getStoreIndex(store, ORDER_INDEX_NAME);
1161
+ if (!index) return void 0;
1162
+ let total = 0;
1163
+ await iterateCursor(index.openCursor(), (cursor) => {
1164
+ total += cursor.value.byteLength;
1165
+ });
1166
+ return total;
1167
+ });
1168
+ const queryEntriesByCursor = async (queryOptions) => withStore("readonly", async (store) => {
1169
+ const index = getStoreIndex(store, ORDER_INDEX_NAME);
1170
+ if (!index) return void 0;
1171
+ const entries = [];
1172
+ const limit = queryOptions.limit;
1173
+ if (limit !== void 0 && limit <= 0) return entries;
1174
+ await iterateCursor(index.openCursor(orderRangeForQuery(queryOptions), queryOptions.order === "desc" ? "prev" : "next"), (cursor) => {
1175
+ const entry = cursor.value;
1176
+ if (!shouldKeepEntry(entry, queryOptions)) return true;
1177
+ entries.push(entry);
1178
+ return limit === void 0 || entries.length < limit;
1179
+ });
1180
+ return entries;
1181
+ });
1182
+ const pruneWithEntries = async () => {
1183
+ const now = Date.now();
1184
+ let entries = await getEntries();
1185
+ const expired = options.ttlMs === void 0 ? [] : entries.filter((entry) => now - entry.createdAt > options.ttlMs);
1186
+ if (expired.length > 0) {
1187
+ await deleteEntries(expired, "ttl");
1188
+ const expiredIds = new Set(expired.map((entry) => entry.id));
1189
+ entries = entries.filter((entry) => !expiredIds.has(entry.id));
1190
+ }
1191
+ if (maxEntries >= 0 && entries.length > maxEntries) {
1192
+ await deleteEntries(entries.slice(0, entries.length - maxEntries), "max-entries");
1193
+ entries = entries.slice(entries.length - maxEntries);
1194
+ }
1195
+ if (maxBytes >= 0) {
1196
+ let total = entries.reduce((sum, entry) => sum + entry.byteLength, 0);
1197
+ const dropped = [];
1198
+ for (const entry of entries) {
1199
+ if (total <= maxBytes) break;
1200
+ total -= entry.byteLength;
1201
+ dropped.push(entry);
1202
+ }
1203
+ await deleteEntries(dropped, "max-bytes");
1204
+ }
1205
+ };
1206
+ const pruneWithCursors = async () => {
1207
+ if (options.ttlMs !== void 0) {
1208
+ if (await deleteExpiredByCursor(Date.now() - options.ttlMs) === void 0) return false;
1209
+ }
1210
+ if (maxEntries >= 0) {
1211
+ const persistedCount = await countEntries();
1212
+ if (persistedCount === void 0) return false;
1213
+ if (persistedCount > maxEntries) {
1214
+ if (await deleteOldestByCursor("max-entries", persistedCount - maxEntries) === void 0) return false;
1215
+ }
1216
+ }
1217
+ if (maxBytes >= 0) {
1218
+ const totalBytes = await sumBytesByCursor();
1219
+ if (totalBytes === void 0) return false;
1220
+ if (totalBytes > maxBytes) {
1221
+ if (await deleteOldestBytesByCursor(totalBytes - maxBytes) === void 0) return false;
1222
+ }
1223
+ }
1224
+ return true;
1225
+ };
1226
+ const prune = async () => {
1227
+ const startedAt = nowMs$1();
1228
+ try {
1229
+ if (await pruneWithCursors()) return;
1230
+ statsState.pruneFallbacks += 1;
1231
+ await pruneWithEntries();
1232
+ } finally {
1233
+ statsState.prunes += 1;
1234
+ statsState.lastPruneDurationMs = nowMs$1() - startedAt;
1235
+ }
1236
+ };
1237
+ const eventToEntry = (event) => {
1238
+ const payload = codec.encode(event);
1239
+ return {
1240
+ id: event.id,
1241
+ seq: event.seq,
1242
+ createdAt: event.time,
1243
+ level: event.level,
1244
+ levelName: event.levelName,
1245
+ logger: event.logger,
1246
+ type: event.type,
1247
+ byteLength: payloadByteLength(payload),
1248
+ payload
1249
+ };
1250
+ };
1251
+ const writeBatch = async (events) => {
1252
+ if (events.length === 0) return;
1253
+ const startedAt = nowMs$1();
1254
+ statsState.lastFlushBatchSize = events.length;
1255
+ const entries = events.map(eventToEntry);
1256
+ try {
1257
+ await withStore("readwrite", (store) => {
1258
+ for (const entry of entries) store.put(entry);
1259
+ });
1260
+ statsState.persisted += entries.length;
1261
+ incrementLoggerMetaCounter("transport.indexeddb.persisted", entries.length);
1262
+ await prune();
1263
+ statsState.flushes += 1;
1264
+ } catch (error) {
1265
+ statsState.flushErrors += 1;
1266
+ throw error;
1267
+ } finally {
1268
+ statsState.lastFlushDurationMs = nowMs$1() - startedAt;
1269
+ }
1270
+ };
1271
+ const clearTimer = () => {
1272
+ if (timer) clearTimeout(timer);
1273
+ timer = void 0;
1274
+ };
1275
+ const flushPending = async () => {
1276
+ if (flushPromise) return flushPromise;
1277
+ clearTimer();
1278
+ flushPromise = writeBatch(buffer.splice(0, buffer.length)).finally(() => {
1279
+ flushPromise = void 0;
1280
+ if (buffer.length > 0 && !closed) schedule();
1281
+ });
1282
+ return flushPromise;
1283
+ };
1284
+ const schedule = () => {
1285
+ if (timer || flushIntervalMs <= 0 || closed) return;
1286
+ timer = setTimeout(() => {
1287
+ flushPending().catch((error) => reportInternalError(error, "flush"));
1288
+ }, flushIntervalMs);
1289
+ };
1290
+ const onPageHide = () => {
1291
+ flushPending().catch((error) => reportInternalError(error, "pagehide-flush"));
1292
+ };
1293
+ const onVisibilityChange = () => {
1294
+ if (typeof document !== "undefined" && document.visibilityState === "hidden") flushPending().catch((error) => reportInternalError(error, "visibility-flush"));
1295
+ };
1296
+ if (flushOnPageHide) {
1297
+ globalThis.addEventListener?.("pagehide", onPageHide);
1298
+ globalThis.addEventListener?.("visibilitychange", onVisibilityChange);
1299
+ }
1300
+ return {
1301
+ name: options.name ?? "indexeddb",
1302
+ minLevel: options.minLevel,
1303
+ log(event, context) {
1304
+ if (closed) return;
1305
+ if (options.minLevel !== void 0 && event.level < toLevelValue(options.minLevel)) return;
1306
+ lastContext = context;
1307
+ if (maxBufferSize === 0 || buffer.length >= maxBufferSize) {
1308
+ if (dropPolicy === "drop-newest") {
1309
+ dropEvent(event, "buffer-full");
1310
+ return;
1311
+ }
1312
+ const dropped = buffer.shift();
1313
+ if (dropped) dropEvent(dropped, "buffer-full");
1314
+ }
1315
+ buffer.push(event);
1316
+ statsState.enqueued += 1;
1317
+ statsState.maxBufferDepth = Math.max(statsState.maxBufferDepth, buffer.length);
1318
+ if (buffer.length >= batchSize || flushIntervalMs === 0) flushPending().catch((error) => reportInternalError(error, "flush"));
1319
+ else schedule();
1320
+ },
1321
+ flush() {
1322
+ return flushPending();
1323
+ },
1324
+ async count() {
1325
+ await flushPending();
1326
+ return await countEntries() ?? (await getEntries()).length;
1327
+ },
1328
+ async clear() {
1329
+ buffer.length = 0;
1330
+ clearTimer();
1331
+ await withStore("readwrite", (store) => requestToPromise(store.clear()));
1332
+ },
1333
+ async *query(queryOptions = {}) {
1334
+ await flushPending();
1335
+ const startedAt = nowMs$1();
1336
+ const cursorEntries = await queryEntriesByCursor(queryOptions);
1337
+ if (cursorEntries === void 0) statsState.queryFallbacks += 1;
1338
+ const entries = cursorEntries ?? orderEntries(await getEntries(), queryOptions.order ?? "asc").filter((entry) => shouldKeepEntry(entry, queryOptions)).slice(0, queryOptions.limit);
1339
+ statsState.queries += 1;
1340
+ statsState.lastQueryDurationMs = nowMs$1() - startedAt;
1341
+ for (const entry of entries) {
1342
+ const event = decodeEntry(entry, codec);
1343
+ if (event) yield event;
1344
+ }
1345
+ },
1346
+ stats() {
1347
+ return snapshotStats();
1348
+ },
1349
+ close() {
1350
+ closed = true;
1351
+ clearTimer();
1352
+ globalThis.removeEventListener?.("pagehide", onPageHide);
1353
+ globalThis.removeEventListener?.("visibilitychange", onVisibilityChange);
1354
+ const flushed = flushPending();
1355
+ const promise = dbPromise;
1356
+ dbPromise = void 0;
1357
+ return flushed.finally(() => {
1358
+ promise?.then((db) => db.close());
1359
+ });
1360
+ }
1361
+ };
1362
+ }
1363
+ //#endregion
1364
+ //#region src/zip-export.ts
1365
+ const ZIP_UTF8_FLAG = 2048;
1366
+ const ZIP_STORE_METHOD = 0;
1367
+ const ZIP_VERSION = 20;
1368
+ const ZIP_MAX_UINT16 = 65535;
1369
+ const ZIP_MAX_UINT32 = 4294967295;
1370
+ const LOCAL_FILE_HEADER_SIGNATURE = 67324752;
1371
+ const CENTRAL_DIRECTORY_SIGNATURE = 33639248;
1372
+ const END_OF_CENTRAL_DIRECTORY_SIGNATURE = 101010256;
1373
+ let crc32Table;
1374
+ function getCrc32Table() {
1375
+ if (crc32Table) return crc32Table;
1376
+ const table = new Uint32Array(256);
1377
+ for (let index = 0; index < table.length; index += 1) {
1378
+ let crc = index;
1379
+ for (let bit = 0; bit < 8; bit += 1) crc = crc & 1 ? 3988292384 ^ crc >>> 1 : crc >>> 1;
1380
+ table[index] = crc >>> 0;
1381
+ }
1382
+ crc32Table = table;
1383
+ return table;
1384
+ }
1385
+ function crc32(bytes) {
1386
+ const table = getCrc32Table();
1387
+ let crc = 4294967295;
1388
+ for (const byte of bytes) crc = crc >>> 8 ^ (table[(crc ^ byte) & 255] ?? 0);
1389
+ return (crc ^ 4294967295) >>> 0;
1390
+ }
1391
+ function encodeText(value) {
1392
+ return new TextEncoder().encode(value);
1393
+ }
1394
+ function normalizeFileContent(content) {
1395
+ if (typeof content === "string") return encodeText(content);
1396
+ if (content instanceof Uint8Array) return content;
1397
+ return new Uint8Array(content);
1398
+ }
1399
+ function normalizeZipFileName(name) {
1400
+ const normalized = name.replace(/\\/g, "/").split("/").filter((part) => part.length > 0 && part !== "." && part !== "..").join("/");
1401
+ if (!normalized) throw new Error("ZIP file name cannot be empty");
1402
+ return normalized;
1403
+ }
1404
+ function toDosDateTime(timestamp) {
1405
+ const date = new Date(Number.isFinite(timestamp) ? timestamp : Date.now());
1406
+ const year = Math.min(2107, Math.max(1980, date.getFullYear()));
1407
+ return {
1408
+ time: date.getHours() << 11 | date.getMinutes() << 5 | Math.floor(date.getSeconds() / 2),
1409
+ date: year - 1980 << 9 | date.getMonth() + 1 << 5 | date.getDate()
1410
+ };
1411
+ }
1412
+ function assertZipUint16(value, label) {
1413
+ if (!Number.isSafeInteger(value) || value < 0 || value > ZIP_MAX_UINT16) throw new Error(`${label} exceeds ZIP32 limit`);
1414
+ }
1415
+ function assertZipUint32(value, label) {
1416
+ if (!Number.isSafeInteger(value) || value < 0 || value > ZIP_MAX_UINT32) throw new Error(`${label} exceeds ZIP32 limit`);
1417
+ }
1418
+ function createLocalFileHeader(file) {
1419
+ const header = new Uint8Array(30);
1420
+ const view = new DataView(header.buffer);
1421
+ view.setUint32(0, LOCAL_FILE_HEADER_SIGNATURE, true);
1422
+ view.setUint16(4, ZIP_VERSION, true);
1423
+ view.setUint16(6, ZIP_UTF8_FLAG, true);
1424
+ view.setUint16(8, ZIP_STORE_METHOD, true);
1425
+ view.setUint16(10, file.modifiedTime, true);
1426
+ view.setUint16(12, file.modifiedDate, true);
1427
+ view.setUint32(14, file.crc32, true);
1428
+ view.setUint32(18, file.contentBytes.byteLength, true);
1429
+ view.setUint32(22, file.contentBytes.byteLength, true);
1430
+ view.setUint16(26, file.nameBytes.byteLength, true);
1431
+ view.setUint16(28, 0, true);
1432
+ return header;
1433
+ }
1434
+ function createCentralDirectoryHeader(file) {
1435
+ const header = new Uint8Array(46);
1436
+ const view = new DataView(header.buffer);
1437
+ view.setUint32(0, CENTRAL_DIRECTORY_SIGNATURE, true);
1438
+ view.setUint16(4, ZIP_VERSION, true);
1439
+ view.setUint16(6, ZIP_VERSION, true);
1440
+ view.setUint16(8, ZIP_UTF8_FLAG, true);
1441
+ view.setUint16(10, ZIP_STORE_METHOD, true);
1442
+ view.setUint16(12, file.modifiedTime, true);
1443
+ view.setUint16(14, file.modifiedDate, true);
1444
+ view.setUint32(16, file.crc32, true);
1445
+ view.setUint32(20, file.contentBytes.byteLength, true);
1446
+ view.setUint32(24, file.contentBytes.byteLength, true);
1447
+ view.setUint16(28, file.nameBytes.byteLength, true);
1448
+ view.setUint16(30, 0, true);
1449
+ view.setUint16(32, 0, true);
1450
+ view.setUint16(34, 0, true);
1451
+ view.setUint16(36, 0, true);
1452
+ view.setUint32(38, 0, true);
1453
+ view.setUint32(42, file.localHeaderOffset, true);
1454
+ return header;
1455
+ }
1456
+ function createEndOfCentralDirectory(fileCount, centralDirectorySize, centralDirectoryOffset) {
1457
+ assertZipUint16(fileCount, "ZIP file count");
1458
+ assertZipUint32(centralDirectorySize, "ZIP central directory size");
1459
+ assertZipUint32(centralDirectoryOffset, "ZIP central directory offset");
1460
+ const footer = new Uint8Array(22);
1461
+ const view = new DataView(footer.buffer);
1462
+ view.setUint32(0, END_OF_CENTRAL_DIRECTORY_SIGNATURE, true);
1463
+ view.setUint16(4, 0, true);
1464
+ view.setUint16(6, 0, true);
1465
+ view.setUint16(8, fileCount, true);
1466
+ view.setUint16(10, fileCount, true);
1467
+ view.setUint32(12, centralDirectorySize, true);
1468
+ view.setUint32(16, centralDirectoryOffset, true);
1469
+ view.setUint16(20, 0, true);
1470
+ return footer;
1471
+ }
1472
+ function createNormalizedZipFiles(files, options) {
1473
+ const names = /* @__PURE__ */ new Set();
1474
+ let offset = 0;
1475
+ return files.map((file) => {
1476
+ const name = normalizeZipFileName(file.name);
1477
+ if (names.has(name)) throw new Error(`Duplicate ZIP file name: ${name}`);
1478
+ names.add(name);
1479
+ const nameBytes = encodeText(name);
1480
+ const contentBytes = normalizeFileContent(file.content);
1481
+ assertZipUint16(nameBytes.byteLength, `ZIP file name ${name}`);
1482
+ assertZipUint32(contentBytes.byteLength, `ZIP file ${name}`);
1483
+ assertZipUint32(offset, "ZIP local header offset");
1484
+ const modified = toDosDateTime(file.lastModified ?? options.lastModified ?? Date.now());
1485
+ const normalized = {
1486
+ name,
1487
+ nameBytes,
1488
+ contentBytes,
1489
+ crc32: crc32(contentBytes),
1490
+ modifiedTime: modified.time,
1491
+ modifiedDate: modified.date,
1492
+ localHeaderOffset: offset
1493
+ };
1494
+ offset += 30 + nameBytes.byteLength + contentBytes.byteLength;
1495
+ assertZipUint32(offset, "ZIP local data size");
1496
+ return normalized;
1497
+ });
1498
+ }
1499
+ function isQuerySource(source) {
1500
+ return "query" in source && typeof source.query === "function";
1501
+ }
1502
+ function resolveSource(source, query) {
1503
+ if (isQuerySource(source)) return source.query(query);
1504
+ if (query) throw new Error("Log ZIP export query options require a source with query()");
1505
+ return source;
1506
+ }
1507
+ function normalizeMaxEvents(value) {
1508
+ if (value === void 0) return void 0;
1509
+ if (!Number.isFinite(value)) return void 0;
1510
+ return Math.max(0, Math.floor(value));
1511
+ }
1512
+ function createLogZipBlob(files, options = {}) {
1513
+ const normalizedFiles = createNormalizedZipFiles(files, options);
1514
+ const parts = [];
1515
+ let centralDirectoryOffset = 0;
1516
+ for (const file of normalizedFiles) {
1517
+ parts.push(createLocalFileHeader(file), file.nameBytes, file.contentBytes);
1518
+ centralDirectoryOffset += 30 + file.nameBytes.byteLength + file.contentBytes.byteLength;
1519
+ }
1520
+ let centralDirectorySize = 0;
1521
+ for (const file of normalizedFiles) {
1522
+ const header = createCentralDirectoryHeader(file);
1523
+ parts.push(header, file.nameBytes);
1524
+ centralDirectorySize += header.byteLength + file.nameBytes.byteLength;
1525
+ }
1526
+ parts.push(createEndOfCentralDirectory(normalizedFiles.length, centralDirectorySize, centralDirectoryOffset));
1527
+ return new Blob(parts, { type: options.type ?? "application/zip" });
1528
+ }
1529
+ async function exportLogsToZip(source, options = {}) {
1530
+ const format = options.format ?? "ndjson";
1531
+ const createdAt = options.createdAt ?? Date.now();
1532
+ const logFileName = options.logFileName ?? (format === "json" ? "logs.json" : "logs.ndjson");
1533
+ const maxEvents = normalizeMaxEvents(options.maxEvents);
1534
+ const serialize = options.serializeEvent ?? ((event) => safeJsonStringify(event, options.stringify ?? {}));
1535
+ const lines = [];
1536
+ let skippedCount = 0;
1537
+ let from;
1538
+ let to;
1539
+ for await (const event of resolveSource(source, options.query)) {
1540
+ if (maxEvents !== void 0 && lines.length >= maxEvents) break;
1541
+ const mapped = options.mapEvent ? options.mapEvent(event) : event;
1542
+ if (!mapped) {
1543
+ skippedCount += 1;
1544
+ continue;
1545
+ }
1546
+ lines.push(serialize(mapped));
1547
+ from = from === void 0 ? mapped.time : Math.min(from, mapped.time);
1548
+ to = to === void 0 ? mapped.time : Math.max(to, mapped.time);
1549
+ }
1550
+ const files = [{
1551
+ name: logFileName,
1552
+ content: format === "json" ? `[${lines.join(",")}]` : lines.length > 0 ? `${lines.join("\n")}\n` : "",
1553
+ lastModified: createdAt
1554
+ }];
1555
+ if (options.includeManifest ?? true) {
1556
+ const manifest = {
1557
+ schema: "loggerjs.log-export.v1",
1558
+ createdAt: new Date(createdAt).toISOString(),
1559
+ source: options.source,
1560
+ format,
1561
+ logFileName: normalizeZipFileName(logFileName),
1562
+ logCount: lines.length,
1563
+ skippedCount,
1564
+ query: options.query,
1565
+ timeRange: from === void 0 || to === void 0 ? void 0 : {
1566
+ from,
1567
+ to
1568
+ }
1569
+ };
1570
+ files.push({
1571
+ name: options.manifestFileName ?? "manifest.json",
1572
+ content: `${safeJsonStringify(manifest, { space: 2 })}\n`,
1573
+ lastModified: createdAt
1574
+ });
1575
+ }
1576
+ return createLogZipBlob(files, { lastModified: createdAt });
1577
+ }
1578
+ function downloadBlob(blob, filenameOrOptions = {}) {
1579
+ const options = typeof filenameOrOptions === "string" ? { filename: filenameOrOptions } : filenameOrOptions;
1580
+ const documentRef = options.document ?? globalThis.document;
1581
+ const urlRef = options.url ?? globalThis.URL;
1582
+ if (!documentRef?.body || !urlRef?.createObjectURL || !urlRef.revokeObjectURL) throw new Error("downloadBlob requires document and URL.createObjectURL");
1583
+ const href = urlRef.createObjectURL(blob);
1584
+ const anchor = documentRef.createElement("a");
1585
+ anchor.href = href;
1586
+ anchor.download = options.filename ?? "loggerjs-logs.zip";
1587
+ anchor.style.display = "none";
1588
+ documentRef.body.appendChild(anchor);
1589
+ anchor.click();
1590
+ anchor.remove();
1591
+ const revoke = () => urlRef.revokeObjectURL(href);
1592
+ const revokeDelayMs = options.revokeDelayMs ?? 1e3;
1593
+ if (revokeDelayMs <= 0) revoke();
1594
+ else setTimeout(revoke, revokeDelayMs);
1595
+ return href;
1596
+ }
1597
+ //#endregion
1598
+ //#region src/console-integration.ts
1599
+ const defaultLevels = [
1600
+ "debug",
1601
+ "info",
1602
+ "log",
1603
+ "trace",
1604
+ "warn",
1605
+ "error"
1606
+ ];
1607
+ const levelMap = {
1608
+ debug: "debug",
1609
+ info: "info",
1610
+ log: "info",
1611
+ trace: "trace",
1612
+ warn: "warn",
1613
+ error: "error"
1614
+ };
1615
+ function formatConsoleMessage(args) {
1616
+ return args.map((arg) => {
1617
+ if (typeof arg === "string") return arg;
1618
+ if (arg instanceof Error) return arg.message;
1619
+ try {
1620
+ return JSON.stringify(normalizeValue(arg, {
1621
+ maxDepth: 3,
1622
+ maxArrayLength: 20,
1623
+ maxObjectKeys: 40
1624
+ }));
1625
+ } catch {
1626
+ return String(arg);
1627
+ }
1628
+ }).join(" ");
1629
+ }
1630
+ function reportRateLimitDrop() {
1631
+ incrementLoggerMetaCounter("integration.dropped");
1632
+ incrementLoggerMetaCounter("integration.dropped.rate-limit");
1633
+ }
1634
+ function captureConsoleIntegration(options = {}) {
1635
+ const levels = options.levels ?? defaultLevels;
1636
+ const preserveConsole = options.preserveConsole ?? true;
1637
+ const captureArguments = options.captureArguments ?? false;
1638
+ const maxCapturesPerSecond = options.maxCapturesPerSecond ?? 100;
1639
+ return {
1640
+ name: "capture-console",
1641
+ setup(api) {
1642
+ if (typeof console === "undefined") return;
1643
+ const originals = {};
1644
+ const boundOriginals = {};
1645
+ let windowStarted = Date.now();
1646
+ let capturesInWindow = 0;
1647
+ let disposed = false;
1648
+ const shouldCapture = () => {
1649
+ const now = Date.now();
1650
+ if (now - windowStarted >= 1e3) {
1651
+ windowStarted = now;
1652
+ capturesInWindow = 0;
1653
+ }
1654
+ if (capturesInWindow >= maxCapturesPerSecond) {
1655
+ reportRateLimitDrop();
1656
+ return false;
1657
+ }
1658
+ capturesInWindow += 1;
1659
+ return true;
1660
+ };
1661
+ for (const level of levels) {
1662
+ const original = console[level] ?? console.log.bind(console);
1663
+ originals[level] = original;
1664
+ const unpatched = api.unpatched.console[level] ?? original;
1665
+ api.unpatched.console[level] ??= original;
1666
+ boundOriginals[level] = unpatched.bind(console);
1667
+ const capture = api.guard((args) => {
1668
+ if (!shouldCapture()) return;
1669
+ const consoleData = { level };
1670
+ if (captureArguments) consoleData.arguments = normalizeValue(args, { maxDepth: 4 });
1671
+ api.capture({
1672
+ level: levelMap[level],
1673
+ message: formatConsoleMessage(args),
1674
+ props: { console: consoleData }
1675
+ });
1676
+ });
1677
+ console[level] = (...args) => {
1678
+ capture(args);
1679
+ if (preserveConsole) boundOriginals[level]?.(...args);
1680
+ };
1681
+ }
1682
+ return () => {
1683
+ if (disposed) return;
1684
+ disposed = true;
1685
+ for (const level of levels) if (originals[level]) console[level] = originals[level];
1686
+ };
1687
+ }
1688
+ };
1689
+ }
1690
+ //#endregion
1691
+ //#region src/error-integration.ts
1692
+ function resourceInfo(target) {
1693
+ const element = target;
1694
+ return {
1695
+ tagName: element?.tagName,
1696
+ url: element?.src || element?.currentSrc || element?.href,
1697
+ html: element?.outerHTML?.slice(0, 500)
1698
+ };
1699
+ }
1700
+ function reportDrop(reason) {
1701
+ incrementLoggerMetaCounter("integration.dropped");
1702
+ incrementLoggerMetaCounter(`integration.dropped.${reason}`);
1703
+ }
1704
+ function captureBrowserErrorsIntegration(options = {}) {
1705
+ const captureWindowError = options.captureWindowError ?? true;
1706
+ const captureUnhandledRejection = options.captureUnhandledRejection ?? true;
1707
+ const captureResourceErrors = options.captureResourceErrors ?? true;
1708
+ const captureSecurityPolicyViolation = options.captureSecurityPolicyViolation ?? false;
1709
+ const scriptErrorDedupeWindowMs = options.scriptErrorDedupeWindowMs ?? 1e3;
1710
+ return {
1711
+ name: "capture-browser-errors",
1712
+ setup(api) {
1713
+ const disposers = [];
1714
+ let lastScriptErrorAt = 0;
1715
+ let disposed = false;
1716
+ const shouldDropScriptError = (message) => {
1717
+ if (message !== "Script error.") return false;
1718
+ const now = Date.now();
1719
+ if (now - lastScriptErrorAt <= scriptErrorDedupeWindowMs) {
1720
+ reportDrop("script-error-duplicate");
1721
+ return true;
1722
+ }
1723
+ lastScriptErrorAt = now;
1724
+ return false;
1725
+ };
1726
+ if (captureWindowError && typeof addEventListener !== "undefined") {
1727
+ const onError = api.guard((event) => {
1728
+ const errorEvent = event;
1729
+ if ("message" in errorEvent && shouldDropScriptError(errorEvent.message)) return;
1730
+ if ("error" in errorEvent && errorEvent.error) {
1731
+ api.capture({
1732
+ level: "error",
1733
+ message: errorEvent.message || errorEvent.error.message,
1734
+ error: errorEvent.error,
1735
+ props: { browser: {
1736
+ kind: "script-error",
1737
+ file: errorEvent.filename,
1738
+ line: errorEvent.lineno,
1739
+ column: errorEvent.colno
1740
+ } }
1741
+ });
1742
+ return;
1743
+ }
1744
+ if ("message" in errorEvent && errorEvent.message) {
1745
+ api.capture({
1746
+ level: "error",
1747
+ message: errorEvent.message,
1748
+ props: { browser: {
1749
+ kind: "script-error",
1750
+ file: errorEvent.filename,
1751
+ line: errorEvent.lineno,
1752
+ column: errorEvent.colno
1753
+ } }
1754
+ });
1755
+ return;
1756
+ }
1757
+ if (captureResourceErrors) api.capture({
1758
+ level: "error",
1759
+ message: "Browser resource load error",
1760
+ props: { browser: {
1761
+ kind: "resource-error",
1762
+ ...resourceInfo(event.target)
1763
+ } }
1764
+ });
1765
+ });
1766
+ addEventListener("error", onError, true);
1767
+ disposers.push(() => removeEventListener("error", onError, true));
1768
+ }
1769
+ if (captureUnhandledRejection && typeof addEventListener !== "undefined") {
1770
+ const onUnhandledRejection = api.guard((event) => {
1771
+ const reason = event.reason;
1772
+ api.capture({
1773
+ level: "error",
1774
+ message: reason instanceof Error ? reason.message : "Unhandled promise rejection",
1775
+ error: reason,
1776
+ props: {
1777
+ browser: { kind: "unhandledrejection" },
1778
+ reason: normalizeValue(reason, { maxDepth: 5 })
1779
+ }
1780
+ });
1781
+ });
1782
+ addEventListener("unhandledrejection", onUnhandledRejection);
1783
+ disposers.push(() => removeEventListener("unhandledrejection", onUnhandledRejection));
1784
+ }
1785
+ if (captureSecurityPolicyViolation && typeof addEventListener !== "undefined") {
1786
+ const onSecurityPolicyViolation = api.guard((event) => {
1787
+ api.capture({
1788
+ level: "warn",
1789
+ message: `Security policy violation: ${event.violatedDirective}`,
1790
+ props: { browser: {
1791
+ kind: "securitypolicyviolation",
1792
+ blockedURI: event.blockedURI,
1793
+ documentURI: event.documentURI,
1794
+ file: event.sourceFile,
1795
+ line: event.lineNumber,
1796
+ column: event.columnNumber,
1797
+ originalPolicy: event.originalPolicy,
1798
+ effectiveDirective: event.effectiveDirective
1799
+ } }
1800
+ });
1801
+ });
1802
+ addEventListener("securitypolicyviolation", onSecurityPolicyViolation);
1803
+ disposers.push(() => removeEventListener("securitypolicyviolation", onSecurityPolicyViolation));
1804
+ }
1805
+ return () => {
1806
+ if (disposed) return;
1807
+ disposed = true;
1808
+ for (const dispose of disposers) dispose();
1809
+ };
1810
+ }
1811
+ };
1812
+ }
1813
+ //#endregion
1814
+ //#region src/http-capture-utils.ts
1815
+ function nowMs() {
1816
+ return typeof performance !== "undefined" ? performance.now() : Date.now();
1817
+ }
1818
+ function durationMs(started) {
1819
+ return nowMs() - started;
1820
+ }
1821
+ function sanitizeHttpUrl(rawUrl, sanitizer) {
1822
+ const stripped = stripSensitiveUrlParts(rawUrl);
1823
+ return sanitizer ? sanitizer(stripped) : stripped;
1824
+ }
1825
+ function pickAllowedHeaders(headers, allowList) {
1826
+ if (!headers || !allowList || allowList.length === 0) return void 0;
1827
+ const out = {};
1828
+ for (const name of allowList) {
1829
+ const value = headers.get(name);
1830
+ if (value !== null) out[name.toLowerCase()] = value;
1831
+ }
1832
+ return Object.keys(out).length > 0 ? out : void 0;
1833
+ }
1834
+ function headersFromInit(headers) {
1835
+ if (!headers) return void 0;
1836
+ try {
1837
+ return new Headers(headers);
1838
+ } catch {
1839
+ return;
1840
+ }
1841
+ }
1842
+ function shouldSample(sampleRate, random) {
1843
+ if (sampleRate >= 1) return true;
1844
+ if (sampleRate <= 0) return false;
1845
+ return random() < sampleRate;
1846
+ }
1847
+ function stripSensitiveUrlParts(rawUrl) {
1848
+ if (!/^[a-zA-Z][a-zA-Z\d+.-]*:/.test(rawUrl)) return rawUrl.split(/[?#]/, 1)[0] ?? rawUrl;
1849
+ try {
1850
+ const url = new URL(rawUrl);
1851
+ url.search = "";
1852
+ url.hash = "";
1853
+ return url.toString();
1854
+ } catch {
1855
+ return rawUrl.split(/[?#]/, 1)[0] ?? rawUrl;
1856
+ }
1857
+ }
1858
+ //#endregion
1859
+ //#region src/fetch-integration.ts
1860
+ function requestUrl(input) {
1861
+ if (typeof input === "string") return input;
1862
+ if (input instanceof URL) return input.toString();
1863
+ return input.url;
1864
+ }
1865
+ function requestMethod(input, init) {
1866
+ if (init?.method) return init.method.toUpperCase();
1867
+ if (typeof input === "object" && "method" in input && input.method) return input.method.toUpperCase();
1868
+ return "GET";
1869
+ }
1870
+ function captureFetchIntegration(options = {}) {
1871
+ const minStatus = options.minStatus ?? 400;
1872
+ const captureAll = options.captureAll ?? options.captureSuccessful ?? false;
1873
+ const sampleRate = options.sampleRate ?? 1;
1874
+ const random = options.random ?? Math.random;
1875
+ return {
1876
+ name: "capture-fetch",
1877
+ setup(api) {
1878
+ const current = globalThis.fetch;
1879
+ if (!current) return;
1880
+ const original = api.unpatched.fetch ?? current.bind(globalThis);
1881
+ api.unpatched.fetch ??= original;
1882
+ const capture = api.guard((input) => api.capture(input));
1883
+ globalThis.fetch = async (input, init) => {
1884
+ const started = nowMs();
1885
+ const url = sanitizeHttpUrl(requestUrl(input), options.sanitizeUrl);
1886
+ const method = requestMethod(input, init);
1887
+ try {
1888
+ const response = await original(input, init);
1889
+ if (response.status >= minStatus || captureAll && shouldSample(sampleRate, random)) capture({
1890
+ level: response.status >= minStatus ? "warn" : "debug",
1891
+ message: `Fetch ${response.status} ${method} ${url}`,
1892
+ props: { http: {
1893
+ kind: "fetch",
1894
+ method,
1895
+ url,
1896
+ status: response.status,
1897
+ ok: response.ok,
1898
+ durationMs: durationMs(started),
1899
+ requestHeaders: pickAllowedHeaders(headersFromInit(init?.headers), options.captureRequestHeaders),
1900
+ responseHeaders: pickAllowedHeaders(response.headers, options.captureResponseHeaders)
1901
+ } }
1902
+ });
1903
+ return response;
1904
+ } catch (error) {
1905
+ capture({
1906
+ level: "error",
1907
+ message: `Fetch network error ${method} ${url}`,
1908
+ error,
1909
+ props: {
1910
+ http: {
1911
+ kind: "fetch",
1912
+ method,
1913
+ url,
1914
+ durationMs: durationMs(started)
1915
+ },
1916
+ input: normalizeValue(input, { maxDepth: 3 })
1917
+ }
1918
+ });
1919
+ throw error;
1920
+ }
1921
+ };
1922
+ return () => {
1923
+ globalThis.fetch = current;
1924
+ };
1925
+ }
1926
+ };
1927
+ }
1928
+ //#endregion
1929
+ //#region src/xhr-integration.ts
1930
+ const XHR_META = "__LOGGERJS_XHR_META__";
1931
+ function captureXHRIntegration(options = {}) {
1932
+ const minStatus = options.minStatus ?? 400;
1933
+ const captureAll = options.captureAll ?? options.captureSuccessful ?? false;
1934
+ const sampleRate = options.sampleRate ?? 1;
1935
+ const random = options.random ?? Math.random;
1936
+ return {
1937
+ name: "capture-xhr",
1938
+ setup(api) {
1939
+ if (typeof XMLHttpRequest === "undefined") return;
1940
+ const proto = XMLHttpRequest.prototype;
1941
+ const originalOpen = proto.open;
1942
+ const originalSend = proto.send;
1943
+ api.unpatched.XMLHttpRequest ??= XMLHttpRequest;
1944
+ const capture = api.guard((input) => api.capture(input));
1945
+ proto.open = function patchedOpen(method, url, ...rest) {
1946
+ this[XHR_META] = {
1947
+ method: method.toUpperCase(),
1948
+ url: sanitizeHttpUrl(String(url), options.sanitizeUrl),
1949
+ started: 0
1950
+ };
1951
+ return originalOpen.apply(this, [
1952
+ method,
1953
+ url,
1954
+ ...rest
1955
+ ]);
1956
+ };
1957
+ proto.send = function patchedSend(body) {
1958
+ const meta = this[XHR_META];
1959
+ if (meta) meta.started = nowMs();
1960
+ let captured = false;
1961
+ const onLoadEnd = () => {
1962
+ if (!meta || captured) return;
1963
+ if (this.status >= minStatus || captureAll && shouldSample(sampleRate, random)) {
1964
+ captured = true;
1965
+ capture({
1966
+ level: this.status >= minStatus ? "warn" : "debug",
1967
+ message: `XHR ${this.status} ${meta.method} ${meta.url}`,
1968
+ props: { http: {
1969
+ kind: "xhr",
1970
+ method: meta.method,
1971
+ url: meta.url,
1972
+ status: this.status,
1973
+ durationMs: durationMs(meta.started)
1974
+ } }
1975
+ });
1976
+ }
1977
+ };
1978
+ const onError = () => {
1979
+ if (!meta || captured) return;
1980
+ captured = true;
1981
+ capture({
1982
+ level: "error",
1983
+ message: `XHR network error ${meta.method} ${meta.url}`,
1984
+ props: { http: {
1985
+ kind: "xhr",
1986
+ method: meta.method,
1987
+ url: meta.url,
1988
+ durationMs: durationMs(meta.started)
1989
+ } }
1990
+ });
1991
+ };
1992
+ this.addEventListener("loadend", onLoadEnd, { once: true });
1993
+ this.addEventListener("error", onError, { once: true });
1994
+ return originalSend.apply(this, [body]);
1995
+ };
1996
+ return () => {
1997
+ proto.open = originalOpen;
1998
+ proto.send = originalSend;
1999
+ };
2000
+ }
2001
+ };
2002
+ }
2003
+ //#endregion
2004
+ //#region src/framework-error-integration.ts
2005
+ function errorMessage(error) {
2006
+ return error instanceof Error ? error.message : "Framework error";
2007
+ }
2008
+ function instanceComponentName(instance) {
2009
+ const record = instance;
2010
+ return record?.$options?.name ?? record?.type?.displayName ?? record?.type?.name ?? record?.constructor?.name;
2011
+ }
2012
+ function normalizeFrameworkInfo(input, fallbackFramework) {
2013
+ if (typeof input === "string") return {
2014
+ framework: fallbackFramework,
2015
+ info: input
2016
+ };
2017
+ return {
2018
+ framework: fallbackFramework,
2019
+ ...input
2020
+ };
2021
+ }
2022
+ function captureFrameworkErrorsIntegration(options = {}) {
2023
+ const integrationName = options.name ?? "capture-framework-errors";
2024
+ const fallbackFramework = options.framework ?? "framework";
2025
+ const level = options.level ?? "error";
2026
+ const maxPending = Math.max(0, Math.floor(options.maxPending ?? 10));
2027
+ const infoMaxDepth = options.infoMaxDepth ?? 6;
2028
+ const pending = [];
2029
+ let api;
2030
+ let started = false;
2031
+ let disposed = false;
2032
+ const emit = (error, rawInfo) => {
2033
+ const info = normalizeFrameworkInfo(rawInfo, fallbackFramework);
2034
+ const message = options.getMessage?.(error, info) ?? errorMessage(error);
2035
+ api?.capture({
2036
+ level,
2037
+ message,
2038
+ error,
2039
+ props: { browser: {
2040
+ kind: "framework-error",
2041
+ framework: info.framework,
2042
+ componentName: info.componentName,
2043
+ componentStack: info.componentStack,
2044
+ info: normalizeValue(info.info, {
2045
+ maxDepth: infoMaxDepth,
2046
+ maxObjectKeys: 80
2047
+ }),
2048
+ props: normalizeValue(info.props, {
2049
+ maxDepth: infoMaxDepth,
2050
+ maxObjectKeys: 80
2051
+ })
2052
+ } }
2053
+ });
2054
+ };
2055
+ const capture = (error, info) => {
2056
+ if (api) {
2057
+ emit(error, info);
2058
+ return;
2059
+ }
2060
+ if (started || disposed || pending.length >= maxPending) return;
2061
+ pending.push({
2062
+ error,
2063
+ info
2064
+ });
2065
+ };
2066
+ return {
2067
+ name: integrationName,
2068
+ setup(nextApi) {
2069
+ api = nextApi;
2070
+ started = true;
2071
+ disposed = false;
2072
+ for (const item of pending.splice(0)) emit(item.error, item.info);
2073
+ return () => {
2074
+ api = void 0;
2075
+ disposed = true;
2076
+ };
2077
+ },
2078
+ capture,
2079
+ reactComponentDidCatch(error, errorInfo) {
2080
+ capture(error, {
2081
+ framework: "react",
2082
+ componentStack: errorInfo?.componentStack
2083
+ });
2084
+ },
2085
+ vueErrorHandler(error, instance, info) {
2086
+ capture(error, {
2087
+ framework: "vue",
2088
+ componentName: instanceComponentName(instance),
2089
+ info
2090
+ });
2091
+ },
2092
+ solidErrorHandler(error) {
2093
+ capture(error, { framework: "solid" });
2094
+ },
2095
+ svelteErrorHandler(error, info) {
2096
+ capture(error, normalizeFrameworkInfo(info, "svelte"));
2097
+ }
2098
+ };
2099
+ }
2100
+ //#endregion
2101
+ //#region src/reporting-integration.ts
2102
+ const defaultReportTypes = [
2103
+ "csp-violation",
2104
+ "deprecation",
2105
+ "intervention",
2106
+ "crash"
2107
+ ];
2108
+ function levelFor$1(level, report) {
2109
+ if (typeof level === "function") return level(report);
2110
+ if (level) return level;
2111
+ return report.type === "deprecation" ? "info" : "warn";
2112
+ }
2113
+ function maybeSanitize(url, sanitizeUrl) {
2114
+ return url && sanitizeUrl ? sanitizeUrl(url) : url;
2115
+ }
2116
+ function normalizeReport(report, sanitizeUrl) {
2117
+ const raw = typeof report.toJSON === "function" ? report.toJSON() : report;
2118
+ const record = raw && typeof raw === "object" ? raw : {};
2119
+ const body = "body" in record ? record.body : report.body;
2120
+ const url = typeof record.url === "string" ? record.url : report.url;
2121
+ return {
2122
+ type: typeof record.type === "string" ? record.type : report.type ?? "unknown",
2123
+ url: maybeSanitize(url, sanitizeUrl),
2124
+ body: normalizeValue(body, {
2125
+ maxDepth: 6,
2126
+ maxObjectKeys: 80
2127
+ })
2128
+ };
2129
+ }
2130
+ function normalizeCspViolation(event, sanitizeUrl) {
2131
+ return {
2132
+ type: "securitypolicyviolation",
2133
+ blockedURI: maybeSanitize(event.blockedURI, sanitizeUrl),
2134
+ documentURI: maybeSanitize(event.documentURI, sanitizeUrl),
2135
+ effectiveDirective: event.effectiveDirective,
2136
+ violatedDirective: event.violatedDirective,
2137
+ disposition: event.disposition,
2138
+ sourceFile: maybeSanitize(event.sourceFile, sanitizeUrl),
2139
+ lineNumber: event.lineNumber,
2140
+ columnNumber: event.columnNumber,
2141
+ statusCode: event.statusCode,
2142
+ sample: event.sample
2143
+ };
2144
+ }
2145
+ function captureReportingIntegration(options = {}) {
2146
+ const captureSecurityPolicyViolation = options.captureSecurityPolicyViolation ?? true;
2147
+ const captureReportingObserver = options.captureReportingObserver ?? true;
2148
+ const reportTypes = options.reportTypes ?? defaultReportTypes;
2149
+ const buffered = options.buffered ?? true;
2150
+ return {
2151
+ name: "capture-reporting",
2152
+ setup(api) {
2153
+ const add = options.addEventListener ?? globalThis.addEventListener?.bind(globalThis);
2154
+ const remove = options.removeEventListener ?? globalThis.removeEventListener?.bind(globalThis);
2155
+ const Observer = options.ReportingObserver ?? globalThis.ReportingObserver;
2156
+ const disposers = [];
2157
+ let disposed = false;
2158
+ const captureReport = api.guard((report) => {
2159
+ if (disposed) return;
2160
+ api.capture({
2161
+ level: levelFor$1(options.level, report),
2162
+ message: `Browser report ${report.type}`,
2163
+ props: { browser: {
2164
+ kind: "report",
2165
+ report
2166
+ } }
2167
+ });
2168
+ });
2169
+ if (captureSecurityPolicyViolation && add && remove) {
2170
+ const onViolation = api.guard((event) => {
2171
+ const report = normalizeCspViolation(event, options.sanitizeUrl);
2172
+ api.capture({
2173
+ level: levelFor$1(options.level, report),
2174
+ message: `Security policy violation: ${report.effectiveDirective ?? report.violatedDirective ?? "unknown"}`,
2175
+ props: { browser: {
2176
+ kind: "securitypolicyviolation",
2177
+ report
2178
+ } }
2179
+ });
2180
+ });
2181
+ add("securitypolicyviolation", onViolation);
2182
+ disposers.push(() => remove("securitypolicyviolation", onViolation));
2183
+ }
2184
+ if (captureReportingObserver && Observer) try {
2185
+ const observer = new Observer((reports) => {
2186
+ for (const report of reports) captureReport(normalizeReport(report, options.sanitizeUrl));
2187
+ }, {
2188
+ buffered,
2189
+ types: reportTypes
2190
+ });
2191
+ observer.observe();
2192
+ disposers.push(() => {
2193
+ for (const report of observer.takeRecords?.() ?? []) captureReport(normalizeReport(report, options.sanitizeUrl));
2194
+ observer.disconnect();
2195
+ });
2196
+ } catch {}
2197
+ return () => {
2198
+ if (disposed) return;
2199
+ for (const dispose of disposers) dispose();
2200
+ disposed = true;
2201
+ };
2202
+ }
2203
+ };
2204
+ }
2205
+ //#endregion
2206
+ //#region src/router-integration.ts
2207
+ function locationHref(location) {
2208
+ if (!location) return "http://localhost/";
2209
+ if (location.href) return location.href;
2210
+ return `${location.pathname ?? "/"}${location.search ?? ""}${location.hash ?? ""}`;
2211
+ }
2212
+ function normalizeRouteUrl(url, location, mode, sanitizeUrl) {
2213
+ const fallback = locationHref(location);
2214
+ const raw = url === void 0 || url === null ? fallback : String(url);
2215
+ let normalized = raw;
2216
+ try {
2217
+ const parsed = new URL(raw, fallback);
2218
+ normalized = mode === "href" ? parsed.href : `${parsed.pathname}${parsed.search}${parsed.hash}`;
2219
+ } catch {
2220
+ normalized = raw;
2221
+ }
2222
+ return sanitizeUrl ? sanitizeUrl(normalized) : normalized;
2223
+ }
2224
+ function routeState(includeState, state, maxDepth) {
2225
+ return includeState ? normalizeValue(state, {
2226
+ maxDepth,
2227
+ maxObjectKeys: 80
2228
+ }) : void 0;
2229
+ }
2230
+ function captureRouterIntegration(options = {}) {
2231
+ const level = options.level ?? "info";
2232
+ const captureInitial = options.captureInitial ?? true;
2233
+ const includeState = options.includeState ?? false;
2234
+ const stateMaxDepth = options.stateMaxDepth ?? 4;
2235
+ const urlMode = options.urlMode ?? "path";
2236
+ return {
2237
+ name: "capture-router",
2238
+ setup(api) {
2239
+ const history = options.history ?? globalThis.history;
2240
+ const location = options.location ?? globalThis.location;
2241
+ const add = options.addEventListener ?? globalThis.addEventListener?.bind(globalThis);
2242
+ const remove = options.removeEventListener ?? globalThis.removeEventListener?.bind(globalThis);
2243
+ const originalPushState = history?.pushState;
2244
+ const originalReplaceState = history?.replaceState;
2245
+ let current = normalizeRouteUrl(void 0, location, urlMode, options.sanitizeUrl);
2246
+ let disposed = false;
2247
+ const capture = (trigger, to, state) => {
2248
+ if (disposed) return;
2249
+ const previous = current;
2250
+ current = to;
2251
+ if (trigger !== "initial" && previous === to) return;
2252
+ const route = {
2253
+ trigger,
2254
+ from: trigger === "initial" ? void 0 : previous,
2255
+ to,
2256
+ state: routeState(includeState, state, stateMaxDepth)
2257
+ };
2258
+ api.capture({
2259
+ level,
2260
+ message: trigger === "initial" ? `Route initial ${to}` : `Route change ${to}`,
2261
+ props: { browser: {
2262
+ kind: "route-change",
2263
+ route
2264
+ } }
2265
+ });
2266
+ };
2267
+ if (captureInitial) capture("initial", current, history?.state);
2268
+ if (history && originalPushState) history.pushState = function pushState(data, unused, url) {
2269
+ const result = originalPushState.call(this, data, unused, url);
2270
+ capture("pushState", normalizeRouteUrl(url, location, urlMode, options.sanitizeUrl), data);
2271
+ return result;
2272
+ };
2273
+ if (history && originalReplaceState) history.replaceState = function replaceState(data, unused, url) {
2274
+ const result = originalReplaceState.call(this, data, unused, url);
2275
+ capture("replaceState", normalizeRouteUrl(url, location, urlMode, options.sanitizeUrl), data);
2276
+ return result;
2277
+ };
2278
+ const onPopState = api.guard((event) => {
2279
+ capture("popstate", normalizeRouteUrl(void 0, location, urlMode, options.sanitizeUrl), event.state);
2280
+ });
2281
+ const onHashChange = api.guard((event) => {
2282
+ capture("hashchange", normalizeRouteUrl(event.newURL, location, urlMode, options.sanitizeUrl), history?.state);
2283
+ });
2284
+ add?.("popstate", onPopState);
2285
+ add?.("hashchange", onHashChange);
2286
+ return () => {
2287
+ if (disposed) return;
2288
+ disposed = true;
2289
+ if (history && originalPushState) history.pushState = originalPushState;
2290
+ if (history && originalReplaceState) history.replaceState = originalReplaceState;
2291
+ remove?.("popstate", onPopState);
2292
+ remove?.("hashchange", onHashChange);
2293
+ };
2294
+ }
2295
+ };
2296
+ }
2297
+ //#endregion
2298
+ //#region src/runtime-host-integration.ts
2299
+ const textEncoder$2 = new TextEncoder();
2300
+ function byteLength(value) {
2301
+ if (typeof value === "string") return textEncoder$2.encode(value).byteLength;
2302
+ if (value instanceof ArrayBuffer) return value.byteLength;
2303
+ if (ArrayBuffer.isView(value)) return value.byteLength;
2304
+ }
2305
+ function valueSummary(value, includeData) {
2306
+ return {
2307
+ dataType: typeof value === "object" && value ? value.constructor?.name ?? "object" : typeof value,
2308
+ byteLength: byteLength(value),
2309
+ data: includeData ? normalizeValue(value, {
2310
+ maxDepth: 4,
2311
+ maxObjectKeys: 80
2312
+ }) : void 0
2313
+ };
2314
+ }
2315
+ function senderInfo(sender, sanitizeUrl) {
2316
+ if (!sender) return void 0;
2317
+ const sanitize = (url) => url && sanitizeUrl ? sanitizeUrl(url) : url;
2318
+ return {
2319
+ id: sender.id,
2320
+ origin: sender.origin,
2321
+ url: sanitize(sender.url),
2322
+ tab: sender.tab ? {
2323
+ id: sender.tab.id,
2324
+ url: sanitize(sender.tab.url)
2325
+ } : void 0
2326
+ };
2327
+ }
2328
+ function defaultExtensionRuntime() {
2329
+ const global = globalThis;
2330
+ return global.browser?.runtime ?? global.chrome?.runtime;
2331
+ }
2332
+ function defaultIpcRenderer() {
2333
+ const global = globalThis;
2334
+ if (global.electron?.ipcRenderer) return global.electron.ipcRenderer;
2335
+ try {
2336
+ return (global.require?.("electron"))?.ipcRenderer;
2337
+ } catch {
2338
+ return;
2339
+ }
2340
+ }
2341
+ function captureRuntimeHostIntegration(options = {}) {
2342
+ const level = options.level ?? "debug";
2343
+ const captureExtensionMessages = options.captureExtensionMessages ?? true;
2344
+ const captureExtensionInstalled = options.captureExtensionInstalled ?? true;
2345
+ const captureExtensionMessageData = options.captureExtensionMessageData ?? false;
2346
+ const captureElectronMessages = options.captureElectronMessages ?? true;
2347
+ const captureElectronSend = options.captureElectronSend ?? true;
2348
+ const captureElectronInvoke = options.captureElectronInvoke ?? true;
2349
+ const captureElectronMessageData = options.captureElectronMessageData ?? false;
2350
+ const electronChannels = options.electronChannels ?? [];
2351
+ return {
2352
+ name: "capture-runtime-host",
2353
+ setup(api) {
2354
+ const extensionRuntime = options.extensionRuntime ?? defaultExtensionRuntime();
2355
+ const ipcRenderer = options.ipcRenderer ?? defaultIpcRenderer();
2356
+ const disposers = [];
2357
+ let disposed = false;
2358
+ const capture = api.guard((input) => {
2359
+ if (!disposed) api.capture(input);
2360
+ });
2361
+ if (extensionRuntime) {
2362
+ const manifest = extensionRuntime.getManifest?.();
2363
+ if (captureExtensionMessages && extensionRuntime.onMessage?.addListener) {
2364
+ const onMessage = (message, sender) => {
2365
+ capture({
2366
+ level,
2367
+ message: "Browser extension message",
2368
+ props: { browser: {
2369
+ kind: "extension-message",
2370
+ extension: {
2371
+ id: extensionRuntime.id,
2372
+ name: manifest?.name,
2373
+ version: manifest?.version
2374
+ },
2375
+ message: valueSummary(message, captureExtensionMessageData),
2376
+ sender: senderInfo(sender, options.sanitizeUrl)
2377
+ } }
2378
+ });
2379
+ };
2380
+ extensionRuntime.onMessage.addListener(onMessage);
2381
+ disposers.push(() => extensionRuntime.onMessage?.removeListener?.(onMessage));
2382
+ }
2383
+ if (captureExtensionInstalled && extensionRuntime.onInstalled?.addListener) {
2384
+ const onInstalled = (details) => {
2385
+ capture({
2386
+ level,
2387
+ message: "Browser extension installed",
2388
+ props: { browser: {
2389
+ kind: "extension-installed",
2390
+ extension: {
2391
+ id: extensionRuntime.id,
2392
+ name: manifest?.name,
2393
+ version: manifest?.version
2394
+ },
2395
+ details: normalizeValue(details, {
2396
+ maxDepth: 4,
2397
+ maxObjectKeys: 80
2398
+ })
2399
+ } }
2400
+ });
2401
+ };
2402
+ extensionRuntime.onInstalled.addListener(onInstalled);
2403
+ disposers.push(() => extensionRuntime.onInstalled?.removeListener?.(onInstalled));
2404
+ }
2405
+ }
2406
+ if (ipcRenderer) {
2407
+ if (captureElectronMessages && ipcRenderer.on) for (const channel of electronChannels) {
2408
+ const listener = (_event, ...args) => {
2409
+ capture({
2410
+ level,
2411
+ message: `Electron IPC message ${channel}`,
2412
+ props: { browser: {
2413
+ kind: "electron-ipc",
2414
+ direction: "incoming",
2415
+ channel,
2416
+ args: args.map((arg) => valueSummary(arg, captureElectronMessageData))
2417
+ } }
2418
+ });
2419
+ };
2420
+ ipcRenderer.on(channel, listener);
2421
+ disposers.push(() => {
2422
+ if (ipcRenderer.off) ipcRenderer.off(channel, listener);
2423
+ else ipcRenderer.removeListener?.(channel, listener);
2424
+ });
2425
+ }
2426
+ if (captureElectronSend && ipcRenderer.send) {
2427
+ const originalSend = ipcRenderer.send;
2428
+ ipcRenderer.send = (channel, ...args) => {
2429
+ capture({
2430
+ level,
2431
+ message: `Electron IPC send ${channel}`,
2432
+ props: { browser: {
2433
+ kind: "electron-ipc",
2434
+ direction: "outgoing",
2435
+ channel,
2436
+ args: args.map((arg) => valueSummary(arg, captureElectronMessageData))
2437
+ } }
2438
+ });
2439
+ return originalSend.call(ipcRenderer, channel, ...args);
2440
+ };
2441
+ disposers.push(() => {
2442
+ ipcRenderer.send = originalSend;
2443
+ });
2444
+ }
2445
+ if (captureElectronInvoke && ipcRenderer.invoke) {
2446
+ const originalInvoke = ipcRenderer.invoke;
2447
+ ipcRenderer.invoke = (channel, ...args) => {
2448
+ capture({
2449
+ level,
2450
+ message: `Electron IPC invoke ${channel}`,
2451
+ props: { browser: {
2452
+ kind: "electron-ipc",
2453
+ direction: "invoke",
2454
+ channel,
2455
+ args: args.map((arg) => valueSummary(arg, captureElectronMessageData))
2456
+ } }
2457
+ });
2458
+ return originalInvoke.call(ipcRenderer, channel, ...args);
2459
+ };
2460
+ disposers.push(() => {
2461
+ ipcRenderer.invoke = originalInvoke;
2462
+ });
2463
+ }
2464
+ }
2465
+ return () => {
2466
+ if (disposed) return;
2467
+ disposed = true;
2468
+ for (const dispose of disposers) dispose();
2469
+ };
2470
+ }
2471
+ };
2472
+ }
2473
+ //#endregion
2474
+ //#region src/service-worker-integration.ts
2475
+ const textEncoder$1 = new TextEncoder();
2476
+ function dataType$1(data) {
2477
+ if (typeof data === "string") return "string";
2478
+ if (data instanceof ArrayBuffer) return "arraybuffer";
2479
+ if (ArrayBuffer.isView(data)) return data.constructor.name;
2480
+ return data?.constructor?.name ?? typeof data;
2481
+ }
2482
+ function dataByteLength$1(data) {
2483
+ if (typeof data === "string") return textEncoder$1.encode(data).byteLength;
2484
+ if (data instanceof ArrayBuffer) return data.byteLength;
2485
+ if (ArrayBuffer.isView(data)) return data.byteLength;
2486
+ const size = data?.size;
2487
+ return typeof size === "number" && Number.isFinite(size) ? size : void 0;
2488
+ }
2489
+ function controllerInfo(controller, sanitizeUrl) {
2490
+ if (!controller) return void 0;
2491
+ return {
2492
+ scriptURL: controller.scriptURL && sanitizeUrl ? sanitizeUrl(controller.scriptURL) : controller.scriptURL,
2493
+ state: controller.state
2494
+ };
2495
+ }
2496
+ function messagePayload$1(event, captureMessageData) {
2497
+ return {
2498
+ dataType: dataType$1(event.data),
2499
+ byteLength: dataByteLength$1(event.data),
2500
+ data: captureMessageData ? normalizeValue(event.data, {
2501
+ maxDepth: 4,
2502
+ maxObjectKeys: 80
2503
+ }) : void 0,
2504
+ origin: event.origin,
2505
+ lastEventId: event.lastEventId
2506
+ };
2507
+ }
2508
+ function captureServiceWorkerIntegration(options = {}) {
2509
+ const level = options.level ?? "debug";
2510
+ const captureControllerChange = options.captureControllerChange ?? true;
2511
+ const captureMessages = options.captureMessages ?? true;
2512
+ const captureMessageErrors = options.captureMessageErrors ?? true;
2513
+ const captureMessageData = options.captureMessageData ?? false;
2514
+ return {
2515
+ name: "capture-service-worker",
2516
+ setup(api) {
2517
+ const serviceWorker = options.serviceWorker ?? (typeof navigator === "undefined" ? void 0 : navigator.serviceWorker);
2518
+ if (!serviceWorker?.addEventListener || !serviceWorker.removeEventListener) return;
2519
+ const disposers = [];
2520
+ let disposed = false;
2521
+ const capture = api.guard((input) => {
2522
+ if (!disposed) api.capture(input);
2523
+ });
2524
+ if (captureControllerChange) {
2525
+ const onControllerChange = () => {
2526
+ capture({
2527
+ level,
2528
+ message: "Service worker controller change",
2529
+ props: { browser: {
2530
+ kind: "service-worker",
2531
+ event: "controllerchange",
2532
+ controller: controllerInfo(serviceWorker.controller, options.sanitizeUrl)
2533
+ } }
2534
+ });
2535
+ };
2536
+ serviceWorker.addEventListener("controllerchange", onControllerChange);
2537
+ disposers.push(() => serviceWorker.removeEventListener("controllerchange", onControllerChange));
2538
+ }
2539
+ if (captureMessages) {
2540
+ const onMessage = (event) => {
2541
+ capture({
2542
+ level,
2543
+ message: "Service worker message",
2544
+ props: { browser: {
2545
+ kind: "service-worker",
2546
+ event: "message",
2547
+ message: messagePayload$1(event, captureMessageData)
2548
+ } }
2549
+ });
2550
+ };
2551
+ serviceWorker.addEventListener("message", onMessage);
2552
+ disposers.push(() => serviceWorker.removeEventListener("message", onMessage));
2553
+ }
2554
+ if (captureMessageErrors) {
2555
+ const onMessageError = (event) => {
2556
+ capture({
2557
+ level: "warn",
2558
+ message: "Service worker message error",
2559
+ props: { browser: {
2560
+ kind: "service-worker",
2561
+ event: "messageerror",
2562
+ type: event.type
2563
+ } }
2564
+ });
2565
+ };
2566
+ serviceWorker.addEventListener("messageerror", onMessageError);
2567
+ disposers.push(() => serviceWorker.removeEventListener("messageerror", onMessageError));
2568
+ }
2569
+ return () => {
2570
+ if (disposed) return;
2571
+ disposed = true;
2572
+ for (const dispose of disposers) dispose();
2573
+ };
2574
+ }
2575
+ };
2576
+ }
2577
+ //#endregion
2578
+ //#region src/user-action-integration.ts
2579
+ const defaultEvents = [
2580
+ "click",
2581
+ "input",
2582
+ "change",
2583
+ "submit"
2584
+ ];
2585
+ const defaultLabelAttributes = [
2586
+ "data-loggerjs-label",
2587
+ "aria-label",
2588
+ "name",
2589
+ "id"
2590
+ ];
2591
+ function truncate(value, maxLength) {
2592
+ if (!value) return void 0;
2593
+ return value.length > maxLength ? value.slice(0, maxLength) : value;
2594
+ }
2595
+ function readAttribute(target, name) {
2596
+ return target.getAttribute?.(name) || void 0;
2597
+ }
2598
+ function sanitizeField(value, field, sanitize) {
2599
+ if (!value) return void 0;
2600
+ return sanitize ? sanitize(value, field) : value;
2601
+ }
2602
+ function firstLabel(target, attributes) {
2603
+ for (const name of attributes) {
2604
+ const value = readAttribute(target, name) ?? target[name];
2605
+ if (typeof value === "string" && value) return value;
2606
+ }
2607
+ }
2608
+ function targetInfo(target, options) {
2609
+ const element = target;
2610
+ if (!element || typeof element !== "object") return {};
2611
+ const maxTextLength = options.maxTextLength ?? 120;
2612
+ const sanitize = options.sanitize;
2613
+ const info = {
2614
+ tagName: sanitizeField(element.tagName?.toLowerCase(), "tagName", sanitize),
2615
+ id: sanitizeField(element.id, "id", sanitize),
2616
+ name: sanitizeField(element.name, "name", sanitize),
2617
+ role: sanitizeField(element.role ?? readAttribute(element, "role"), "role", sanitize),
2618
+ type: sanitizeField(element.type, "type", sanitize),
2619
+ href: sanitizeField(element.href, "href", sanitize),
2620
+ label: sanitizeField(firstLabel(element, options.labelAttributes ?? defaultLabelAttributes), "label", sanitize)
2621
+ };
2622
+ if (options.captureText) info.text = sanitizeField(truncate(element.textContent ?? void 0, maxTextLength), "text", sanitize);
2623
+ if (options.captureValue) info.value = sanitizeField(truncate(element.value, maxTextLength), "value", sanitize);
2624
+ return info;
2625
+ }
2626
+ function throttleKey(event, target) {
2627
+ return `${event.type}:${target.tagName ?? ""}:${target.id ?? ""}:${target.name ?? ""}:${target.label ?? ""}`;
2628
+ }
2629
+ function captureUserActionsIntegration(options = {}) {
2630
+ const events = options.events ?? defaultEvents;
2631
+ const level = options.level ?? "info";
2632
+ const listenerCapture = options.listenerCapture ?? true;
2633
+ const throttleMs = Math.max(0, Math.floor(options.throttleMs ?? 250));
2634
+ const clock = options.clock ?? Date.now;
2635
+ return {
2636
+ name: "capture-user-actions",
2637
+ setup(api) {
2638
+ const root = options.root ?? globalThis.document;
2639
+ if (!root?.addEventListener || !root.removeEventListener) return;
2640
+ const lastSeen = /* @__PURE__ */ new Map();
2641
+ let disposed = false;
2642
+ const onAction = api.guard((event) => {
2643
+ if (disposed) return;
2644
+ const target = targetInfo(event.target, options);
2645
+ if (options.ignore?.(event, target)) return;
2646
+ const now = clock();
2647
+ const key = throttleKey(event, target);
2648
+ const last = lastSeen.get(key) ?? 0;
2649
+ if (throttleMs > 0 && now - last < throttleMs) return;
2650
+ lastSeen.set(key, now);
2651
+ const action = {
2652
+ type: event.type,
2653
+ target
2654
+ };
2655
+ api.capture({
2656
+ level,
2657
+ message: `User action ${event.type}${target.label ? ` ${target.label}` : ""}`,
2658
+ props: { browser: {
2659
+ kind: "user-action",
2660
+ action
2661
+ } }
2662
+ });
2663
+ });
2664
+ for (const event of events) root.addEventListener(event, onAction, listenerCapture);
2665
+ return () => {
2666
+ if (disposed) return;
2667
+ disposed = true;
2668
+ for (const event of events) root.removeEventListener(event, onAction, listenerCapture);
2669
+ };
2670
+ }
2671
+ };
2672
+ }
2673
+ //#endregion
2674
+ //#region src/websocket-integration.ts
2675
+ const registryKey = "browser.WebSocket";
2676
+ const textEncoder = new TextEncoder();
2677
+ function dataType(data) {
2678
+ if (typeof data === "string") return "string";
2679
+ if (data instanceof ArrayBuffer) return "arraybuffer";
2680
+ if (ArrayBuffer.isView(data)) return data.constructor.name;
2681
+ return data?.constructor?.name ?? typeof data;
2682
+ }
2683
+ function dataByteLength(data) {
2684
+ if (typeof data === "string") return textEncoder.encode(data).byteLength;
2685
+ if (data instanceof ArrayBuffer) return data.byteLength;
2686
+ if (ArrayBuffer.isView(data)) return data.byteLength;
2687
+ const size = data?.size;
2688
+ return typeof size === "number" && Number.isFinite(size) ? size : void 0;
2689
+ }
2690
+ function messagePayload(direction, data, captureMessageData) {
2691
+ return {
2692
+ direction,
2693
+ dataType: dataType(data),
2694
+ byteLength: dataByteLength(data),
2695
+ data: captureMessageData ? normalizeValue(data, {
2696
+ maxDepth: 4,
2697
+ maxObjectKeys: 80
2698
+ }) : void 0
2699
+ };
2700
+ }
2701
+ function closeEventPayload(event) {
2702
+ const close = event;
2703
+ return {
2704
+ code: close.code,
2705
+ reason: close.reason,
2706
+ wasClean: close.wasClean
2707
+ };
2708
+ }
2709
+ function copyStaticConstants(target, source) {
2710
+ for (const key of [
2711
+ "CONNECTING",
2712
+ "OPEN",
2713
+ "CLOSING",
2714
+ "CLOSED"
2715
+ ]) if (source[key] !== void 0) target[key] = source[key];
2716
+ }
2717
+ function captureWebSocketIntegration(options = {}) {
2718
+ const level = options.level ?? "debug";
2719
+ const captureConnect = options.captureConnect ?? true;
2720
+ const captureOpen = options.captureOpen ?? true;
2721
+ const captureClose = options.captureClose ?? true;
2722
+ const captureError = options.captureError ?? true;
2723
+ const captureMessages = options.captureMessages ?? false;
2724
+ const captureSentMessages = options.captureSentMessages ?? false;
2725
+ const captureMessageData = options.captureMessageData ?? false;
2726
+ const sampleRate = options.sampleRate ?? 1;
2727
+ const random = options.random ?? Math.random;
2728
+ return {
2729
+ name: "capture-websocket",
2730
+ setup(api) {
2731
+ const globalWebSocket = globalThis.WebSocket;
2732
+ const current = options.WebSocket ?? globalWebSocket;
2733
+ if (!current) return;
2734
+ const original = api.unpatched.get(registryKey) ?? current;
2735
+ api.unpatched.set(registryKey, original);
2736
+ const capture = api.guard((input) => api.capture(input));
2737
+ const PatchedWebSocket = function WebSocket(url, protocols) {
2738
+ const started = nowMs();
2739
+ const sanitizedUrl = sanitizeHttpUrl(String(url), options.sanitizeUrl);
2740
+ const socket = new original(url, protocols);
2741
+ const captureLifecycle = (event, extra = {}, error) => {
2742
+ if (!shouldSample(sampleRate, random)) return;
2743
+ capture({
2744
+ level: event === "error" ? "error" : level,
2745
+ message: `WebSocket ${event} ${sanitizedUrl}`,
2746
+ error,
2747
+ props: { websocket: {
2748
+ kind: "websocket",
2749
+ event,
2750
+ url: sanitizedUrl,
2751
+ durationMs: durationMs(started),
2752
+ ...extra
2753
+ } }
2754
+ });
2755
+ };
2756
+ if (captureConnect) captureLifecycle("connect");
2757
+ if (captureOpen) socket.addEventListener?.("open", () => captureLifecycle("open"));
2758
+ if (captureClose) socket.addEventListener?.("close", (event) => captureLifecycle("close", closeEventPayload(event)));
2759
+ if (captureError) socket.addEventListener?.("error", (event) => captureLifecycle("error", { type: event.type }, event));
2760
+ if (captureMessages) socket.addEventListener?.("message", (event) => {
2761
+ const message = event;
2762
+ capture({
2763
+ level,
2764
+ message: `WebSocket message ${sanitizedUrl}`,
2765
+ props: { websocket: {
2766
+ kind: "websocket-message",
2767
+ url: sanitizedUrl,
2768
+ message: messagePayload("incoming", message.data, captureMessageData)
2769
+ } }
2770
+ });
2771
+ });
2772
+ if (captureSentMessages && socket.send) {
2773
+ const originalSend = socket.send.bind(socket);
2774
+ socket.send = (data) => {
2775
+ capture({
2776
+ level,
2777
+ message: `WebSocket send ${sanitizedUrl}`,
2778
+ props: { websocket: {
2779
+ kind: "websocket-message",
2780
+ url: sanitizedUrl,
2781
+ message: messagePayload("outgoing", data, captureMessageData)
2782
+ } }
2783
+ });
2784
+ return originalSend(data);
2785
+ };
2786
+ }
2787
+ return socket;
2788
+ };
2789
+ PatchedWebSocket.prototype = original.prototype;
2790
+ copyStaticConstants(PatchedWebSocket, original);
2791
+ globalThis.WebSocket = PatchedWebSocket;
2792
+ return () => {
2793
+ globalThis.WebSocket = current;
2794
+ };
2795
+ }
2796
+ };
2797
+ }
2798
+ //#endregion
2799
+ //#region src/web-vitals-integration.ts
2800
+ const defaultMetrics = [
2801
+ "CLS",
2802
+ "FCP",
2803
+ "INP",
2804
+ "LCP",
2805
+ "TTFB"
2806
+ ];
2807
+ const thresholds = {
2808
+ CLS: [.1, .25],
2809
+ FCP: [1800, 3e3],
2810
+ INP: [200, 500],
2811
+ LCP: [2500, 4e3],
2812
+ TTFB: [800, 1800]
2813
+ };
2814
+ function ratingFor(name, value) {
2815
+ const [good, needsImprovement] = thresholds[name];
2816
+ if (value <= good) return "good";
2817
+ if (value <= needsImprovement) return "needs-improvement";
2818
+ return "poor";
2819
+ }
2820
+ function metricId(name) {
2821
+ return `${name.toLowerCase()}-${Date.now().toString(36)}`;
2822
+ }
2823
+ function entryValue(entry, field) {
2824
+ const value = entry[field];
2825
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
2826
+ }
2827
+ function hasMetric(metrics, name) {
2828
+ return metrics.includes(name);
2829
+ }
2830
+ function supportsEntryType$1(Observer, type) {
2831
+ const supported = Observer?.supportedEntryTypes;
2832
+ return !supported || supported.includes(type);
2833
+ }
2834
+ function captureWebVitalsIntegration(options = {}) {
2835
+ const metrics = options.metrics ?? defaultMetrics;
2836
+ const level = options.level ?? "info";
2837
+ const reportAllChanges = options.reportAllChanges ?? false;
2838
+ const flushOnHidden = options.flushOnHidden ?? true;
2839
+ return {
2840
+ name: "capture-web-vitals",
2841
+ setup(api) {
2842
+ const Observer = options.PerformanceObserver ?? globalThis.PerformanceObserver;
2843
+ const perf = options.performance ?? globalThis.performance;
2844
+ const add = options.addEventListener ?? globalThis.addEventListener?.bind(globalThis);
2845
+ const remove = options.removeEventListener ?? globalThis.removeEventListener?.bind(globalThis);
2846
+ const disposers = [];
2847
+ const currentValues = /* @__PURE__ */ new Map();
2848
+ const reportedValues = /* @__PURE__ */ new Map();
2849
+ const emitted = /* @__PURE__ */ new Set();
2850
+ let disposed = false;
2851
+ const emit = (name, value, final) => {
2852
+ if (!Number.isFinite(value)) return;
2853
+ const finalKey = `${name}:final`;
2854
+ if (final && emitted.has(finalKey)) return;
2855
+ const previous = reportedValues.get(name) ?? 0;
2856
+ currentValues.set(name, value);
2857
+ reportedValues.set(name, value);
2858
+ if (final) emitted.add(finalKey);
2859
+ const metric = {
2860
+ name,
2861
+ value,
2862
+ delta: value - previous,
2863
+ rating: ratingFor(name, value),
2864
+ id: metricId(name),
2865
+ final
2866
+ };
2867
+ api.capture({
2868
+ level,
2869
+ message: `Web vital ${metric.name} ${metric.value}`,
2870
+ props: { webVital: metric }
2871
+ });
2872
+ };
2873
+ const observe = (type, callback, init = {
2874
+ type,
2875
+ buffered: true
2876
+ }) => {
2877
+ if (!Observer || !supportsEntryType$1(Observer, type)) return;
2878
+ try {
2879
+ const observer = new Observer((list) => {
2880
+ for (const entry of list.getEntries()) callback(entry);
2881
+ });
2882
+ observer.observe(init);
2883
+ disposers.push(() => observer.disconnect());
2884
+ } catch {}
2885
+ };
2886
+ if (hasMetric(metrics, "TTFB") && perf) {
2887
+ const navigation = perf.getEntriesByType("navigation")[0];
2888
+ if (navigation) {
2889
+ const responseStart = entryValue(navigation, "responseStart");
2890
+ const startTime = entryValue(navigation, "startTime") ?? 0;
2891
+ if (responseStart !== void 0) emit("TTFB", responseStart - startTime, true);
2892
+ }
2893
+ }
2894
+ if (hasMetric(metrics, "FCP") && perf) {
2895
+ const existing = perf.getEntriesByName("first-contentful-paint")[0];
2896
+ if (existing) emit("FCP", existing.startTime, true);
2897
+ observe("paint", (entry) => {
2898
+ if (entry.name === "first-contentful-paint") emit("FCP", entry.startTime, true);
2899
+ });
2900
+ }
2901
+ if (hasMetric(metrics, "CLS")) observe("layout-shift", (entry) => {
2902
+ if (entry.hadRecentInput === true) return;
2903
+ const value = entryValue(entry, "value") ?? 0;
2904
+ const next = (currentValues.get("CLS") ?? 0) + value;
2905
+ if (reportAllChanges) emit("CLS", next, false);
2906
+ else currentValues.set("CLS", next);
2907
+ });
2908
+ if (hasMetric(metrics, "LCP")) observe("largest-contentful-paint", (entry) => {
2909
+ currentValues.set("LCP", entry.startTime);
2910
+ if (reportAllChanges) emit("LCP", entry.startTime, false);
2911
+ });
2912
+ if (hasMetric(metrics, "INP")) observe("event", (entry) => {
2913
+ const duration = entryValue(entry, "duration") ?? 0;
2914
+ if (duration <= (currentValues.get("INP") ?? 0)) return;
2915
+ currentValues.set("INP", duration);
2916
+ if (reportAllChanges) emit("INP", duration, false);
2917
+ }, {
2918
+ type: "event",
2919
+ buffered: true,
2920
+ durationThreshold: 40
2921
+ });
2922
+ const flushFinal = api.guard(() => {
2923
+ const cls = currentValues.get("CLS");
2924
+ if (cls !== void 0 && cls > 0) emit("CLS", cls, true);
2925
+ const lcp = currentValues.get("LCP");
2926
+ if (lcp !== void 0) emit("LCP", lcp, true);
2927
+ const inp = currentValues.get("INP");
2928
+ if (inp !== void 0) emit("INP", inp, true);
2929
+ });
2930
+ if (flushOnHidden && add && remove) {
2931
+ const onPageHide = () => flushFinal();
2932
+ const onVisibilityChange = () => {
2933
+ if (typeof document !== "undefined" && document.visibilityState === "hidden") flushFinal();
2934
+ };
2935
+ add("pagehide", onPageHide);
2936
+ add("visibilitychange", onVisibilityChange);
2937
+ disposers.push(() => {
2938
+ remove("pagehide", onPageHide);
2939
+ remove("visibilitychange", onVisibilityChange);
2940
+ });
2941
+ }
2942
+ return () => {
2943
+ if (disposed) return;
2944
+ disposed = true;
2945
+ flushFinal();
2946
+ for (const dispose of disposers) dispose();
2947
+ };
2948
+ }
2949
+ };
2950
+ }
2951
+ //#endregion
2952
+ //#region src/performance-integration.ts
2953
+ const defaultEntryTypes = [
2954
+ "navigation",
2955
+ "resource",
2956
+ "longtask",
2957
+ "measure",
2958
+ "mark"
2959
+ ];
2960
+ const numberFields = [
2961
+ "transferSize",
2962
+ "encodedBodySize",
2963
+ "decodedBodySize",
2964
+ "workerStart",
2965
+ "redirectStart",
2966
+ "redirectEnd",
2967
+ "fetchStart",
2968
+ "domainLookupStart",
2969
+ "domainLookupEnd",
2970
+ "connectStart",
2971
+ "connectEnd",
2972
+ "requestStart",
2973
+ "responseStart",
2974
+ "responseEnd"
2975
+ ];
2976
+ function supportsEntryType(Observer, type) {
2977
+ const supported = Observer?.supportedEntryTypes;
2978
+ return !supported || supported.includes(type);
2979
+ }
2980
+ function finiteNumber(value) {
2981
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
2982
+ }
2983
+ function stringValue(value) {
2984
+ return typeof value === "string" && value ? value : void 0;
2985
+ }
2986
+ function normalizeBrowserPerformanceEntry(entry, options = {}) {
2987
+ const raw = entry;
2988
+ const payload = {
2989
+ name: options.sanitizeName ? options.sanitizeName(entry.name, entry.entryType) : entry.name,
2990
+ entryType: entry.entryType,
2991
+ startTime: entry.startTime,
2992
+ duration: entry.duration
2993
+ };
2994
+ for (const field of numberFields) {
2995
+ const value = finiteNumber(raw[field]);
2996
+ if (value !== void 0) payload[field] = value;
2997
+ }
2998
+ const responseStatus = finiteNumber(raw.responseStatus);
2999
+ if (responseStatus !== void 0) payload.responseStatus = responseStatus;
3000
+ const initiatorType = stringValue(raw.initiatorType);
3001
+ if (initiatorType) payload.initiatorType = initiatorType;
3002
+ const nextHopProtocol = stringValue(raw.nextHopProtocol);
3003
+ if (nextHopProtocol) payload.nextHopProtocol = nextHopProtocol;
3004
+ const renderBlockingStatus = stringValue(raw.renderBlockingStatus);
3005
+ if (renderBlockingStatus) payload.renderBlockingStatus = renderBlockingStatus;
3006
+ if (options.captureDetail && raw.detail !== void 0) payload.detail = raw.detail;
3007
+ return payload;
3008
+ }
3009
+ function minDurationFor(value, entryType) {
3010
+ if (typeof value === "number") return value;
3011
+ return value?.[entryType] ?? 0;
3012
+ }
3013
+ function levelFor(value, entry) {
3014
+ if (typeof value === "function") return value(entry);
3015
+ if (value) return value;
3016
+ return entry.entryType === "longtask" ? "warn" : "info";
3017
+ }
3018
+ function entryKey(entry) {
3019
+ return `${entry.entryType}:${entry.name}:${entry.startTime}:${entry.duration}`;
3020
+ }
3021
+ function capturePerformanceIntegration(options = {}) {
3022
+ const entryTypes = options.entryTypes ?? defaultEntryTypes;
3023
+ const buffered = options.buffered ?? true;
3024
+ const emitExisting = options.emitExisting ?? true;
3025
+ const maxEntries = Math.max(0, Math.floor(options.maxEntries ?? 200));
3026
+ const sampleRate = options.sampleRate ?? 1;
3027
+ const random = options.random ?? Math.random;
3028
+ return {
3029
+ name: "capture-performance",
3030
+ setup(api) {
3031
+ const Observer = options.PerformanceObserver ?? globalThis.PerformanceObserver;
3032
+ const perf = options.performance ?? globalThis.performance;
3033
+ const seen = /* @__PURE__ */ new Set();
3034
+ const disposers = [];
3035
+ let emitted = 0;
3036
+ let disposed = false;
3037
+ const capture = api.guard((entry) => {
3038
+ if (disposed || emitted >= maxEntries) return;
3039
+ const payload = normalizeBrowserPerformanceEntry(entry, options);
3040
+ if (payload.duration < minDurationFor(options.minDurationMs, payload.entryType)) return;
3041
+ if (sampleRate < 1 && random() >= sampleRate) return;
3042
+ if (options.ignore?.(payload)) return;
3043
+ const key = entryKey(payload);
3044
+ if (seen.has(key)) return;
3045
+ seen.add(key);
3046
+ emitted += 1;
3047
+ api.capture({
3048
+ level: levelFor(options.level, payload),
3049
+ message: `Performance ${payload.entryType} ${payload.name}`,
3050
+ props: { performance: payload }
3051
+ });
3052
+ });
3053
+ const emitExistingEntries = (type) => {
3054
+ if (!emitExisting || !perf) return;
3055
+ try {
3056
+ for (const entry of perf.getEntriesByType(type)) capture(entry);
3057
+ } catch {}
3058
+ };
3059
+ for (const type of entryTypes) {
3060
+ emitExistingEntries(type);
3061
+ if (!Observer || !supportsEntryType(Observer, type)) continue;
3062
+ try {
3063
+ const observer = new Observer((list) => {
3064
+ for (const entry of list.getEntries()) capture(entry);
3065
+ });
3066
+ observer.observe({
3067
+ type,
3068
+ buffered
3069
+ });
3070
+ disposers.push(() => observer.disconnect());
3071
+ } catch {}
3072
+ }
3073
+ return () => {
3074
+ if (disposed) return;
3075
+ disposed = true;
3076
+ for (const dispose of disposers) dispose();
3077
+ };
3078
+ }
3079
+ };
3080
+ }
3081
+ //#endregion
3082
+ //#region src/page-lifecycle.ts
3083
+ function pageLifecycleIntegration(options = {}) {
3084
+ const flushOnPageHide = options.flushOnPageHide ?? true;
3085
+ const flushOnHidden = options.flushOnHidden ?? true;
3086
+ const coalesceMs = options.coalesceMs ?? 250;
3087
+ return {
3088
+ name: "page-lifecycle",
3089
+ setup(api) {
3090
+ if (typeof addEventListener === "undefined") return;
3091
+ let disposed = false;
3092
+ let flushInFlight;
3093
+ let lastFlushAt = 0;
3094
+ const flush = () => {
3095
+ const now = Date.now();
3096
+ if (flushInFlight || now - lastFlushAt < coalesceMs) return;
3097
+ lastFlushAt = now;
3098
+ flushInFlight = Promise.resolve(api.flush()).catch(() => {}).finally(() => {
3099
+ flushInFlight = void 0;
3100
+ });
3101
+ };
3102
+ const onVisibility = () => {
3103
+ if (typeof document !== "undefined" && document.visibilityState === "hidden") flush();
3104
+ };
3105
+ if (flushOnPageHide) addEventListener("pagehide", flush);
3106
+ if (flushOnHidden) addEventListener("visibilitychange", onVisibility);
3107
+ return () => {
3108
+ if (disposed) return;
3109
+ disposed = true;
3110
+ if (flushOnPageHide) removeEventListener("pagehide", flush);
3111
+ if (flushOnHidden) removeEventListener("visibilitychange", onVisibility);
3112
+ };
3113
+ }
3114
+ };
3115
+ }
3116
+ //#endregion
3117
+ export { browserBroadcastChannelTransport, browserHttpTransport, browserServiceWorkerTransport, browserWebSocketTransport, captureBrowserErrorsIntegration, captureConsoleIntegration, captureFetchIntegration, captureFrameworkErrorsIntegration, capturePerformanceIntegration, captureReportingIntegration, captureRouterIntegration, captureRuntimeHostIntegration, captureServiceWorkerIntegration, captureUserActionsIntegration, captureWebSocketIntegration, captureWebVitalsIntegration, captureXHRIntegration, createLogZipBlob, downloadBlob, exportLogsToZip, indexedDbBrowserHttpOfflineQueue, indexedDbTransport, memoryBrowserHttpOfflineQueue, normalizeBrowserPerformanceEntry, pageLifecycleIntegration };
3118
+
3119
+ //# sourceMappingURL=index.js.map