@loggerjs/browser 0.5.0 → 0.5.2

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 (103) hide show
  1. package/dist/broadcast-channel-transport.cjs +115 -0
  2. package/dist/broadcast-channel-transport.cjs.map +1 -0
  3. package/dist/broadcast-channel-transport.js +114 -0
  4. package/dist/broadcast-channel-transport.js.map +1 -0
  5. package/dist/console-integration.cjs +98 -0
  6. package/dist/console-integration.cjs.map +1 -0
  7. package/dist/console-integration.js +97 -0
  8. package/dist/console-integration.js.map +1 -0
  9. package/dist/context-propagation-integration.cjs +78 -0
  10. package/dist/context-propagation-integration.cjs.map +1 -0
  11. package/dist/context-propagation-integration.js +77 -0
  12. package/dist/context-propagation-integration.js.map +1 -0
  13. package/dist/error-integration.cjs +128 -0
  14. package/dist/error-integration.cjs.map +1 -0
  15. package/dist/error-integration.js +127 -0
  16. package/dist/error-integration.js.map +1 -0
  17. package/dist/fetch-integration.cjs +120 -0
  18. package/dist/fetch-integration.cjs.map +1 -0
  19. package/dist/fetch-integration.js +119 -0
  20. package/dist/fetch-integration.js.map +1 -0
  21. package/dist/framework-error-integration.cjs +102 -0
  22. package/dist/framework-error-integration.cjs.map +1 -0
  23. package/dist/framework-error-integration.js +101 -0
  24. package/dist/framework-error-integration.js.map +1 -0
  25. package/dist/framework-router-integrations.cjs +88 -0
  26. package/dist/framework-router-integrations.cjs.map +1 -0
  27. package/dist/framework-router-integrations.js +84 -0
  28. package/dist/framework-router-integrations.js.map +1 -0
  29. package/dist/http-transport.cjs +301 -0
  30. package/dist/http-transport.cjs.map +1 -0
  31. package/dist/http-transport.js +299 -0
  32. package/dist/http-transport.js.map +1 -0
  33. package/dist/index.cjs.map +1 -1
  34. package/dist/index.js.map +1 -1
  35. package/dist/indexeddb-offline-queue.cjs +105 -0
  36. package/dist/indexeddb-offline-queue.cjs.map +1 -0
  37. package/dist/indexeddb-offline-queue.js +104 -0
  38. package/dist/indexeddb-offline-queue.js.map +1 -0
  39. package/dist/indexeddb-transport.cjs +531 -0
  40. package/dist/indexeddb-transport.cjs.map +1 -0
  41. package/dist/indexeddb-transport.js +530 -0
  42. package/dist/indexeddb-transport.js.map +1 -0
  43. package/dist/offline-first-transport.cjs +675 -0
  44. package/dist/offline-first-transport.cjs.map +1 -0
  45. package/dist/offline-first-transport.js +674 -0
  46. package/dist/offline-first-transport.js.map +1 -0
  47. package/dist/page-lifecycle.cjs +39 -0
  48. package/dist/page-lifecycle.cjs.map +1 -0
  49. package/dist/page-lifecycle.js +38 -0
  50. package/dist/page-lifecycle.js.map +1 -0
  51. package/dist/payload-transforms.cjs +30 -0
  52. package/dist/payload-transforms.cjs.map +1 -0
  53. package/dist/payload-transforms.js +29 -0
  54. package/dist/payload-transforms.js.map +1 -0
  55. package/dist/performance-integration.cjs +135 -0
  56. package/dist/performance-integration.cjs.map +1 -0
  57. package/dist/performance-integration.js +133 -0
  58. package/dist/performance-integration.js.map +1 -0
  59. package/dist/reporting-integration.cjs +110 -0
  60. package/dist/reporting-integration.cjs.map +1 -0
  61. package/dist/reporting-integration.js +109 -0
  62. package/dist/reporting-integration.js.map +1 -0
  63. package/dist/router-integration.cjs +97 -0
  64. package/dist/router-integration.cjs.map +1 -0
  65. package/dist/router-integration.js +96 -0
  66. package/dist/router-integration.js.map +1 -0
  67. package/dist/runtime-host-integration.cjs +181 -0
  68. package/dist/runtime-host-integration.cjs.map +1 -0
  69. package/dist/runtime-host-integration.js +180 -0
  70. package/dist/runtime-host-integration.js.map +1 -0
  71. package/dist/service-worker-integration.cjs +109 -0
  72. package/dist/service-worker-integration.cjs.map +1 -0
  73. package/dist/service-worker-integration.js +108 -0
  74. package/dist/service-worker-integration.js.map +1 -0
  75. package/dist/service-worker-transport.cjs +184 -0
  76. package/dist/service-worker-transport.cjs.map +1 -0
  77. package/dist/service-worker-transport.js +183 -0
  78. package/dist/service-worker-transport.js.map +1 -0
  79. package/dist/user-action-integration.cjs +100 -0
  80. package/dist/user-action-integration.cjs.map +1 -0
  81. package/dist/user-action-integration.js +99 -0
  82. package/dist/user-action-integration.js.map +1 -0
  83. package/dist/web-vitals-integration.cjs +157 -0
  84. package/dist/web-vitals-integration.cjs.map +1 -0
  85. package/dist/web-vitals-integration.js +156 -0
  86. package/dist/web-vitals-integration.js.map +1 -0
  87. package/dist/websocket-integration.cjs +158 -0
  88. package/dist/websocket-integration.cjs.map +1 -0
  89. package/dist/websocket-integration.js +157 -0
  90. package/dist/websocket-integration.js.map +1 -0
  91. package/dist/websocket-transport.cjs +187 -0
  92. package/dist/websocket-transport.cjs.map +1 -0
  93. package/dist/websocket-transport.js +186 -0
  94. package/dist/websocket-transport.js.map +1 -0
  95. package/dist/xhr-integration.cjs +107 -0
  96. package/dist/xhr-integration.cjs.map +1 -0
  97. package/dist/xhr-integration.js +106 -0
  98. package/dist/xhr-integration.js.map +1 -0
  99. package/dist/zip-export.cjs +241 -0
  100. package/dist/zip-export.cjs.map +1 -0
  101. package/dist/zip-export.js +238 -0
  102. package/dist/zip-export.js.map +1 -0
  103. package/package.json +104 -77
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service-worker-integration.js","names":["record","message"],"sources":["../src/service-worker-integration.ts"],"sourcesContent":["import {\n normalizeValue,\n type Integration,\n type IntegrationSetupContext,\n type LoggerLevel,\n} from \"@loggerjs/core\";\n\nexport interface BrowserServiceWorkerControllerLike {\n scriptURL?: string;\n state?: string;\n}\n\nexport interface BrowserServiceWorkerContainerEventsLike {\n controller?: BrowserServiceWorkerControllerLike | null;\n addEventListener: (type: string, listener: EventListenerOrEventListenerObject) => void;\n removeEventListener: (type: string, listener: EventListenerOrEventListenerObject) => void;\n}\n\nexport interface BrowserServiceWorkerMessagePayload {\n dataType: string;\n byteLength?: number;\n data?: unknown;\n origin?: string;\n lastEventId?: string;\n}\n\nexport interface CaptureServiceWorkerOptions {\n level?: LoggerLevel;\n captureControllerChange?: boolean;\n captureMessages?: boolean;\n captureMessageErrors?: boolean;\n captureMessageData?: boolean;\n serviceWorker?: BrowserServiceWorkerContainerEventsLike;\n sanitizeUrl?: (url: string) => string;\n}\n\nconst textEncoder = new TextEncoder();\n\nfunction dataType(data: unknown): string {\n if (typeof data === \"string\") return \"string\";\n if (data instanceof ArrayBuffer) return \"arraybuffer\";\n if (ArrayBuffer.isView(data)) return data.constructor.name;\n const record = data as { constructor?: { name?: string } } | undefined;\n return record?.constructor?.name ?? typeof data;\n}\n\nfunction dataByteLength(data: unknown): number | undefined {\n if (typeof data === \"string\") return textEncoder.encode(data).byteLength;\n if (data instanceof ArrayBuffer) return data.byteLength;\n if (ArrayBuffer.isView(data)) return data.byteLength;\n const size = (data as { size?: unknown } | undefined)?.size;\n return typeof size === \"number\" && Number.isFinite(size) ? size : undefined;\n}\n\nfunction controllerInfo(\n controller: BrowserServiceWorkerControllerLike | null | undefined,\n sanitizeUrl: ((url: string) => string) | undefined,\n) {\n if (!controller) return undefined;\n return {\n scriptURL:\n controller.scriptURL && sanitizeUrl\n ? sanitizeUrl(controller.scriptURL)\n : controller.scriptURL,\n state: controller.state,\n };\n}\n\nfunction messagePayload(\n event: MessageEvent,\n captureMessageData: boolean,\n): BrowserServiceWorkerMessagePayload {\n return {\n dataType: dataType(event.data),\n byteLength: dataByteLength(event.data),\n data: captureMessageData\n ? normalizeValue(event.data, { maxDepth: 4, maxObjectKeys: 80 })\n : undefined,\n origin: event.origin,\n lastEventId: event.lastEventId,\n };\n}\n\nexport function captureServiceWorkerIntegration(\n options: CaptureServiceWorkerOptions = {},\n): Integration {\n const level = options.level ?? \"debug\";\n const captureControllerChange = options.captureControllerChange ?? true;\n const captureMessages = options.captureMessages ?? true;\n const captureMessageErrors = options.captureMessageErrors ?? true;\n const captureMessageData = options.captureMessageData ?? false;\n\n return {\n name: \"capture-service-worker\",\n setup(api: IntegrationSetupContext) {\n const serviceWorker =\n options.serviceWorker ??\n (typeof navigator === \"undefined\" ? undefined : navigator.serviceWorker);\n if (!serviceWorker?.addEventListener || !serviceWorker.removeEventListener) return;\n const disposers: Array<() => void> = [];\n let disposed = false;\n\n const capture = api.guard((input: Parameters<IntegrationSetupContext[\"capture\"]>[0]) => {\n if (!disposed) api.capture(input);\n });\n\n if (captureControllerChange) {\n const onControllerChange = () => {\n capture({\n level,\n message: \"Service worker controller change\",\n props: {\n browser: {\n kind: \"service-worker\",\n event: \"controllerchange\",\n controller: controllerInfo(serviceWorker.controller, options.sanitizeUrl),\n },\n },\n });\n };\n serviceWorker.addEventListener(\"controllerchange\", onControllerChange);\n disposers.push(() =>\n serviceWorker.removeEventListener(\"controllerchange\", onControllerChange),\n );\n }\n\n if (captureMessages) {\n const onMessage = (event: Event) => {\n const message = event as MessageEvent;\n capture({\n level,\n message: \"Service worker message\",\n props: {\n browser: {\n kind: \"service-worker\",\n event: \"message\",\n message: messagePayload(message, captureMessageData),\n },\n },\n });\n };\n serviceWorker.addEventListener(\"message\", onMessage);\n disposers.push(() => serviceWorker.removeEventListener(\"message\", onMessage));\n }\n\n if (captureMessageErrors) {\n const onMessageError = (event: Event) => {\n capture({\n level: \"warn\",\n message: \"Service worker message error\",\n props: {\n browser: {\n kind: \"service-worker\",\n event: \"messageerror\",\n type: event.type,\n },\n },\n });\n };\n serviceWorker.addEventListener(\"messageerror\", onMessageError);\n disposers.push(() => serviceWorker.removeEventListener(\"messageerror\", onMessageError));\n }\n\n return () => {\n if (disposed) return;\n disposed = true;\n for (const dispose of disposers) dispose();\n };\n },\n };\n}\n"],"mappings":"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"}
@@ -0,0 +1,184 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let _loggerjs_core = require("@loggerjs/core");
3
+ //#region src/service-worker-transport.ts
4
+ let sourceSequence = 0;
5
+ function createSourceId() {
6
+ sourceSequence += 1;
7
+ return `loggerjs-${Date.now().toString(36)}-${sourceSequence.toString(36)}`;
8
+ }
9
+ function defaultMapEvent(event, context) {
10
+ return {
11
+ type: "loggerjs.event",
12
+ source: context.source,
13
+ event
14
+ };
15
+ }
16
+ function defaultMapBatch(events, context) {
17
+ return {
18
+ type: "loggerjs.batch",
19
+ source: context.source,
20
+ events
21
+ };
22
+ }
23
+ function serviceWorkerContainer() {
24
+ return typeof navigator === "undefined" ? void 0 : navigator.serviceWorker;
25
+ }
26
+ function workerFromRegistration(registration) {
27
+ return registration.active ?? registration.waiting ?? registration.installing ?? void 0;
28
+ }
29
+ function browserServiceWorkerTransport(options = {}) {
30
+ const transportName = options.name ?? "browser-service-worker";
31
+ const target = options.target ?? "controller";
32
+ const source = options.source ?? createSourceId();
33
+ const maxQueueSize = options.maxQueueSize ?? 1e3;
34
+ const dropPolicy = options.dropPolicy ?? "drop-oldest";
35
+ const mapContext = {
36
+ source,
37
+ target
38
+ };
39
+ const mapEvent = options.mapEvent ?? defaultMapEvent;
40
+ const mapBatch = options.mapBatch ?? defaultMapBatch;
41
+ const queue = [];
42
+ let readyWorker;
43
+ let readyPromise;
44
+ let lastContext;
45
+ const reportError = (error, context, operation, droppedEvents = 0) => {
46
+ try {
47
+ options.onError?.(error, {
48
+ droppedEvents,
49
+ operation
50
+ });
51
+ } catch (onErrorError) {
52
+ context?.reportInternalError(onErrorError, {
53
+ operation: "on-error",
54
+ phase: "transport",
55
+ transport: transportName
56
+ });
57
+ }
58
+ context?.reportInternalError(error, {
59
+ operation,
60
+ phase: "transport",
61
+ transport: transportName
62
+ });
63
+ };
64
+ const reportDrop = (item, reason) => {
65
+ (0, _loggerjs_core.incrementLoggerMetaCounter)("transport.dropped", item.events.length);
66
+ (0, _loggerjs_core.incrementLoggerMetaCounter)(`transport.dropped.${reason}`, item.events.length);
67
+ for (const event of item.events) options.onDrop?.(event, reason);
68
+ };
69
+ const currentWorker = () => {
70
+ const container = options.serviceWorker ?? serviceWorkerContainer();
71
+ if (!container) return void 0;
72
+ if (target === "controller") return container.controller ?? void 0;
73
+ return readyWorker;
74
+ };
75
+ const post = (worker, item, context) => {
76
+ try {
77
+ worker.postMessage(item.message, options.transfer?.(item.message));
78
+ return true;
79
+ } catch (error) {
80
+ reportDrop(item, "post-message");
81
+ reportError(error, context, "post-message", item.events.length);
82
+ return false;
83
+ }
84
+ };
85
+ const drain = (context = lastContext) => {
86
+ const worker = currentWorker();
87
+ if (!worker || !context) return;
88
+ while (queue.length > 0) {
89
+ const item = queue.shift();
90
+ if (!item) return;
91
+ post(worker, item, context);
92
+ }
93
+ };
94
+ const dropQueued = (reason) => {
95
+ const pending = queue.splice(0);
96
+ for (const item of pending) reportDrop(item, reason);
97
+ return pending.reduce((total, item) => total + item.events.length, 0);
98
+ };
99
+ const waitForReady = (context) => {
100
+ if (currentWorker()) return Promise.resolve();
101
+ if (target !== "ready") return Promise.reject(/* @__PURE__ */ new Error("service worker controller is not available"));
102
+ if (readyPromise) return readyPromise;
103
+ const container = options.serviceWorker ?? serviceWorkerContainer();
104
+ if (!container?.ready) {
105
+ const error = /* @__PURE__ */ new Error("serviceWorker.ready is not available");
106
+ reportError(error, context, "ready", dropQueued("ready"));
107
+ return Promise.reject(error);
108
+ }
109
+ readyPromise = container.ready.then((registration) => {
110
+ readyWorker = workerFromRegistration(registration);
111
+ if (!readyWorker) throw new Error("service worker registration has no active worker");
112
+ drain(context ?? lastContext);
113
+ }).catch((error) => {
114
+ readyPromise = void 0;
115
+ const droppedEvents = dropQueued("ready");
116
+ reportError(error, context ?? lastContext, "ready", droppedEvents);
117
+ throw error;
118
+ });
119
+ return readyPromise;
120
+ };
121
+ const startReady = (context) => {
122
+ waitForReady(context).catch(() => {});
123
+ };
124
+ const enqueue = (item) => {
125
+ if (queue.length >= maxQueueSize) {
126
+ if (dropPolicy === "drop-newest") {
127
+ reportDrop(item, "queue-full");
128
+ return false;
129
+ }
130
+ const dropped = queue.shift();
131
+ if (dropped) reportDrop(dropped, "queue-full");
132
+ }
133
+ queue.push(item);
134
+ return true;
135
+ };
136
+ const send = (item, context) => {
137
+ lastContext = context;
138
+ const worker = currentWorker();
139
+ if (worker) {
140
+ post(worker, item, context);
141
+ return;
142
+ }
143
+ if (target === "ready") {
144
+ if (enqueue(item)) startReady(context);
145
+ return;
146
+ }
147
+ reportDrop(item, "unavailable");
148
+ reportError(/* @__PURE__ */ new Error("service worker controller is not available"), context, "unavailable", item.events.length);
149
+ };
150
+ return {
151
+ name: transportName,
152
+ minLevel: options.minLevel,
153
+ queueSize() {
154
+ return queue.length;
155
+ },
156
+ ready() {
157
+ return waitForReady();
158
+ },
159
+ log(event, context) {
160
+ send({
161
+ events: [event],
162
+ message: mapEvent(event, mapContext)
163
+ }, context);
164
+ },
165
+ logBatch(events, context) {
166
+ if (events.length === 0) return;
167
+ send({
168
+ events,
169
+ message: mapBatch(events, mapContext)
170
+ }, context);
171
+ },
172
+ flush() {
173
+ drain();
174
+ },
175
+ close() {
176
+ const pending = queue.splice(0);
177
+ for (const item of pending) reportDrop(item, "closed");
178
+ }
179
+ };
180
+ }
181
+ //#endregion
182
+ exports.browserServiceWorkerTransport = browserServiceWorkerTransport;
183
+
184
+ //# sourceMappingURL=service-worker-transport.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service-worker-transport.cjs","names":[],"sources":["../src/service-worker-transport.ts"],"sourcesContent":["import {\n incrementLoggerMetaCounter,\n type LogEvent,\n type LoggerLevel,\n type Transport,\n type TransportContext,\n} from \"@loggerjs/core\";\n\nexport type BrowserServiceWorkerDropPolicy = \"drop-oldest\" | \"drop-newest\";\nexport type BrowserServiceWorkerTarget = \"controller\" | \"ready\";\n\nexport interface BrowserServiceWorkerLike {\n postMessage: (message: unknown, transfer?: Transferable[] | StructuredSerializeOptions) => void;\n}\n\nexport interface BrowserServiceWorkerRegistrationLike {\n active?: BrowserServiceWorkerLike | null;\n waiting?: BrowserServiceWorkerLike | null;\n installing?: BrowserServiceWorkerLike | null;\n}\n\nexport interface BrowserServiceWorkerContainerLike {\n controller?: BrowserServiceWorkerLike | null;\n ready?: Promise<BrowserServiceWorkerRegistrationLike>;\n}\n\nexport interface BrowserServiceWorkerEventMessage {\n type: \"loggerjs.event\";\n source: string;\n event: LogEvent;\n}\n\nexport interface BrowserServiceWorkerBatchMessage {\n type: \"loggerjs.batch\";\n source: string;\n events: readonly LogEvent[];\n}\n\nexport type BrowserServiceWorkerMessage =\n | BrowserServiceWorkerEventMessage\n | BrowserServiceWorkerBatchMessage;\n\nexport interface BrowserServiceWorkerMapContext {\n source: string;\n target: BrowserServiceWorkerTarget;\n}\n\nexport interface BrowserServiceWorkerTransportOptions {\n name?: string;\n minLevel?: LoggerLevel;\n source?: string;\n target?: BrowserServiceWorkerTarget;\n serviceWorker?: BrowserServiceWorkerContainerLike;\n maxQueueSize?: number;\n dropPolicy?: BrowserServiceWorkerDropPolicy;\n transfer?: (message: unknown) => Transferable[] | StructuredSerializeOptions | undefined;\n mapEvent?: (event: LogEvent, context: BrowserServiceWorkerMapContext) => unknown;\n mapBatch?: (events: readonly LogEvent[], context: BrowserServiceWorkerMapContext) => unknown;\n onDrop?: (event: LogEvent, reason: string) => void;\n onError?: (error: unknown, detail: { operation: string; droppedEvents: number }) => void;\n}\n\ninterface QueueItem {\n events: readonly LogEvent[];\n message: unknown;\n}\n\nlet sourceSequence = 0;\n\nfunction createSourceId() {\n sourceSequence += 1;\n return `loggerjs-${Date.now().toString(36)}-${sourceSequence.toString(36)}`;\n}\n\nfunction defaultMapEvent(\n event: LogEvent,\n context: BrowserServiceWorkerMapContext,\n): BrowserServiceWorkerEventMessage {\n return { type: \"loggerjs.event\", source: context.source, event };\n}\n\nfunction defaultMapBatch(\n events: readonly LogEvent[],\n context: BrowserServiceWorkerMapContext,\n): BrowserServiceWorkerBatchMessage {\n return { type: \"loggerjs.batch\", source: context.source, events };\n}\n\nfunction serviceWorkerContainer(): BrowserServiceWorkerContainerLike | undefined {\n return typeof navigator === \"undefined\"\n ? undefined\n : (navigator.serviceWorker as unknown as BrowserServiceWorkerContainerLike);\n}\n\nfunction workerFromRegistration(registration: BrowserServiceWorkerRegistrationLike) {\n return registration.active ?? registration.waiting ?? registration.installing ?? undefined;\n}\n\nexport function browserServiceWorkerTransport(\n options: BrowserServiceWorkerTransportOptions = {},\n): Transport & { queueSize: () => number } {\n const transportName = options.name ?? \"browser-service-worker\";\n const target = options.target ?? \"controller\";\n const source = options.source ?? createSourceId();\n const maxQueueSize = options.maxQueueSize ?? 1000;\n const dropPolicy = options.dropPolicy ?? \"drop-oldest\";\n const mapContext: BrowserServiceWorkerMapContext = { source, target };\n const mapEvent = options.mapEvent ?? defaultMapEvent;\n const mapBatch = options.mapBatch ?? defaultMapBatch;\n const queue: QueueItem[] = [];\n let readyWorker: BrowserServiceWorkerLike | undefined;\n let readyPromise: Promise<void> | undefined;\n let lastContext: TransportContext | undefined;\n\n const reportError = (\n error: unknown,\n context: TransportContext | undefined,\n operation: string,\n droppedEvents = 0,\n ) => {\n try {\n options.onError?.(error, { droppedEvents, operation });\n } catch (onErrorError) {\n context?.reportInternalError(onErrorError, {\n operation: \"on-error\",\n phase: \"transport\",\n transport: transportName,\n });\n }\n context?.reportInternalError(error, {\n operation,\n phase: \"transport\",\n transport: transportName,\n });\n };\n\n const reportDrop = (item: QueueItem, reason: string) => {\n incrementLoggerMetaCounter(\"transport.dropped\", item.events.length);\n incrementLoggerMetaCounter(`transport.dropped.${reason}`, item.events.length);\n for (const event of item.events) options.onDrop?.(event, reason);\n };\n\n const currentWorker = () => {\n const container = options.serviceWorker ?? serviceWorkerContainer();\n if (!container) return undefined;\n if (target === \"controller\") return container.controller ?? undefined;\n return readyWorker;\n };\n\n const post = (worker: BrowserServiceWorkerLike, item: QueueItem, context: TransportContext) => {\n try {\n worker.postMessage(item.message, options.transfer?.(item.message));\n return true;\n } catch (error) {\n reportDrop(item, \"post-message\");\n reportError(error, context, \"post-message\", item.events.length);\n return false;\n }\n };\n\n const drain = (context = lastContext) => {\n const worker = currentWorker();\n if (!worker || !context) return;\n while (queue.length > 0) {\n const item = queue.shift();\n if (!item) return;\n post(worker, item, context);\n }\n };\n\n const dropQueued = (reason: string) => {\n const pending = queue.splice(0);\n for (const item of pending) reportDrop(item, reason);\n return pending.reduce((total, item) => total + item.events.length, 0);\n };\n\n const waitForReady = (context?: TransportContext) => {\n const worker = currentWorker();\n if (worker) return Promise.resolve();\n if (target !== \"ready\") {\n return Promise.reject(new Error(\"service worker controller is not available\"));\n }\n if (readyPromise) return readyPromise;\n const container = options.serviceWorker ?? serviceWorkerContainer();\n if (!container?.ready) {\n const error = new Error(\"serviceWorker.ready is not available\");\n const droppedEvents = dropQueued(\"ready\");\n reportError(error, context, \"ready\", droppedEvents);\n return Promise.reject(error);\n }\n readyPromise = container.ready\n .then((registration) => {\n readyWorker = workerFromRegistration(registration);\n if (!readyWorker) {\n throw new Error(\"service worker registration has no active worker\");\n }\n drain(context ?? lastContext);\n })\n .catch((error) => {\n readyPromise = undefined;\n const droppedEvents = dropQueued(\"ready\");\n reportError(error, context ?? lastContext, \"ready\", droppedEvents);\n throw error;\n });\n return readyPromise;\n };\n\n const startReady = (context: TransportContext) => {\n void waitForReady(context).catch(() => {\n // Errors are already reported through the transport context.\n });\n };\n\n const enqueue = (item: QueueItem) => {\n if (queue.length >= maxQueueSize) {\n if (dropPolicy === \"drop-newest\") {\n reportDrop(item, \"queue-full\");\n return false;\n }\n const dropped = queue.shift();\n if (dropped) reportDrop(dropped, \"queue-full\");\n }\n queue.push(item);\n return true;\n };\n\n const send = (item: QueueItem, context: TransportContext) => {\n lastContext = context;\n const worker = currentWorker();\n if (worker) {\n post(worker, item, context);\n return;\n }\n if (target === \"ready\") {\n if (enqueue(item)) startReady(context);\n return;\n }\n reportDrop(item, \"unavailable\");\n reportError(\n new Error(\"service worker controller is not available\"),\n context,\n \"unavailable\",\n item.events.length,\n );\n };\n\n return {\n name: transportName,\n minLevel: options.minLevel,\n queueSize() {\n return queue.length;\n },\n ready() {\n return waitForReady();\n },\n log(event, context) {\n send({ events: [event], message: mapEvent(event, mapContext) }, context);\n },\n logBatch(events, context) {\n if (events.length === 0) return;\n send({ events, message: mapBatch(events, mapContext) }, context);\n },\n flush() {\n drain();\n },\n close() {\n const pending = queue.splice(0);\n for (const item of pending) reportDrop(item, \"closed\");\n },\n };\n}\n"],"mappings":"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"}
@@ -0,0 +1,183 @@
1
+ import { incrementLoggerMetaCounter } from "@loggerjs/core";
2
+ //#region src/service-worker-transport.ts
3
+ let sourceSequence = 0;
4
+ function createSourceId() {
5
+ sourceSequence += 1;
6
+ return `loggerjs-${Date.now().toString(36)}-${sourceSequence.toString(36)}`;
7
+ }
8
+ function defaultMapEvent(event, context) {
9
+ return {
10
+ type: "loggerjs.event",
11
+ source: context.source,
12
+ event
13
+ };
14
+ }
15
+ function defaultMapBatch(events, context) {
16
+ return {
17
+ type: "loggerjs.batch",
18
+ source: context.source,
19
+ events
20
+ };
21
+ }
22
+ function serviceWorkerContainer() {
23
+ return typeof navigator === "undefined" ? void 0 : navigator.serviceWorker;
24
+ }
25
+ function workerFromRegistration(registration) {
26
+ return registration.active ?? registration.waiting ?? registration.installing ?? void 0;
27
+ }
28
+ function browserServiceWorkerTransport(options = {}) {
29
+ const transportName = options.name ?? "browser-service-worker";
30
+ const target = options.target ?? "controller";
31
+ const source = options.source ?? createSourceId();
32
+ const maxQueueSize = options.maxQueueSize ?? 1e3;
33
+ const dropPolicy = options.dropPolicy ?? "drop-oldest";
34
+ const mapContext = {
35
+ source,
36
+ target
37
+ };
38
+ const mapEvent = options.mapEvent ?? defaultMapEvent;
39
+ const mapBatch = options.mapBatch ?? defaultMapBatch;
40
+ const queue = [];
41
+ let readyWorker;
42
+ let readyPromise;
43
+ let lastContext;
44
+ const reportError = (error, context, operation, droppedEvents = 0) => {
45
+ try {
46
+ options.onError?.(error, {
47
+ droppedEvents,
48
+ operation
49
+ });
50
+ } catch (onErrorError) {
51
+ context?.reportInternalError(onErrorError, {
52
+ operation: "on-error",
53
+ phase: "transport",
54
+ transport: transportName
55
+ });
56
+ }
57
+ context?.reportInternalError(error, {
58
+ operation,
59
+ phase: "transport",
60
+ transport: transportName
61
+ });
62
+ };
63
+ const reportDrop = (item, reason) => {
64
+ incrementLoggerMetaCounter("transport.dropped", item.events.length);
65
+ incrementLoggerMetaCounter(`transport.dropped.${reason}`, item.events.length);
66
+ for (const event of item.events) options.onDrop?.(event, reason);
67
+ };
68
+ const currentWorker = () => {
69
+ const container = options.serviceWorker ?? serviceWorkerContainer();
70
+ if (!container) return void 0;
71
+ if (target === "controller") return container.controller ?? void 0;
72
+ return readyWorker;
73
+ };
74
+ const post = (worker, item, context) => {
75
+ try {
76
+ worker.postMessage(item.message, options.transfer?.(item.message));
77
+ return true;
78
+ } catch (error) {
79
+ reportDrop(item, "post-message");
80
+ reportError(error, context, "post-message", item.events.length);
81
+ return false;
82
+ }
83
+ };
84
+ const drain = (context = lastContext) => {
85
+ const worker = currentWorker();
86
+ if (!worker || !context) return;
87
+ while (queue.length > 0) {
88
+ const item = queue.shift();
89
+ if (!item) return;
90
+ post(worker, item, context);
91
+ }
92
+ };
93
+ const dropQueued = (reason) => {
94
+ const pending = queue.splice(0);
95
+ for (const item of pending) reportDrop(item, reason);
96
+ return pending.reduce((total, item) => total + item.events.length, 0);
97
+ };
98
+ const waitForReady = (context) => {
99
+ if (currentWorker()) return Promise.resolve();
100
+ if (target !== "ready") return Promise.reject(/* @__PURE__ */ new Error("service worker controller is not available"));
101
+ if (readyPromise) return readyPromise;
102
+ const container = options.serviceWorker ?? serviceWorkerContainer();
103
+ if (!container?.ready) {
104
+ const error = /* @__PURE__ */ new Error("serviceWorker.ready is not available");
105
+ reportError(error, context, "ready", dropQueued("ready"));
106
+ return Promise.reject(error);
107
+ }
108
+ readyPromise = container.ready.then((registration) => {
109
+ readyWorker = workerFromRegistration(registration);
110
+ if (!readyWorker) throw new Error("service worker registration has no active worker");
111
+ drain(context ?? lastContext);
112
+ }).catch((error) => {
113
+ readyPromise = void 0;
114
+ const droppedEvents = dropQueued("ready");
115
+ reportError(error, context ?? lastContext, "ready", droppedEvents);
116
+ throw error;
117
+ });
118
+ return readyPromise;
119
+ };
120
+ const startReady = (context) => {
121
+ waitForReady(context).catch(() => {});
122
+ };
123
+ const enqueue = (item) => {
124
+ if (queue.length >= maxQueueSize) {
125
+ if (dropPolicy === "drop-newest") {
126
+ reportDrop(item, "queue-full");
127
+ return false;
128
+ }
129
+ const dropped = queue.shift();
130
+ if (dropped) reportDrop(dropped, "queue-full");
131
+ }
132
+ queue.push(item);
133
+ return true;
134
+ };
135
+ const send = (item, context) => {
136
+ lastContext = context;
137
+ const worker = currentWorker();
138
+ if (worker) {
139
+ post(worker, item, context);
140
+ return;
141
+ }
142
+ if (target === "ready") {
143
+ if (enqueue(item)) startReady(context);
144
+ return;
145
+ }
146
+ reportDrop(item, "unavailable");
147
+ reportError(/* @__PURE__ */ new Error("service worker controller is not available"), context, "unavailable", item.events.length);
148
+ };
149
+ return {
150
+ name: transportName,
151
+ minLevel: options.minLevel,
152
+ queueSize() {
153
+ return queue.length;
154
+ },
155
+ ready() {
156
+ return waitForReady();
157
+ },
158
+ log(event, context) {
159
+ send({
160
+ events: [event],
161
+ message: mapEvent(event, mapContext)
162
+ }, context);
163
+ },
164
+ logBatch(events, context) {
165
+ if (events.length === 0) return;
166
+ send({
167
+ events,
168
+ message: mapBatch(events, mapContext)
169
+ }, context);
170
+ },
171
+ flush() {
172
+ drain();
173
+ },
174
+ close() {
175
+ const pending = queue.splice(0);
176
+ for (const item of pending) reportDrop(item, "closed");
177
+ }
178
+ };
179
+ }
180
+ //#endregion
181
+ export { browserServiceWorkerTransport };
182
+
183
+ //# sourceMappingURL=service-worker-transport.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service-worker-transport.js","names":[],"sources":["../src/service-worker-transport.ts"],"sourcesContent":["import {\n incrementLoggerMetaCounter,\n type LogEvent,\n type LoggerLevel,\n type Transport,\n type TransportContext,\n} from \"@loggerjs/core\";\n\nexport type BrowserServiceWorkerDropPolicy = \"drop-oldest\" | \"drop-newest\";\nexport type BrowserServiceWorkerTarget = \"controller\" | \"ready\";\n\nexport interface BrowserServiceWorkerLike {\n postMessage: (message: unknown, transfer?: Transferable[] | StructuredSerializeOptions) => void;\n}\n\nexport interface BrowserServiceWorkerRegistrationLike {\n active?: BrowserServiceWorkerLike | null;\n waiting?: BrowserServiceWorkerLike | null;\n installing?: BrowserServiceWorkerLike | null;\n}\n\nexport interface BrowserServiceWorkerContainerLike {\n controller?: BrowserServiceWorkerLike | null;\n ready?: Promise<BrowserServiceWorkerRegistrationLike>;\n}\n\nexport interface BrowserServiceWorkerEventMessage {\n type: \"loggerjs.event\";\n source: string;\n event: LogEvent;\n}\n\nexport interface BrowserServiceWorkerBatchMessage {\n type: \"loggerjs.batch\";\n source: string;\n events: readonly LogEvent[];\n}\n\nexport type BrowserServiceWorkerMessage =\n | BrowserServiceWorkerEventMessage\n | BrowserServiceWorkerBatchMessage;\n\nexport interface BrowserServiceWorkerMapContext {\n source: string;\n target: BrowserServiceWorkerTarget;\n}\n\nexport interface BrowserServiceWorkerTransportOptions {\n name?: string;\n minLevel?: LoggerLevel;\n source?: string;\n target?: BrowserServiceWorkerTarget;\n serviceWorker?: BrowserServiceWorkerContainerLike;\n maxQueueSize?: number;\n dropPolicy?: BrowserServiceWorkerDropPolicy;\n transfer?: (message: unknown) => Transferable[] | StructuredSerializeOptions | undefined;\n mapEvent?: (event: LogEvent, context: BrowserServiceWorkerMapContext) => unknown;\n mapBatch?: (events: readonly LogEvent[], context: BrowserServiceWorkerMapContext) => unknown;\n onDrop?: (event: LogEvent, reason: string) => void;\n onError?: (error: unknown, detail: { operation: string; droppedEvents: number }) => void;\n}\n\ninterface QueueItem {\n events: readonly LogEvent[];\n message: unknown;\n}\n\nlet sourceSequence = 0;\n\nfunction createSourceId() {\n sourceSequence += 1;\n return `loggerjs-${Date.now().toString(36)}-${sourceSequence.toString(36)}`;\n}\n\nfunction defaultMapEvent(\n event: LogEvent,\n context: BrowserServiceWorkerMapContext,\n): BrowserServiceWorkerEventMessage {\n return { type: \"loggerjs.event\", source: context.source, event };\n}\n\nfunction defaultMapBatch(\n events: readonly LogEvent[],\n context: BrowserServiceWorkerMapContext,\n): BrowserServiceWorkerBatchMessage {\n return { type: \"loggerjs.batch\", source: context.source, events };\n}\n\nfunction serviceWorkerContainer(): BrowserServiceWorkerContainerLike | undefined {\n return typeof navigator === \"undefined\"\n ? undefined\n : (navigator.serviceWorker as unknown as BrowserServiceWorkerContainerLike);\n}\n\nfunction workerFromRegistration(registration: BrowserServiceWorkerRegistrationLike) {\n return registration.active ?? registration.waiting ?? registration.installing ?? undefined;\n}\n\nexport function browserServiceWorkerTransport(\n options: BrowserServiceWorkerTransportOptions = {},\n): Transport & { queueSize: () => number } {\n const transportName = options.name ?? \"browser-service-worker\";\n const target = options.target ?? \"controller\";\n const source = options.source ?? createSourceId();\n const maxQueueSize = options.maxQueueSize ?? 1000;\n const dropPolicy = options.dropPolicy ?? \"drop-oldest\";\n const mapContext: BrowserServiceWorkerMapContext = { source, target };\n const mapEvent = options.mapEvent ?? defaultMapEvent;\n const mapBatch = options.mapBatch ?? defaultMapBatch;\n const queue: QueueItem[] = [];\n let readyWorker: BrowserServiceWorkerLike | undefined;\n let readyPromise: Promise<void> | undefined;\n let lastContext: TransportContext | undefined;\n\n const reportError = (\n error: unknown,\n context: TransportContext | undefined,\n operation: string,\n droppedEvents = 0,\n ) => {\n try {\n options.onError?.(error, { droppedEvents, operation });\n } catch (onErrorError) {\n context?.reportInternalError(onErrorError, {\n operation: \"on-error\",\n phase: \"transport\",\n transport: transportName,\n });\n }\n context?.reportInternalError(error, {\n operation,\n phase: \"transport\",\n transport: transportName,\n });\n };\n\n const reportDrop = (item: QueueItem, reason: string) => {\n incrementLoggerMetaCounter(\"transport.dropped\", item.events.length);\n incrementLoggerMetaCounter(`transport.dropped.${reason}`, item.events.length);\n for (const event of item.events) options.onDrop?.(event, reason);\n };\n\n const currentWorker = () => {\n const container = options.serviceWorker ?? serviceWorkerContainer();\n if (!container) return undefined;\n if (target === \"controller\") return container.controller ?? undefined;\n return readyWorker;\n };\n\n const post = (worker: BrowserServiceWorkerLike, item: QueueItem, context: TransportContext) => {\n try {\n worker.postMessage(item.message, options.transfer?.(item.message));\n return true;\n } catch (error) {\n reportDrop(item, \"post-message\");\n reportError(error, context, \"post-message\", item.events.length);\n return false;\n }\n };\n\n const drain = (context = lastContext) => {\n const worker = currentWorker();\n if (!worker || !context) return;\n while (queue.length > 0) {\n const item = queue.shift();\n if (!item) return;\n post(worker, item, context);\n }\n };\n\n const dropQueued = (reason: string) => {\n const pending = queue.splice(0);\n for (const item of pending) reportDrop(item, reason);\n return pending.reduce((total, item) => total + item.events.length, 0);\n };\n\n const waitForReady = (context?: TransportContext) => {\n const worker = currentWorker();\n if (worker) return Promise.resolve();\n if (target !== \"ready\") {\n return Promise.reject(new Error(\"service worker controller is not available\"));\n }\n if (readyPromise) return readyPromise;\n const container = options.serviceWorker ?? serviceWorkerContainer();\n if (!container?.ready) {\n const error = new Error(\"serviceWorker.ready is not available\");\n const droppedEvents = dropQueued(\"ready\");\n reportError(error, context, \"ready\", droppedEvents);\n return Promise.reject(error);\n }\n readyPromise = container.ready\n .then((registration) => {\n readyWorker = workerFromRegistration(registration);\n if (!readyWorker) {\n throw new Error(\"service worker registration has no active worker\");\n }\n drain(context ?? lastContext);\n })\n .catch((error) => {\n readyPromise = undefined;\n const droppedEvents = dropQueued(\"ready\");\n reportError(error, context ?? lastContext, \"ready\", droppedEvents);\n throw error;\n });\n return readyPromise;\n };\n\n const startReady = (context: TransportContext) => {\n void waitForReady(context).catch(() => {\n // Errors are already reported through the transport context.\n });\n };\n\n const enqueue = (item: QueueItem) => {\n if (queue.length >= maxQueueSize) {\n if (dropPolicy === \"drop-newest\") {\n reportDrop(item, \"queue-full\");\n return false;\n }\n const dropped = queue.shift();\n if (dropped) reportDrop(dropped, \"queue-full\");\n }\n queue.push(item);\n return true;\n };\n\n const send = (item: QueueItem, context: TransportContext) => {\n lastContext = context;\n const worker = currentWorker();\n if (worker) {\n post(worker, item, context);\n return;\n }\n if (target === \"ready\") {\n if (enqueue(item)) startReady(context);\n return;\n }\n reportDrop(item, \"unavailable\");\n reportError(\n new Error(\"service worker controller is not available\"),\n context,\n \"unavailable\",\n item.events.length,\n );\n };\n\n return {\n name: transportName,\n minLevel: options.minLevel,\n queueSize() {\n return queue.length;\n },\n ready() {\n return waitForReady();\n },\n log(event, context) {\n send({ events: [event], message: mapEvent(event, mapContext) }, context);\n },\n logBatch(events, context) {\n if (events.length === 0) return;\n send({ events, message: mapBatch(events, mapContext) }, context);\n },\n flush() {\n drain();\n },\n close() {\n const pending = queue.splice(0);\n for (const item of pending) reportDrop(item, \"closed\");\n },\n };\n}\n"],"mappings":"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"}
@@ -0,0 +1,100 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region src/user-action-integration.ts
3
+ const defaultEvents = [
4
+ "click",
5
+ "input",
6
+ "change",
7
+ "submit"
8
+ ];
9
+ const defaultLabelAttributes = [
10
+ "data-loggerjs-label",
11
+ "aria-label",
12
+ "name",
13
+ "id"
14
+ ];
15
+ function truncate(value, maxLength) {
16
+ if (!value) return void 0;
17
+ return value.length > maxLength ? value.slice(0, maxLength) : value;
18
+ }
19
+ function readAttribute(target, name) {
20
+ return target.getAttribute?.(name) || void 0;
21
+ }
22
+ function sanitizeField(value, field, sanitize) {
23
+ if (!value) return void 0;
24
+ return sanitize ? sanitize(value, field) : value;
25
+ }
26
+ function firstLabel(target, attributes) {
27
+ for (const name of attributes) {
28
+ const value = readAttribute(target, name) ?? target[name];
29
+ if (typeof value === "string" && value) return value;
30
+ }
31
+ }
32
+ function targetInfo(target, options) {
33
+ const element = target;
34
+ if (!element || typeof element !== "object") return {};
35
+ const maxTextLength = options.maxTextLength ?? 120;
36
+ const sanitize = options.sanitize;
37
+ const info = {
38
+ tagName: sanitizeField(element.tagName?.toLowerCase(), "tagName", sanitize),
39
+ id: sanitizeField(element.id, "id", sanitize),
40
+ name: sanitizeField(element.name, "name", sanitize),
41
+ role: sanitizeField(element.role ?? readAttribute(element, "role"), "role", sanitize),
42
+ type: sanitizeField(element.type, "type", sanitize),
43
+ href: sanitizeField(element.href, "href", sanitize),
44
+ label: sanitizeField(firstLabel(element, options.labelAttributes ?? defaultLabelAttributes), "label", sanitize)
45
+ };
46
+ if (options.captureText) info.text = sanitizeField(truncate(element.textContent ?? void 0, maxTextLength), "text", sanitize);
47
+ if (options.captureValue) info.value = sanitizeField(truncate(element.value, maxTextLength), "value", sanitize);
48
+ return info;
49
+ }
50
+ function throttleKey(event, target) {
51
+ return `${event.type}:${target.tagName ?? ""}:${target.id ?? ""}:${target.name ?? ""}:${target.label ?? ""}`;
52
+ }
53
+ function captureUserActionsIntegration(options = {}) {
54
+ const events = options.events ?? defaultEvents;
55
+ const level = options.level ?? "info";
56
+ const listenerCapture = options.listenerCapture ?? true;
57
+ const throttleMs = Math.max(0, Math.floor(options.throttleMs ?? 250));
58
+ const clock = options.clock ?? Date.now;
59
+ return {
60
+ name: "capture-user-actions",
61
+ setup(api) {
62
+ const root = options.root ?? globalThis.document;
63
+ if (!root?.addEventListener || !root.removeEventListener) return;
64
+ const lastSeen = /* @__PURE__ */ new Map();
65
+ let disposed = false;
66
+ const onAction = api.guard((event) => {
67
+ if (disposed) return;
68
+ const target = targetInfo(event.target, options);
69
+ if (options.ignore?.(event, target)) return;
70
+ const now = clock();
71
+ const key = throttleKey(event, target);
72
+ const last = lastSeen.get(key) ?? 0;
73
+ if (throttleMs > 0 && now - last < throttleMs) return;
74
+ lastSeen.set(key, now);
75
+ const action = {
76
+ type: event.type,
77
+ target
78
+ };
79
+ api.capture({
80
+ level,
81
+ message: `User action ${event.type}${target.label ? ` ${target.label}` : ""}`,
82
+ props: { browser: {
83
+ kind: "user-action",
84
+ action
85
+ } }
86
+ });
87
+ });
88
+ for (const event of events) root.addEventListener(event, onAction, listenerCapture);
89
+ return () => {
90
+ if (disposed) return;
91
+ disposed = true;
92
+ for (const event of events) root.removeEventListener(event, onAction, listenerCapture);
93
+ };
94
+ }
95
+ };
96
+ }
97
+ //#endregion
98
+ exports.captureUserActionsIntegration = captureUserActionsIntegration;
99
+
100
+ //# sourceMappingURL=user-action-integration.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"user-action-integration.cjs","names":[],"sources":["../src/user-action-integration.ts"],"sourcesContent":["import type { Integration, IntegrationSetupContext, LoggerLevel } from \"@loggerjs/core\";\n\nexport type BrowserUserActionEventName =\n | \"change\"\n | \"click\"\n | \"dblclick\"\n | \"input\"\n | \"keydown\"\n | \"submit\";\n\nexport interface BrowserUserActionTarget {\n tagName?: string;\n id?: string;\n name?: string;\n role?: string;\n type?: string;\n href?: string;\n label?: string;\n text?: string;\n value?: string;\n}\n\nexport interface BrowserUserActionPayload {\n type: BrowserUserActionEventName | string;\n target: BrowserUserActionTarget;\n}\n\nexport interface BrowserEventTargetLike {\n addEventListener: typeof globalThis.addEventListener;\n removeEventListener: typeof globalThis.removeEventListener;\n}\n\nexport interface CaptureUserActionsOptions {\n events?: readonly BrowserUserActionEventName[];\n level?: LoggerLevel;\n listenerCapture?: boolean;\n throttleMs?: number;\n captureText?: boolean;\n captureValue?: boolean;\n maxTextLength?: number;\n labelAttributes?: readonly string[];\n root?: BrowserEventTargetLike;\n clock?: () => number;\n sanitize?: (value: string, field: keyof BrowserUserActionTarget) => string;\n ignore?: (event: Event, target: BrowserUserActionTarget) => boolean;\n}\n\ntype ElementLike = {\n tagName?: string;\n id?: string;\n name?: string;\n role?: string;\n type?: string;\n href?: string;\n textContent?: string | null;\n value?: string;\n getAttribute?: (name: string) => string | null;\n};\n\nconst defaultEvents: readonly BrowserUserActionEventName[] = [\"click\", \"input\", \"change\", \"submit\"];\nconst defaultLabelAttributes = [\"data-loggerjs-label\", \"aria-label\", \"name\", \"id\"] as const;\n\nfunction truncate(value: string | undefined, maxLength: number): string | undefined {\n if (!value) return undefined;\n return value.length > maxLength ? value.slice(0, maxLength) : value;\n}\n\nfunction readAttribute(target: ElementLike, name: string): string | undefined {\n const value = target.getAttribute?.(name);\n return value || undefined;\n}\n\nfunction sanitizeField(\n value: string | undefined,\n field: keyof BrowserUserActionTarget,\n sanitize: CaptureUserActionsOptions[\"sanitize\"],\n): string | undefined {\n if (!value) return undefined;\n return sanitize ? sanitize(value, field) : value;\n}\n\nfunction firstLabel(target: ElementLike, attributes: readonly string[]): string | undefined {\n for (const name of attributes) {\n const value = readAttribute(target, name) ?? (target as Record<string, unknown>)[name];\n if (typeof value === \"string\" && value) return value;\n }\n return undefined;\n}\n\nfunction targetInfo(target: EventTarget | null, options: CaptureUserActionsOptions) {\n const element = target as ElementLike | null;\n if (!element || typeof element !== \"object\") return {};\n const maxTextLength = options.maxTextLength ?? 120;\n const sanitize = options.sanitize;\n const info: BrowserUserActionTarget = {\n tagName: sanitizeField(element.tagName?.toLowerCase(), \"tagName\", sanitize),\n id: sanitizeField(element.id, \"id\", sanitize),\n name: sanitizeField(element.name, \"name\", sanitize),\n role: sanitizeField(element.role ?? readAttribute(element, \"role\"), \"role\", sanitize),\n type: sanitizeField(element.type, \"type\", sanitize),\n href: sanitizeField(element.href, \"href\", sanitize),\n label: sanitizeField(\n firstLabel(element, options.labelAttributes ?? defaultLabelAttributes),\n \"label\",\n sanitize,\n ),\n };\n\n if (options.captureText) {\n info.text = sanitizeField(\n truncate(element.textContent ?? undefined, maxTextLength),\n \"text\",\n sanitize,\n );\n }\n if (options.captureValue) {\n info.value = sanitizeField(truncate(element.value, maxTextLength), \"value\", sanitize);\n }\n return info;\n}\n\nfunction throttleKey(event: Event, target: BrowserUserActionTarget): string {\n return `${event.type}:${target.tagName ?? \"\"}:${target.id ?? \"\"}:${target.name ?? \"\"}:${target.label ?? \"\"}`;\n}\n\nexport function captureUserActionsIntegration(\n options: CaptureUserActionsOptions = {},\n): Integration {\n const events = options.events ?? defaultEvents;\n const level = options.level ?? \"info\";\n const listenerCapture = options.listenerCapture ?? true;\n const throttleMs = Math.max(0, Math.floor(options.throttleMs ?? 250));\n const clock = options.clock ?? Date.now;\n\n return {\n name: \"capture-user-actions\",\n setup(api: IntegrationSetupContext) {\n const root = options.root ?? globalThis.document;\n if (!root?.addEventListener || !root.removeEventListener) return;\n const lastSeen = new Map<string, number>();\n let disposed = false;\n\n const onAction = api.guard((event: Event) => {\n if (disposed) return;\n const target = targetInfo(event.target, options);\n if (options.ignore?.(event, target)) return;\n const now = clock();\n const key = throttleKey(event, target);\n const last = lastSeen.get(key) ?? 0;\n if (throttleMs > 0 && now - last < throttleMs) return;\n lastSeen.set(key, now);\n const action: BrowserUserActionPayload = {\n type: event.type,\n target,\n };\n api.capture({\n level,\n message: `User action ${event.type}${target.label ? ` ${target.label}` : \"\"}`,\n props: { browser: { kind: \"user-action\", action } },\n });\n });\n\n for (const event of events) root.addEventListener(event, onAction, listenerCapture);\n\n return () => {\n if (disposed) return;\n disposed = true;\n for (const event of events) root.removeEventListener(event, onAction, listenerCapture);\n };\n },\n };\n}\n"],"mappings":"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"}
@@ -0,0 +1,99 @@
1
+ //#region src/user-action-integration.ts
2
+ const defaultEvents = [
3
+ "click",
4
+ "input",
5
+ "change",
6
+ "submit"
7
+ ];
8
+ const defaultLabelAttributes = [
9
+ "data-loggerjs-label",
10
+ "aria-label",
11
+ "name",
12
+ "id"
13
+ ];
14
+ function truncate(value, maxLength) {
15
+ if (!value) return void 0;
16
+ return value.length > maxLength ? value.slice(0, maxLength) : value;
17
+ }
18
+ function readAttribute(target, name) {
19
+ return target.getAttribute?.(name) || void 0;
20
+ }
21
+ function sanitizeField(value, field, sanitize) {
22
+ if (!value) return void 0;
23
+ return sanitize ? sanitize(value, field) : value;
24
+ }
25
+ function firstLabel(target, attributes) {
26
+ for (const name of attributes) {
27
+ const value = readAttribute(target, name) ?? target[name];
28
+ if (typeof value === "string" && value) return value;
29
+ }
30
+ }
31
+ function targetInfo(target, options) {
32
+ const element = target;
33
+ if (!element || typeof element !== "object") return {};
34
+ const maxTextLength = options.maxTextLength ?? 120;
35
+ const sanitize = options.sanitize;
36
+ const info = {
37
+ tagName: sanitizeField(element.tagName?.toLowerCase(), "tagName", sanitize),
38
+ id: sanitizeField(element.id, "id", sanitize),
39
+ name: sanitizeField(element.name, "name", sanitize),
40
+ role: sanitizeField(element.role ?? readAttribute(element, "role"), "role", sanitize),
41
+ type: sanitizeField(element.type, "type", sanitize),
42
+ href: sanitizeField(element.href, "href", sanitize),
43
+ label: sanitizeField(firstLabel(element, options.labelAttributes ?? defaultLabelAttributes), "label", sanitize)
44
+ };
45
+ if (options.captureText) info.text = sanitizeField(truncate(element.textContent ?? void 0, maxTextLength), "text", sanitize);
46
+ if (options.captureValue) info.value = sanitizeField(truncate(element.value, maxTextLength), "value", sanitize);
47
+ return info;
48
+ }
49
+ function throttleKey(event, target) {
50
+ return `${event.type}:${target.tagName ?? ""}:${target.id ?? ""}:${target.name ?? ""}:${target.label ?? ""}`;
51
+ }
52
+ function captureUserActionsIntegration(options = {}) {
53
+ const events = options.events ?? defaultEvents;
54
+ const level = options.level ?? "info";
55
+ const listenerCapture = options.listenerCapture ?? true;
56
+ const throttleMs = Math.max(0, Math.floor(options.throttleMs ?? 250));
57
+ const clock = options.clock ?? Date.now;
58
+ return {
59
+ name: "capture-user-actions",
60
+ setup(api) {
61
+ const root = options.root ?? globalThis.document;
62
+ if (!root?.addEventListener || !root.removeEventListener) return;
63
+ const lastSeen = /* @__PURE__ */ new Map();
64
+ let disposed = false;
65
+ const onAction = api.guard((event) => {
66
+ if (disposed) return;
67
+ const target = targetInfo(event.target, options);
68
+ if (options.ignore?.(event, target)) return;
69
+ const now = clock();
70
+ const key = throttleKey(event, target);
71
+ const last = lastSeen.get(key) ?? 0;
72
+ if (throttleMs > 0 && now - last < throttleMs) return;
73
+ lastSeen.set(key, now);
74
+ const action = {
75
+ type: event.type,
76
+ target
77
+ };
78
+ api.capture({
79
+ level,
80
+ message: `User action ${event.type}${target.label ? ` ${target.label}` : ""}`,
81
+ props: { browser: {
82
+ kind: "user-action",
83
+ action
84
+ } }
85
+ });
86
+ });
87
+ for (const event of events) root.addEventListener(event, onAction, listenerCapture);
88
+ return () => {
89
+ if (disposed) return;
90
+ disposed = true;
91
+ for (const event of events) root.removeEventListener(event, onAction, listenerCapture);
92
+ };
93
+ }
94
+ };
95
+ }
96
+ //#endregion
97
+ export { captureUserActionsIntegration };
98
+
99
+ //# sourceMappingURL=user-action-integration.js.map