@loggerjs/browser 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -186,8 +186,9 @@ function browserHttpTransport(options) {
186
186
  let flushing = false;
187
187
  let replayingOffline = false;
188
188
  let lastContext;
189
- const headers = () => ({
190
- "content-type": codec.contentType,
189
+ const headers = (payloadHeaders, contentType = codec.contentType) => ({
190
+ "content-type": contentType,
191
+ ...payloadHeaders,
191
192
  ...options.headers
192
193
  });
193
194
  const clearTimer = () => {
@@ -268,33 +269,40 @@ function browserHttpTransport(options) {
268
269
  });
269
270
  if (!response.ok) throw new Error(`browserHttpTransport failed with status ${response.status}`);
270
271
  };
271
- const createOfflineEntry = (payload) => ({
272
+ const encodeTransformedPayload = async (batch) => {
273
+ return (0, _loggerjs_core.applyPayloadTransforms)(codec.encode(batch), {
274
+ contentType: codec.contentType,
275
+ events: batch,
276
+ transport: options.name ?? "browser-http"
277
+ }, options.transformPayload);
278
+ };
279
+ const createOfflineEntry = (payload, payloadHeaders, contentType) => ({
272
280
  id: `${Date.now().toString(36)}-${(offlineEntrySeq++).toString(36)}`,
273
281
  url: options.url,
274
282
  method: options.method ?? "POST",
275
- headers: headers(),
283
+ headers: headers(payloadHeaders, contentType),
276
284
  body: payload,
277
285
  credentials: options.credentials,
278
286
  keepalive: options.keepalive ?? true,
279
287
  createdAt: Date.now()
280
288
  });
281
- const enqueueOfflinePayload = async (payload) => {
289
+ const enqueueOfflinePayload = async (payload, payloadHeaders, contentType) => {
282
290
  if (!offlineQueue) return false;
283
- await offlineQueue.enqueue(createOfflineEntry(payload));
291
+ await offlineQueue.enqueue(createOfflineEntry(payload, payloadHeaders, contentType));
284
292
  (0, _loggerjs_core.incrementLoggerMetaCounter)("transport.offline.queued");
285
293
  return true;
286
294
  };
287
295
  const sendFetchBatch = async (batch) => {
288
296
  if (batch.length === 0) return;
289
- const payload = codec.encode(batch);
297
+ const transformed = await encodeTransformedPayload(batch);
290
298
  if (offlineQueue && typeof navigator !== "undefined" && navigator.onLine === false) {
291
- await enqueueOfflinePayload(payload);
299
+ await enqueueOfflinePayload(transformed.payload, transformed.headers, transformed.contentType);
292
300
  return;
293
301
  }
294
302
  try {
295
- await sendPayload(createOfflineEntry(payload));
303
+ await sendPayload(createOfflineEntry(transformed.payload, transformed.headers, transformed.contentType));
296
304
  } catch (error) {
297
- if (await enqueueOfflinePayload(payload)) return;
305
+ if (await enqueueOfflinePayload(transformed.payload, transformed.headers, transformed.contentType)) return;
298
306
  throw error;
299
307
  }
300
308
  };
@@ -330,14 +338,18 @@ function browserHttpTransport(options) {
330
338
  flushing = true;
331
339
  clearTimer();
332
340
  const batch = queue.splice(0, queue.length);
341
+ let pendingBatch = batch;
333
342
  try {
334
- let fetchBatch = batch;
335
- if (preferBeacon) {
343
+ if (preferBeacon && !options.transformPayload) {
336
344
  const beaconResult = sendBeaconBatch(batch);
337
345
  if (beaconResult.ok || beaconResult.remaining.length === 0) return;
338
- fetchBatch = beaconResult.remaining;
346
+ pendingBatch = beaconResult.remaining;
339
347
  }
340
- await sendFetchBatch(fetchBatch);
348
+ await sendFetchBatch(pendingBatch);
349
+ pendingBatch = [];
350
+ } catch (error) {
351
+ if (pendingBatch.length > 0) queue.unshift(...pendingBatch);
352
+ throw error;
341
353
  } finally {
342
354
  flushing = false;
343
355
  if (queue.length > 0) schedule();
@@ -393,6 +405,31 @@ function browserHttpTransport(options) {
393
405
  };
394
406
  }
395
407
  //#endregion
408
+ //#region src/payload-transforms.ts
409
+ async function compressWithStream(payload, format, streamFactory) {
410
+ const stream = (streamFactory ?? ((nextFormat) => {
411
+ if (typeof CompressionStream === "undefined") throw new Error("CompressionStream is not available in this browser");
412
+ return new CompressionStream(nextFormat);
413
+ }))(format);
414
+ const writer = stream.writable.getWriter();
415
+ await writer.write(Uint8Array.from(payload));
416
+ await writer.close();
417
+ return new Uint8Array(await new Response(stream.readable).arrayBuffer());
418
+ }
419
+ function browserCompressionPayloadTransform(options = {}) {
420
+ const format = options.format ?? "gzip";
421
+ return async (payload, context) => ({
422
+ payload: await (options.compress ? options.compress((0, _loggerjs_core.encodedPayloadToUint8Array)(payload), {
423
+ ...context,
424
+ format
425
+ }) : compressWithStream((0, _loggerjs_core.encodedPayloadToUint8Array)(payload), format, options.streamFactory)),
426
+ headers: {
427
+ "content-encoding": format,
428
+ ...options.headers
429
+ }
430
+ });
431
+ }
432
+ //#endregion
396
433
  //#region src/service-worker-transport.ts
397
434
  let sourceSequence = 0;
398
435
  function createSourceId() {
@@ -1330,6 +1367,14 @@ function indexedDbTransport(options = {}) {
1330
1367
  clearTimer();
1331
1368
  await withStore("readwrite", (store) => requestToPromise(store.clear()));
1332
1369
  },
1370
+ async remove(ids) {
1371
+ await flushPending();
1372
+ const items = typeof ids === "string" ? [ids] : ids;
1373
+ if (items.length === 0) return;
1374
+ await withStore("readwrite", async (store) => {
1375
+ for (const id of items) await requestToPromise(store.delete(id));
1376
+ });
1377
+ },
1333
1378
  async *query(queryOptions = {}) {
1334
1379
  await flushPending();
1335
1380
  const startedAt = nowMs$1();
@@ -1361,6 +1406,150 @@ function indexedDbTransport(options = {}) {
1361
1406
  };
1362
1407
  }
1363
1408
  //#endregion
1409
+ //#region src/offline-first-transport.ts
1410
+ function normalizeBatchSize(value) {
1411
+ if (value === void 0 || !Number.isFinite(value) || value <= 0) return 100;
1412
+ return Math.floor(value);
1413
+ }
1414
+ async function collectBatch(queue, replayBatchSize) {
1415
+ const events = [];
1416
+ for await (const event of queue.query({
1417
+ order: "asc",
1418
+ limit: replayBatchSize
1419
+ })) events.push(event);
1420
+ return events;
1421
+ }
1422
+ function isBrowserOnline(options) {
1423
+ if (options.online) return options.online();
1424
+ if (typeof navigator === "undefined") return true;
1425
+ return navigator.onLine !== false;
1426
+ }
1427
+ function offlineFirstTransport(remote, options = {}) {
1428
+ const name = options.name ?? `offline-first(${remote.name ?? "transport"})`;
1429
+ const replayBatchSize = normalizeBatchSize(options.replayBatchSize);
1430
+ const queue = options.queue ?? indexedDbTransport({
1431
+ dbName: "loggerjs-offline",
1432
+ flushIntervalMs: 0,
1433
+ name: `${name}:queue`,
1434
+ storeName: "offline-events",
1435
+ ...options.queueOptions
1436
+ });
1437
+ const reliableRemote = (0, _loggerjs_core.retryTransport)(remote, {
1438
+ maxRetries: 2,
1439
+ retryBaseDelayMs: 250,
1440
+ retryMaxDelayMs: 5e3,
1441
+ ...options.retry,
1442
+ name: `${name}:remote`
1443
+ });
1444
+ let lastContext;
1445
+ let replayPromise;
1446
+ const replayContext = {
1447
+ loggerName: name,
1448
+ now: () => Date.now(),
1449
+ toEvent: _loggerjs_core.recordToEvent,
1450
+ reportInternalError() {}
1451
+ };
1452
+ const contextForReplay = () => lastContext ?? replayContext;
1453
+ const reportInternalError = (error, operation) => {
1454
+ contextForReplay().reportInternalError(error, {
1455
+ phase: "transport",
1456
+ transport: name,
1457
+ operation
1458
+ });
1459
+ };
1460
+ const queueEvent = async (event, context, error) => {
1461
+ await queue.log(event, context);
1462
+ await queue.flush?.();
1463
+ (0, _loggerjs_core.incrementLoggerMetaCounter)("transport.offline.queued");
1464
+ options.onQueued?.(event, error);
1465
+ };
1466
+ const sendRemoteBatch = async (events, context) => {
1467
+ await reliableRemote.logBatch?.([...events], context);
1468
+ };
1469
+ const sendOrQueue = async (event, context) => {
1470
+ lastContext = context;
1471
+ if (!isBrowserOnline(options)) {
1472
+ await queueEvent(event, context);
1473
+ return;
1474
+ }
1475
+ try {
1476
+ await reliableRemote.log?.(event, context);
1477
+ } catch (error) {
1478
+ (0, _loggerjs_core.incrementLoggerMetaCounter)("transport.offline.remote.failed");
1479
+ await queueEvent(event, context, error);
1480
+ }
1481
+ };
1482
+ const replay = async () => {
1483
+ if (replayPromise) return replayPromise;
1484
+ replayPromise = (async () => {
1485
+ if (!isBrowserOnline(options)) return;
1486
+ const context = contextForReplay();
1487
+ await queue.flush?.();
1488
+ for (;;) {
1489
+ const batch = await collectBatch(queue, replayBatchSize);
1490
+ if (batch.length === 0) return;
1491
+ try {
1492
+ await sendRemoteBatch(batch, context);
1493
+ await queue.remove(batch.map((event) => event.id));
1494
+ (0, _loggerjs_core.incrementLoggerMetaCounter)("transport.offline.replayed", batch.length);
1495
+ options.onReplayed?.(batch);
1496
+ } catch (error) {
1497
+ (0, _loggerjs_core.incrementLoggerMetaCounter)("transport.offline.replay.failed");
1498
+ reportInternalError(error, "replay");
1499
+ throw error;
1500
+ }
1501
+ }
1502
+ })().finally(() => {
1503
+ replayPromise = void 0;
1504
+ });
1505
+ return replayPromise;
1506
+ };
1507
+ const onOnline = () => {
1508
+ replay().catch((error) => reportInternalError(error, "online-replay"));
1509
+ };
1510
+ if (options.replayOnOnline ?? true) globalThis.addEventListener?.("online", onOnline);
1511
+ return {
1512
+ name,
1513
+ minLevel: options.minLevel ?? remote.minLevel,
1514
+ queue,
1515
+ write(record, context) {
1516
+ return sendOrQueue(context.toEvent(record), context);
1517
+ },
1518
+ writeBatch(records, context) {
1519
+ return Promise.all(records.map((record) => sendOrQueue(context.toEvent(record), context))).then(() => void 0);
1520
+ },
1521
+ log(event, context) {
1522
+ if (options.minLevel !== void 0 && event.level < (0, _loggerjs_core.toLevelValue)(options.minLevel)) return;
1523
+ return sendOrQueue(event, context);
1524
+ },
1525
+ logBatch(events, context) {
1526
+ return Promise.all(events.map((event) => options.minLevel !== void 0 && event.level < (0, _loggerjs_core.toLevelValue)(options.minLevel) ? void 0 : sendOrQueue(event, context))).then(() => void 0);
1527
+ },
1528
+ replay,
1529
+ async flush() {
1530
+ await replay();
1531
+ await reliableRemote.flush?.();
1532
+ await queue.flush?.();
1533
+ },
1534
+ flushSync() {
1535
+ reliableRemote.flushSync?.();
1536
+ },
1537
+ async close() {
1538
+ globalThis.removeEventListener?.("online", onOnline);
1539
+ await replay();
1540
+ await reliableRemote.close?.();
1541
+ await queue.close?.();
1542
+ },
1543
+ async queuedCount() {
1544
+ await queue.flush?.();
1545
+ return queue.count?.();
1546
+ },
1547
+ async clearQueue() {
1548
+ await queue.clear?.();
1549
+ }
1550
+ };
1551
+ }
1552
+ //#endregion
1364
1553
  //#region src/zip-export.ts
1365
1554
  const ZIP_UTF8_FLAG = 2048;
1366
1555
  const ZIP_STORE_METHOD = 0;
@@ -1688,6 +1877,79 @@ function captureConsoleIntegration(options = {}) {
1688
1877
  };
1689
1878
  }
1690
1879
  //#endregion
1880
+ //#region src/context-propagation-integration.ts
1881
+ const defaultActionEvents = [
1882
+ "click",
1883
+ "input",
1884
+ "change",
1885
+ "submit"
1886
+ ];
1887
+ let fallbackIdSeq = 0;
1888
+ function defaultIdFactory() {
1889
+ const cryptoRandomUUID = globalThis.crypto?.randomUUID;
1890
+ if (cryptoRandomUUID) return cryptoRandomUUID.call(globalThis.crypto);
1891
+ return `${Date.now().toString(36)}-${(fallbackIdSeq++).toString(36)}`;
1892
+ }
1893
+ function resolveValue(value) {
1894
+ return typeof value === "function" ? value() : value;
1895
+ }
1896
+ function normalizeBaggage(value) {
1897
+ if (typeof value === "string") return (0, _loggerjs_core.parseBaggage)(value);
1898
+ return value;
1899
+ }
1900
+ function actionTargetName(target) {
1901
+ const item = target;
1902
+ if (!item) return void 0;
1903
+ return item.id || item.name || item.tagName?.toLowerCase();
1904
+ }
1905
+ function browserContextPropagationIntegration(options = {}) {
1906
+ const actionEvents = options.actionEvents ?? defaultActionEvents;
1907
+ const actionTtlMs = Math.max(0, Math.floor(options.actionTtlMs ?? 5e3));
1908
+ const clock = options.clock ?? Date.now;
1909
+ const idFactory = options.idFactory ?? defaultIdFactory;
1910
+ let lastAction;
1911
+ return {
1912
+ name: "browser-context-propagation",
1913
+ setup() {
1914
+ const root = options.root ?? globalThis.document;
1915
+ const disposeProvider = (0, _loggerjs_core.addContextProvider)(() => {
1916
+ const sessionId = resolveValue(options.sessionId);
1917
+ const requestId = options.requestId?.();
1918
+ const trace = resolveValue(options.trace) ?? (0, _loggerjs_core.parseTraceparent)(resolveValue(options.traceparent));
1919
+ const baggage = normalizeBaggage(resolveValue(options.baggage)) ?? trace?.baggage;
1920
+ const traceparent = (0, _loggerjs_core.formatTraceparent)(trace);
1921
+ const now = clock();
1922
+ const action = lastAction && now - lastAction.time <= actionTtlMs ? {
1923
+ actionId: lastAction.id,
1924
+ actionType: lastAction.type,
1925
+ actionTarget: lastAction.target
1926
+ } : void 0;
1927
+ return {
1928
+ sessionId,
1929
+ requestId,
1930
+ traceparent,
1931
+ baggage: (0, _loggerjs_core.formatBaggage)(baggage),
1932
+ ...action
1933
+ };
1934
+ });
1935
+ if (!root?.addEventListener || !root.removeEventListener || actionEvents.length === 0) return disposeProvider;
1936
+ const onAction = (event) => {
1937
+ lastAction = {
1938
+ id: idFactory(),
1939
+ type: event.type,
1940
+ target: actionTargetName(event.target),
1941
+ time: clock()
1942
+ };
1943
+ };
1944
+ for (const event of actionEvents) root.addEventListener(event, onAction, true);
1945
+ return () => {
1946
+ for (const event of actionEvents) root.removeEventListener(event, onAction, true);
1947
+ disposeProvider();
1948
+ };
1949
+ }
1950
+ };
1951
+ }
1952
+ //#endregion
1691
1953
  //#region src/error-integration.ts
1692
1954
  function resourceInfo(target) {
1693
1955
  const element = target;
@@ -2098,6 +2360,86 @@ function captureFrameworkErrorsIntegration(options = {}) {
2098
2360
  };
2099
2361
  }
2100
2362
  //#endregion
2363
+ //#region src/framework-router-integrations.ts
2364
+ function routePath(input) {
2365
+ if (typeof input === "string") return input;
2366
+ if (input && typeof input === "object") {
2367
+ const route = input;
2368
+ if (typeof route.fullPath === "string") return route.fullPath;
2369
+ if (typeof route.pathname === "string") return route.pathname;
2370
+ if (typeof route.path === "string") return route.path;
2371
+ if (route.location) return routePath(route.location);
2372
+ }
2373
+ return "";
2374
+ }
2375
+ function emitRouteChange(api, framework, level, to, from, options) {
2376
+ const sanitize = options.sanitizeUrl;
2377
+ const toPath = sanitize ? sanitize(routePath(to)) : routePath(to);
2378
+ const fromPath = from === void 0 ? void 0 : sanitize ? sanitize(routePath(from)) : routePath(from);
2379
+ api.capture({
2380
+ level,
2381
+ message: `${framework} route ${toPath}`,
2382
+ props: { browser: {
2383
+ kind: "framework-route-change",
2384
+ framework,
2385
+ route: {
2386
+ to: toPath,
2387
+ from: fromPath,
2388
+ state: options.includeState ? (0, _loggerjs_core.normalizeValue)(to, { maxDepth: 4 }) : void 0
2389
+ }
2390
+ } }
2391
+ });
2392
+ }
2393
+ function nextRouterIntegration(options) {
2394
+ const level = options.level ?? "info";
2395
+ return {
2396
+ name: "next-router",
2397
+ setup(api) {
2398
+ const router = options.router;
2399
+ if (!router.events?.on || !router.events.off) return;
2400
+ const onRoute = api.guard((url) => emitRouteChange(api, "next", level, url, router.asPath, options));
2401
+ router.events.on("routeChangeComplete", onRoute);
2402
+ router.events.on("hashChangeComplete", onRoute);
2403
+ return () => {
2404
+ router.events?.off?.("routeChangeComplete", onRoute);
2405
+ router.events?.off?.("hashChangeComplete", onRoute);
2406
+ };
2407
+ }
2408
+ };
2409
+ }
2410
+ function reactRouterIntegration(options) {
2411
+ const level = options.level ?? "info";
2412
+ return {
2413
+ name: "react-router",
2414
+ setup(api) {
2415
+ let previous = options.history.location;
2416
+ const dispose = options.history.listen?.(api.guard((update) => {
2417
+ const next = update?.location ?? update;
2418
+ emitRouteChange(api, "react-router", level, next, previous, options);
2419
+ previous = next;
2420
+ }));
2421
+ return typeof dispose === "function" ? dispose : void 0;
2422
+ }
2423
+ };
2424
+ }
2425
+ function vueRouterIntegration(options) {
2426
+ const level = options.level ?? "info";
2427
+ const framework = options.framework ?? "vue-router";
2428
+ return {
2429
+ name: framework,
2430
+ setup(api) {
2431
+ const dispose = options.router.afterEach?.(api.guard((to, from) => emitRouteChange(api, framework, level, to, from, options)));
2432
+ return typeof dispose === "function" ? dispose : void 0;
2433
+ }
2434
+ };
2435
+ }
2436
+ function nuxtRouterIntegration(options) {
2437
+ return vueRouterIntegration({
2438
+ ...options,
2439
+ framework: "nuxt"
2440
+ });
2441
+ }
2442
+ //#endregion
2101
2443
  //#region src/reporting-integration.ts
2102
2444
  const defaultReportTypes = [
2103
2445
  "csp-violation",
@@ -3115,6 +3457,8 @@ function pageLifecycleIntegration(options = {}) {
3115
3457
  }
3116
3458
  //#endregion
3117
3459
  exports.browserBroadcastChannelTransport = browserBroadcastChannelTransport;
3460
+ exports.browserCompressionPayloadTransform = browserCompressionPayloadTransform;
3461
+ exports.browserContextPropagationIntegration = browserContextPropagationIntegration;
3118
3462
  exports.browserHttpTransport = browserHttpTransport;
3119
3463
  exports.browserServiceWorkerTransport = browserServiceWorkerTransport;
3120
3464
  exports.browserWebSocketTransport = browserWebSocketTransport;
@@ -3137,8 +3481,13 @@ exports.exportLogsToZip = exportLogsToZip;
3137
3481
  exports.indexedDbBrowserHttpOfflineQueue = indexedDbBrowserHttpOfflineQueue;
3138
3482
  exports.indexedDbTransport = indexedDbTransport;
3139
3483
  exports.memoryBrowserHttpOfflineQueue = memoryBrowserHttpOfflineQueue;
3484
+ exports.nextRouterIntegration = nextRouterIntegration;
3140
3485
  exports.normalizeBrowserPerformanceEntry = normalizeBrowserPerformanceEntry;
3486
+ exports.nuxtRouterIntegration = nuxtRouterIntegration;
3487
+ exports.offlineFirstTransport = offlineFirstTransport;
3141
3488
  exports.pageLifecycleIntegration = pageLifecycleIntegration;
3489
+ exports.reactRouterIntegration = reactRouterIntegration;
3490
+ exports.vueRouterIntegration = vueRouterIntegration;
3142
3491
  Object.keys(_loggerjs_core).forEach(function(k) {
3143
3492
  if (k !== "default" && !Object.prototype.hasOwnProperty.call(exports, k)) Object.defineProperty(exports, k, {
3144
3493
  enumerable: true,