@loggerjs/browser 0.4.0 → 0.5.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 (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":"user-action-integration.js","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,157 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region src/web-vitals-integration.ts
3
+ const defaultMetrics = [
4
+ "CLS",
5
+ "FCP",
6
+ "INP",
7
+ "LCP",
8
+ "TTFB"
9
+ ];
10
+ const thresholds = {
11
+ CLS: [.1, .25],
12
+ FCP: [1800, 3e3],
13
+ INP: [200, 500],
14
+ LCP: [2500, 4e3],
15
+ TTFB: [800, 1800]
16
+ };
17
+ function ratingFor(name, value) {
18
+ const [good, needsImprovement] = thresholds[name];
19
+ if (value <= good) return "good";
20
+ if (value <= needsImprovement) return "needs-improvement";
21
+ return "poor";
22
+ }
23
+ function metricId(name) {
24
+ return `${name.toLowerCase()}-${Date.now().toString(36)}`;
25
+ }
26
+ function entryValue(entry, field) {
27
+ const value = entry[field];
28
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
29
+ }
30
+ function hasMetric(metrics, name) {
31
+ return metrics.includes(name);
32
+ }
33
+ function supportsEntryType(Observer, type) {
34
+ const supported = Observer?.supportedEntryTypes;
35
+ return !supported || supported.includes(type);
36
+ }
37
+ function captureWebVitalsIntegration(options = {}) {
38
+ const metrics = options.metrics ?? defaultMetrics;
39
+ const level = options.level ?? "info";
40
+ const reportAllChanges = options.reportAllChanges ?? false;
41
+ const flushOnHidden = options.flushOnHidden ?? true;
42
+ return {
43
+ name: "capture-web-vitals",
44
+ setup(api) {
45
+ const Observer = options.PerformanceObserver ?? globalThis.PerformanceObserver;
46
+ const perf = options.performance ?? globalThis.performance;
47
+ const add = options.addEventListener ?? globalThis.addEventListener?.bind(globalThis);
48
+ const remove = options.removeEventListener ?? globalThis.removeEventListener?.bind(globalThis);
49
+ const disposers = [];
50
+ const currentValues = /* @__PURE__ */ new Map();
51
+ const reportedValues = /* @__PURE__ */ new Map();
52
+ const emitted = /* @__PURE__ */ new Set();
53
+ let disposed = false;
54
+ const emit = (name, value, final) => {
55
+ if (!Number.isFinite(value)) return;
56
+ const finalKey = `${name}:final`;
57
+ if (final && emitted.has(finalKey)) return;
58
+ const previous = reportedValues.get(name) ?? 0;
59
+ currentValues.set(name, value);
60
+ reportedValues.set(name, value);
61
+ if (final) emitted.add(finalKey);
62
+ const metric = {
63
+ name,
64
+ value,
65
+ delta: value - previous,
66
+ rating: ratingFor(name, value),
67
+ id: metricId(name),
68
+ final
69
+ };
70
+ api.capture({
71
+ level,
72
+ message: `Web vital ${metric.name} ${metric.value}`,
73
+ props: { webVital: metric }
74
+ });
75
+ };
76
+ const observe = (type, callback, init = {
77
+ type,
78
+ buffered: true
79
+ }) => {
80
+ if (!Observer || !supportsEntryType(Observer, type)) return;
81
+ try {
82
+ const observer = new Observer((list) => {
83
+ for (const entry of list.getEntries()) callback(entry);
84
+ });
85
+ observer.observe(init);
86
+ disposers.push(() => observer.disconnect());
87
+ } catch {}
88
+ };
89
+ if (hasMetric(metrics, "TTFB") && perf) {
90
+ const navigation = perf.getEntriesByType("navigation")[0];
91
+ if (navigation) {
92
+ const responseStart = entryValue(navigation, "responseStart");
93
+ const startTime = entryValue(navigation, "startTime") ?? 0;
94
+ if (responseStart !== void 0) emit("TTFB", responseStart - startTime, true);
95
+ }
96
+ }
97
+ if (hasMetric(metrics, "FCP") && perf) {
98
+ const existing = perf.getEntriesByName("first-contentful-paint")[0];
99
+ if (existing) emit("FCP", existing.startTime, true);
100
+ observe("paint", (entry) => {
101
+ if (entry.name === "first-contentful-paint") emit("FCP", entry.startTime, true);
102
+ });
103
+ }
104
+ if (hasMetric(metrics, "CLS")) observe("layout-shift", (entry) => {
105
+ if (entry.hadRecentInput === true) return;
106
+ const value = entryValue(entry, "value") ?? 0;
107
+ const next = (currentValues.get("CLS") ?? 0) + value;
108
+ if (reportAllChanges) emit("CLS", next, false);
109
+ else currentValues.set("CLS", next);
110
+ });
111
+ if (hasMetric(metrics, "LCP")) observe("largest-contentful-paint", (entry) => {
112
+ currentValues.set("LCP", entry.startTime);
113
+ if (reportAllChanges) emit("LCP", entry.startTime, false);
114
+ });
115
+ if (hasMetric(metrics, "INP")) observe("event", (entry) => {
116
+ const duration = entryValue(entry, "duration") ?? 0;
117
+ if (duration <= (currentValues.get("INP") ?? 0)) return;
118
+ currentValues.set("INP", duration);
119
+ if (reportAllChanges) emit("INP", duration, false);
120
+ }, {
121
+ type: "event",
122
+ buffered: true,
123
+ durationThreshold: 40
124
+ });
125
+ const flushFinal = api.guard(() => {
126
+ const cls = currentValues.get("CLS");
127
+ if (cls !== void 0 && cls > 0) emit("CLS", cls, true);
128
+ const lcp = currentValues.get("LCP");
129
+ if (lcp !== void 0) emit("LCP", lcp, true);
130
+ const inp = currentValues.get("INP");
131
+ if (inp !== void 0) emit("INP", inp, true);
132
+ });
133
+ if (flushOnHidden && add && remove) {
134
+ const onPageHide = () => flushFinal();
135
+ const onVisibilityChange = () => {
136
+ if (typeof document !== "undefined" && document.visibilityState === "hidden") flushFinal();
137
+ };
138
+ add("pagehide", onPageHide);
139
+ add("visibilitychange", onVisibilityChange);
140
+ disposers.push(() => {
141
+ remove("pagehide", onPageHide);
142
+ remove("visibilitychange", onVisibilityChange);
143
+ });
144
+ }
145
+ return () => {
146
+ if (disposed) return;
147
+ disposed = true;
148
+ flushFinal();
149
+ for (const dispose of disposers) dispose();
150
+ };
151
+ }
152
+ };
153
+ }
154
+ //#endregion
155
+ exports.captureWebVitalsIntegration = captureWebVitalsIntegration;
156
+
157
+ //# sourceMappingURL=web-vitals-integration.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"web-vitals-integration.cjs","names":[],"sources":["../src/web-vitals-integration.ts"],"sourcesContent":["import type { Integration, IntegrationSetupContext, LoggerLevel } from \"@loggerjs/core\";\n\nexport type WebVitalName = \"CLS\" | \"FCP\" | \"INP\" | \"LCP\" | \"TTFB\";\nexport type WebVitalRating = \"good\" | \"needs-improvement\" | \"poor\";\n\nexport interface WebVitalMetric {\n name: WebVitalName;\n value: number;\n delta: number;\n rating: WebVitalRating;\n id: string;\n final: boolean;\n}\n\nexport interface CaptureWebVitalsOptions {\n metrics?: readonly WebVitalName[];\n level?: LoggerLevel;\n reportAllChanges?: boolean;\n flushOnHidden?: boolean;\n PerformanceObserver?: typeof PerformanceObserver;\n performance?: Pick<Performance, \"getEntriesByName\" | \"getEntriesByType\">;\n addEventListener?: typeof globalThis.addEventListener;\n removeEventListener?: typeof globalThis.removeEventListener;\n}\n\ntype PerformanceEntryLike = PerformanceEntry & Record<string, unknown>;\n\nconst defaultMetrics: readonly WebVitalName[] = [\"CLS\", \"FCP\", \"INP\", \"LCP\", \"TTFB\"];\n\nconst thresholds: Record<WebVitalName, readonly [number, number]> = {\n CLS: [0.1, 0.25],\n FCP: [1800, 3000],\n INP: [200, 500],\n LCP: [2500, 4000],\n TTFB: [800, 1800],\n};\n\nfunction ratingFor(name: WebVitalName, value: number): WebVitalRating {\n const [good, needsImprovement] = thresholds[name];\n if (value <= good) return \"good\";\n if (value <= needsImprovement) return \"needs-improvement\";\n return \"poor\";\n}\n\nfunction metricId(name: WebVitalName): string {\n return `${name.toLowerCase()}-${Date.now().toString(36)}`;\n}\n\nfunction entryValue(entry: PerformanceEntryLike, field: string): number | undefined {\n const value = entry[field];\n return typeof value === \"number\" && Number.isFinite(value) ? value : undefined;\n}\n\nfunction hasMetric(metrics: readonly WebVitalName[], name: WebVitalName): boolean {\n return metrics.includes(name);\n}\n\nfunction supportsEntryType(\n Observer: typeof PerformanceObserver | undefined,\n type: string,\n): boolean {\n const supported = Observer?.supportedEntryTypes;\n return !supported || supported.includes(type);\n}\n\nexport function captureWebVitalsIntegration(options: CaptureWebVitalsOptions = {}): Integration {\n const metrics = options.metrics ?? defaultMetrics;\n const level = options.level ?? \"info\";\n const reportAllChanges = options.reportAllChanges ?? false;\n const flushOnHidden = options.flushOnHidden ?? true;\n\n return {\n name: \"capture-web-vitals\",\n setup(api: IntegrationSetupContext) {\n const Observer = options.PerformanceObserver ?? globalThis.PerformanceObserver;\n const perf = options.performance ?? globalThis.performance;\n const add = options.addEventListener ?? globalThis.addEventListener?.bind(globalThis);\n const remove =\n options.removeEventListener ?? globalThis.removeEventListener?.bind(globalThis);\n const disposers: Array<() => void> = [];\n const currentValues = new Map<WebVitalName, number>();\n const reportedValues = new Map<WebVitalName, number>();\n const emitted = new Set<string>();\n let disposed = false;\n\n const emit = (name: WebVitalName, value: number, final: boolean) => {\n if (!Number.isFinite(value)) return;\n const finalKey = `${name}:final`;\n if (final && emitted.has(finalKey)) return;\n const previous = reportedValues.get(name) ?? 0;\n currentValues.set(name, value);\n reportedValues.set(name, value);\n if (final) emitted.add(finalKey);\n const metric: WebVitalMetric = {\n name,\n value,\n delta: value - previous,\n rating: ratingFor(name, value),\n id: metricId(name),\n final,\n };\n api.capture({\n level,\n message: `Web vital ${metric.name} ${metric.value}`,\n props: { webVital: metric },\n });\n };\n\n const observe = (\n type: string,\n callback: (entry: PerformanceEntryLike) => void,\n init: PerformanceObserverInit = { type, buffered: true },\n ) => {\n if (!Observer || !supportsEntryType(Observer, type)) return;\n try {\n const observer = new Observer((list) => {\n for (const entry of list.getEntries()) callback(entry as PerformanceEntryLike);\n });\n observer.observe(init);\n disposers.push(() => observer.disconnect());\n } catch {\n // Unsupported observer types vary across browsers; missing vitals are simply skipped.\n }\n };\n\n if (hasMetric(metrics, \"TTFB\") && perf) {\n const navigation = perf.getEntriesByType(\"navigation\")[0] as\n | PerformanceEntryLike\n | undefined;\n if (navigation) {\n const responseStart = entryValue(navigation, \"responseStart\");\n const startTime = entryValue(navigation, \"startTime\") ?? 0;\n if (responseStart !== undefined) emit(\"TTFB\", responseStart - startTime, true);\n }\n }\n\n if (hasMetric(metrics, \"FCP\") && perf) {\n const existing = perf.getEntriesByName(\"first-contentful-paint\")[0];\n if (existing) emit(\"FCP\", existing.startTime, true);\n observe(\"paint\", (entry) => {\n if (entry.name === \"first-contentful-paint\") emit(\"FCP\", entry.startTime, true);\n });\n }\n\n if (hasMetric(metrics, \"CLS\")) {\n observe(\"layout-shift\", (entry) => {\n if (entry.hadRecentInput === true) return;\n const value = entryValue(entry, \"value\") ?? 0;\n const next = (currentValues.get(\"CLS\") ?? 0) + value;\n if (reportAllChanges) emit(\"CLS\", next, false);\n else currentValues.set(\"CLS\", next);\n });\n }\n\n if (hasMetric(metrics, \"LCP\")) {\n observe(\"largest-contentful-paint\", (entry) => {\n currentValues.set(\"LCP\", entry.startTime);\n if (reportAllChanges) emit(\"LCP\", entry.startTime, false);\n });\n }\n\n if (hasMetric(metrics, \"INP\")) {\n observe(\n \"event\",\n (entry) => {\n const duration = entryValue(entry, \"duration\") ?? 0;\n if (duration <= (currentValues.get(\"INP\") ?? 0)) return;\n currentValues.set(\"INP\", duration);\n if (reportAllChanges) emit(\"INP\", duration, false);\n },\n { type: \"event\", buffered: true, durationThreshold: 40 } as PerformanceObserverInit,\n );\n }\n\n const flushFinal = api.guard(() => {\n const cls = currentValues.get(\"CLS\");\n if (cls !== undefined && cls > 0) emit(\"CLS\", cls, true);\n const lcp = currentValues.get(\"LCP\");\n if (lcp !== undefined) emit(\"LCP\", lcp, true);\n const inp = currentValues.get(\"INP\");\n if (inp !== undefined) emit(\"INP\", inp, true);\n });\n\n if (flushOnHidden && add && remove) {\n const onPageHide = () => flushFinal();\n const onVisibilityChange = () => {\n if (typeof document !== \"undefined\" && document.visibilityState === \"hidden\")\n flushFinal();\n };\n add(\"pagehide\", onPageHide);\n add(\"visibilitychange\", onVisibilityChange);\n disposers.push(() => {\n remove(\"pagehide\", onPageHide);\n remove(\"visibilitychange\", onVisibilityChange);\n });\n }\n\n return () => {\n if (disposed) return;\n disposed = true;\n flushFinal();\n for (const dispose of disposers) dispose();\n };\n },\n };\n}\n"],"mappings":"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"}
@@ -0,0 +1,156 @@
1
+ //#region src/web-vitals-integration.ts
2
+ const defaultMetrics = [
3
+ "CLS",
4
+ "FCP",
5
+ "INP",
6
+ "LCP",
7
+ "TTFB"
8
+ ];
9
+ const thresholds = {
10
+ CLS: [.1, .25],
11
+ FCP: [1800, 3e3],
12
+ INP: [200, 500],
13
+ LCP: [2500, 4e3],
14
+ TTFB: [800, 1800]
15
+ };
16
+ function ratingFor(name, value) {
17
+ const [good, needsImprovement] = thresholds[name];
18
+ if (value <= good) return "good";
19
+ if (value <= needsImprovement) return "needs-improvement";
20
+ return "poor";
21
+ }
22
+ function metricId(name) {
23
+ return `${name.toLowerCase()}-${Date.now().toString(36)}`;
24
+ }
25
+ function entryValue(entry, field) {
26
+ const value = entry[field];
27
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
28
+ }
29
+ function hasMetric(metrics, name) {
30
+ return metrics.includes(name);
31
+ }
32
+ function supportsEntryType(Observer, type) {
33
+ const supported = Observer?.supportedEntryTypes;
34
+ return !supported || supported.includes(type);
35
+ }
36
+ function captureWebVitalsIntegration(options = {}) {
37
+ const metrics = options.metrics ?? defaultMetrics;
38
+ const level = options.level ?? "info";
39
+ const reportAllChanges = options.reportAllChanges ?? false;
40
+ const flushOnHidden = options.flushOnHidden ?? true;
41
+ return {
42
+ name: "capture-web-vitals",
43
+ setup(api) {
44
+ const Observer = options.PerformanceObserver ?? globalThis.PerformanceObserver;
45
+ const perf = options.performance ?? globalThis.performance;
46
+ const add = options.addEventListener ?? globalThis.addEventListener?.bind(globalThis);
47
+ const remove = options.removeEventListener ?? globalThis.removeEventListener?.bind(globalThis);
48
+ const disposers = [];
49
+ const currentValues = /* @__PURE__ */ new Map();
50
+ const reportedValues = /* @__PURE__ */ new Map();
51
+ const emitted = /* @__PURE__ */ new Set();
52
+ let disposed = false;
53
+ const emit = (name, value, final) => {
54
+ if (!Number.isFinite(value)) return;
55
+ const finalKey = `${name}:final`;
56
+ if (final && emitted.has(finalKey)) return;
57
+ const previous = reportedValues.get(name) ?? 0;
58
+ currentValues.set(name, value);
59
+ reportedValues.set(name, value);
60
+ if (final) emitted.add(finalKey);
61
+ const metric = {
62
+ name,
63
+ value,
64
+ delta: value - previous,
65
+ rating: ratingFor(name, value),
66
+ id: metricId(name),
67
+ final
68
+ };
69
+ api.capture({
70
+ level,
71
+ message: `Web vital ${metric.name} ${metric.value}`,
72
+ props: { webVital: metric }
73
+ });
74
+ };
75
+ const observe = (type, callback, init = {
76
+ type,
77
+ buffered: true
78
+ }) => {
79
+ if (!Observer || !supportsEntryType(Observer, type)) return;
80
+ try {
81
+ const observer = new Observer((list) => {
82
+ for (const entry of list.getEntries()) callback(entry);
83
+ });
84
+ observer.observe(init);
85
+ disposers.push(() => observer.disconnect());
86
+ } catch {}
87
+ };
88
+ if (hasMetric(metrics, "TTFB") && perf) {
89
+ const navigation = perf.getEntriesByType("navigation")[0];
90
+ if (navigation) {
91
+ const responseStart = entryValue(navigation, "responseStart");
92
+ const startTime = entryValue(navigation, "startTime") ?? 0;
93
+ if (responseStart !== void 0) emit("TTFB", responseStart - startTime, true);
94
+ }
95
+ }
96
+ if (hasMetric(metrics, "FCP") && perf) {
97
+ const existing = perf.getEntriesByName("first-contentful-paint")[0];
98
+ if (existing) emit("FCP", existing.startTime, true);
99
+ observe("paint", (entry) => {
100
+ if (entry.name === "first-contentful-paint") emit("FCP", entry.startTime, true);
101
+ });
102
+ }
103
+ if (hasMetric(metrics, "CLS")) observe("layout-shift", (entry) => {
104
+ if (entry.hadRecentInput === true) return;
105
+ const value = entryValue(entry, "value") ?? 0;
106
+ const next = (currentValues.get("CLS") ?? 0) + value;
107
+ if (reportAllChanges) emit("CLS", next, false);
108
+ else currentValues.set("CLS", next);
109
+ });
110
+ if (hasMetric(metrics, "LCP")) observe("largest-contentful-paint", (entry) => {
111
+ currentValues.set("LCP", entry.startTime);
112
+ if (reportAllChanges) emit("LCP", entry.startTime, false);
113
+ });
114
+ if (hasMetric(metrics, "INP")) observe("event", (entry) => {
115
+ const duration = entryValue(entry, "duration") ?? 0;
116
+ if (duration <= (currentValues.get("INP") ?? 0)) return;
117
+ currentValues.set("INP", duration);
118
+ if (reportAllChanges) emit("INP", duration, false);
119
+ }, {
120
+ type: "event",
121
+ buffered: true,
122
+ durationThreshold: 40
123
+ });
124
+ const flushFinal = api.guard(() => {
125
+ const cls = currentValues.get("CLS");
126
+ if (cls !== void 0 && cls > 0) emit("CLS", cls, true);
127
+ const lcp = currentValues.get("LCP");
128
+ if (lcp !== void 0) emit("LCP", lcp, true);
129
+ const inp = currentValues.get("INP");
130
+ if (inp !== void 0) emit("INP", inp, true);
131
+ });
132
+ if (flushOnHidden && add && remove) {
133
+ const onPageHide = () => flushFinal();
134
+ const onVisibilityChange = () => {
135
+ if (typeof document !== "undefined" && document.visibilityState === "hidden") flushFinal();
136
+ };
137
+ add("pagehide", onPageHide);
138
+ add("visibilitychange", onVisibilityChange);
139
+ disposers.push(() => {
140
+ remove("pagehide", onPageHide);
141
+ remove("visibilitychange", onVisibilityChange);
142
+ });
143
+ }
144
+ return () => {
145
+ if (disposed) return;
146
+ disposed = true;
147
+ flushFinal();
148
+ for (const dispose of disposers) dispose();
149
+ };
150
+ }
151
+ };
152
+ }
153
+ //#endregion
154
+ export { captureWebVitalsIntegration };
155
+
156
+ //# sourceMappingURL=web-vitals-integration.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"web-vitals-integration.js","names":[],"sources":["../src/web-vitals-integration.ts"],"sourcesContent":["import type { Integration, IntegrationSetupContext, LoggerLevel } from \"@loggerjs/core\";\n\nexport type WebVitalName = \"CLS\" | \"FCP\" | \"INP\" | \"LCP\" | \"TTFB\";\nexport type WebVitalRating = \"good\" | \"needs-improvement\" | \"poor\";\n\nexport interface WebVitalMetric {\n name: WebVitalName;\n value: number;\n delta: number;\n rating: WebVitalRating;\n id: string;\n final: boolean;\n}\n\nexport interface CaptureWebVitalsOptions {\n metrics?: readonly WebVitalName[];\n level?: LoggerLevel;\n reportAllChanges?: boolean;\n flushOnHidden?: boolean;\n PerformanceObserver?: typeof PerformanceObserver;\n performance?: Pick<Performance, \"getEntriesByName\" | \"getEntriesByType\">;\n addEventListener?: typeof globalThis.addEventListener;\n removeEventListener?: typeof globalThis.removeEventListener;\n}\n\ntype PerformanceEntryLike = PerformanceEntry & Record<string, unknown>;\n\nconst defaultMetrics: readonly WebVitalName[] = [\"CLS\", \"FCP\", \"INP\", \"LCP\", \"TTFB\"];\n\nconst thresholds: Record<WebVitalName, readonly [number, number]> = {\n CLS: [0.1, 0.25],\n FCP: [1800, 3000],\n INP: [200, 500],\n LCP: [2500, 4000],\n TTFB: [800, 1800],\n};\n\nfunction ratingFor(name: WebVitalName, value: number): WebVitalRating {\n const [good, needsImprovement] = thresholds[name];\n if (value <= good) return \"good\";\n if (value <= needsImprovement) return \"needs-improvement\";\n return \"poor\";\n}\n\nfunction metricId(name: WebVitalName): string {\n return `${name.toLowerCase()}-${Date.now().toString(36)}`;\n}\n\nfunction entryValue(entry: PerformanceEntryLike, field: string): number | undefined {\n const value = entry[field];\n return typeof value === \"number\" && Number.isFinite(value) ? value : undefined;\n}\n\nfunction hasMetric(metrics: readonly WebVitalName[], name: WebVitalName): boolean {\n return metrics.includes(name);\n}\n\nfunction supportsEntryType(\n Observer: typeof PerformanceObserver | undefined,\n type: string,\n): boolean {\n const supported = Observer?.supportedEntryTypes;\n return !supported || supported.includes(type);\n}\n\nexport function captureWebVitalsIntegration(options: CaptureWebVitalsOptions = {}): Integration {\n const metrics = options.metrics ?? defaultMetrics;\n const level = options.level ?? \"info\";\n const reportAllChanges = options.reportAllChanges ?? false;\n const flushOnHidden = options.flushOnHidden ?? true;\n\n return {\n name: \"capture-web-vitals\",\n setup(api: IntegrationSetupContext) {\n const Observer = options.PerformanceObserver ?? globalThis.PerformanceObserver;\n const perf = options.performance ?? globalThis.performance;\n const add = options.addEventListener ?? globalThis.addEventListener?.bind(globalThis);\n const remove =\n options.removeEventListener ?? globalThis.removeEventListener?.bind(globalThis);\n const disposers: Array<() => void> = [];\n const currentValues = new Map<WebVitalName, number>();\n const reportedValues = new Map<WebVitalName, number>();\n const emitted = new Set<string>();\n let disposed = false;\n\n const emit = (name: WebVitalName, value: number, final: boolean) => {\n if (!Number.isFinite(value)) return;\n const finalKey = `${name}:final`;\n if (final && emitted.has(finalKey)) return;\n const previous = reportedValues.get(name) ?? 0;\n currentValues.set(name, value);\n reportedValues.set(name, value);\n if (final) emitted.add(finalKey);\n const metric: WebVitalMetric = {\n name,\n value,\n delta: value - previous,\n rating: ratingFor(name, value),\n id: metricId(name),\n final,\n };\n api.capture({\n level,\n message: `Web vital ${metric.name} ${metric.value}`,\n props: { webVital: metric },\n });\n };\n\n const observe = (\n type: string,\n callback: (entry: PerformanceEntryLike) => void,\n init: PerformanceObserverInit = { type, buffered: true },\n ) => {\n if (!Observer || !supportsEntryType(Observer, type)) return;\n try {\n const observer = new Observer((list) => {\n for (const entry of list.getEntries()) callback(entry as PerformanceEntryLike);\n });\n observer.observe(init);\n disposers.push(() => observer.disconnect());\n } catch {\n // Unsupported observer types vary across browsers; missing vitals are simply skipped.\n }\n };\n\n if (hasMetric(metrics, \"TTFB\") && perf) {\n const navigation = perf.getEntriesByType(\"navigation\")[0] as\n | PerformanceEntryLike\n | undefined;\n if (navigation) {\n const responseStart = entryValue(navigation, \"responseStart\");\n const startTime = entryValue(navigation, \"startTime\") ?? 0;\n if (responseStart !== undefined) emit(\"TTFB\", responseStart - startTime, true);\n }\n }\n\n if (hasMetric(metrics, \"FCP\") && perf) {\n const existing = perf.getEntriesByName(\"first-contentful-paint\")[0];\n if (existing) emit(\"FCP\", existing.startTime, true);\n observe(\"paint\", (entry) => {\n if (entry.name === \"first-contentful-paint\") emit(\"FCP\", entry.startTime, true);\n });\n }\n\n if (hasMetric(metrics, \"CLS\")) {\n observe(\"layout-shift\", (entry) => {\n if (entry.hadRecentInput === true) return;\n const value = entryValue(entry, \"value\") ?? 0;\n const next = (currentValues.get(\"CLS\") ?? 0) + value;\n if (reportAllChanges) emit(\"CLS\", next, false);\n else currentValues.set(\"CLS\", next);\n });\n }\n\n if (hasMetric(metrics, \"LCP\")) {\n observe(\"largest-contentful-paint\", (entry) => {\n currentValues.set(\"LCP\", entry.startTime);\n if (reportAllChanges) emit(\"LCP\", entry.startTime, false);\n });\n }\n\n if (hasMetric(metrics, \"INP\")) {\n observe(\n \"event\",\n (entry) => {\n const duration = entryValue(entry, \"duration\") ?? 0;\n if (duration <= (currentValues.get(\"INP\") ?? 0)) return;\n currentValues.set(\"INP\", duration);\n if (reportAllChanges) emit(\"INP\", duration, false);\n },\n { type: \"event\", buffered: true, durationThreshold: 40 } as PerformanceObserverInit,\n );\n }\n\n const flushFinal = api.guard(() => {\n const cls = currentValues.get(\"CLS\");\n if (cls !== undefined && cls > 0) emit(\"CLS\", cls, true);\n const lcp = currentValues.get(\"LCP\");\n if (lcp !== undefined) emit(\"LCP\", lcp, true);\n const inp = currentValues.get(\"INP\");\n if (inp !== undefined) emit(\"INP\", inp, true);\n });\n\n if (flushOnHidden && add && remove) {\n const onPageHide = () => flushFinal();\n const onVisibilityChange = () => {\n if (typeof document !== \"undefined\" && document.visibilityState === \"hidden\")\n flushFinal();\n };\n add(\"pagehide\", onPageHide);\n add(\"visibilitychange\", onVisibilityChange);\n disposers.push(() => {\n remove(\"pagehide\", onPageHide);\n remove(\"visibilitychange\", onVisibilityChange);\n });\n }\n\n return () => {\n if (disposed) return;\n disposed = true;\n flushFinal();\n for (const dispose of disposers) dispose();\n };\n },\n };\n}\n"],"mappings":"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"}
@@ -0,0 +1,158 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let _loggerjs_core = require("@loggerjs/core");
3
+ //#region src/http-capture-utils.ts
4
+ function nowMs() {
5
+ return typeof performance !== "undefined" ? performance.now() : Date.now();
6
+ }
7
+ function durationMs(started) {
8
+ return nowMs() - started;
9
+ }
10
+ function sanitizeHttpUrl(rawUrl, sanitizer) {
11
+ const stripped = stripSensitiveUrlParts(rawUrl);
12
+ return sanitizer ? sanitizer(stripped) : stripped;
13
+ }
14
+ function shouldSample(sampleRate, random) {
15
+ if (sampleRate >= 1) return true;
16
+ if (sampleRate <= 0) return false;
17
+ return random() < sampleRate;
18
+ }
19
+ function stripSensitiveUrlParts(rawUrl) {
20
+ if (!/^[a-zA-Z][a-zA-Z\d+.-]*:/.test(rawUrl)) return rawUrl.split(/[?#]/, 1)[0] ?? rawUrl;
21
+ try {
22
+ const url = new URL(rawUrl);
23
+ url.search = "";
24
+ url.hash = "";
25
+ return url.toString();
26
+ } catch {
27
+ return rawUrl.split(/[?#]/, 1)[0] ?? rawUrl;
28
+ }
29
+ }
30
+ //#endregion
31
+ //#region src/websocket-integration.ts
32
+ const registryKey = "browser.WebSocket";
33
+ const textEncoder = new TextEncoder();
34
+ function dataType(data) {
35
+ if (typeof data === "string") return "string";
36
+ if (data instanceof ArrayBuffer) return "arraybuffer";
37
+ if (ArrayBuffer.isView(data)) return data.constructor.name;
38
+ return data?.constructor?.name ?? typeof data;
39
+ }
40
+ function dataByteLength(data) {
41
+ if (typeof data === "string") return textEncoder.encode(data).byteLength;
42
+ if (data instanceof ArrayBuffer) return data.byteLength;
43
+ if (ArrayBuffer.isView(data)) return data.byteLength;
44
+ const size = data?.size;
45
+ return typeof size === "number" && Number.isFinite(size) ? size : void 0;
46
+ }
47
+ function messagePayload(direction, data, captureMessageData) {
48
+ return {
49
+ direction,
50
+ dataType: dataType(data),
51
+ byteLength: dataByteLength(data),
52
+ data: captureMessageData ? (0, _loggerjs_core.normalizeValue)(data, {
53
+ maxDepth: 4,
54
+ maxObjectKeys: 80
55
+ }) : void 0
56
+ };
57
+ }
58
+ function closeEventPayload(event) {
59
+ const close = event;
60
+ return {
61
+ code: close.code,
62
+ reason: close.reason,
63
+ wasClean: close.wasClean
64
+ };
65
+ }
66
+ function copyStaticConstants(target, source) {
67
+ for (const key of [
68
+ "CONNECTING",
69
+ "OPEN",
70
+ "CLOSING",
71
+ "CLOSED"
72
+ ]) if (source[key] !== void 0) target[key] = source[key];
73
+ }
74
+ function captureWebSocketIntegration(options = {}) {
75
+ const level = options.level ?? "debug";
76
+ const captureConnect = options.captureConnect ?? true;
77
+ const captureOpen = options.captureOpen ?? true;
78
+ const captureClose = options.captureClose ?? true;
79
+ const captureError = options.captureError ?? true;
80
+ const captureMessages = options.captureMessages ?? false;
81
+ const captureSentMessages = options.captureSentMessages ?? false;
82
+ const captureMessageData = options.captureMessageData ?? false;
83
+ const sampleRate = options.sampleRate ?? 1;
84
+ const random = options.random ?? Math.random;
85
+ return {
86
+ name: "capture-websocket",
87
+ setup(api) {
88
+ const globalWebSocket = globalThis.WebSocket;
89
+ const current = options.WebSocket ?? globalWebSocket;
90
+ if (!current) return;
91
+ const original = api.unpatched.get(registryKey) ?? current;
92
+ api.unpatched.set(registryKey, original);
93
+ const capture = api.guard((input) => api.capture(input));
94
+ const PatchedWebSocket = function WebSocket(url, protocols) {
95
+ const started = nowMs();
96
+ const sanitizedUrl = sanitizeHttpUrl(String(url), options.sanitizeUrl);
97
+ const socket = new original(url, protocols);
98
+ const captureLifecycle = (event, extra = {}, error) => {
99
+ if (!shouldSample(sampleRate, random)) return;
100
+ capture({
101
+ level: event === "error" ? "error" : level,
102
+ message: `WebSocket ${event} ${sanitizedUrl}`,
103
+ error,
104
+ props: { websocket: {
105
+ kind: "websocket",
106
+ event,
107
+ url: sanitizedUrl,
108
+ durationMs: durationMs(started),
109
+ ...extra
110
+ } }
111
+ });
112
+ };
113
+ if (captureConnect) captureLifecycle("connect");
114
+ if (captureOpen) socket.addEventListener?.("open", () => captureLifecycle("open"));
115
+ if (captureClose) socket.addEventListener?.("close", (event) => captureLifecycle("close", closeEventPayload(event)));
116
+ if (captureError) socket.addEventListener?.("error", (event) => captureLifecycle("error", { type: event.type }, event));
117
+ if (captureMessages) socket.addEventListener?.("message", (event) => {
118
+ const message = event;
119
+ capture({
120
+ level,
121
+ message: `WebSocket message ${sanitizedUrl}`,
122
+ props: { websocket: {
123
+ kind: "websocket-message",
124
+ url: sanitizedUrl,
125
+ message: messagePayload("incoming", message.data, captureMessageData)
126
+ } }
127
+ });
128
+ });
129
+ if (captureSentMessages && socket.send) {
130
+ const originalSend = socket.send.bind(socket);
131
+ socket.send = (data) => {
132
+ capture({
133
+ level,
134
+ message: `WebSocket send ${sanitizedUrl}`,
135
+ props: { websocket: {
136
+ kind: "websocket-message",
137
+ url: sanitizedUrl,
138
+ message: messagePayload("outgoing", data, captureMessageData)
139
+ } }
140
+ });
141
+ return originalSend(data);
142
+ };
143
+ }
144
+ return socket;
145
+ };
146
+ PatchedWebSocket.prototype = original.prototype;
147
+ copyStaticConstants(PatchedWebSocket, original);
148
+ globalThis.WebSocket = PatchedWebSocket;
149
+ return () => {
150
+ globalThis.WebSocket = current;
151
+ };
152
+ }
153
+ };
154
+ }
155
+ //#endregion
156
+ exports.captureWebSocketIntegration = captureWebSocketIntegration;
157
+
158
+ //# sourceMappingURL=websocket-integration.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"websocket-integration.cjs","names":["record"],"sources":["../src/http-capture-utils.ts","../src/websocket-integration.ts"],"sourcesContent":["export function nowMs(): number {\n return typeof performance !== \"undefined\" ? performance.now() : Date.now();\n}\n\nexport function durationMs(started: number): number {\n return nowMs() - started;\n}\n\nexport function sanitizeHttpUrl(rawUrl: string, sanitizer?: (url: string) => string): string {\n const stripped = stripSensitiveUrlParts(rawUrl);\n return sanitizer ? sanitizer(stripped) : stripped;\n}\n\nexport function pickAllowedHeaders(\n headers: Headers | undefined,\n allowList: readonly string[] | undefined,\n): Record<string, string> | undefined {\n if (!headers || !allowList || allowList.length === 0) return undefined;\n const out: Record<string, string> = {};\n for (const name of allowList) {\n const value = headers.get(name);\n if (value !== null) out[name.toLowerCase()] = value;\n }\n return Object.keys(out).length > 0 ? out : undefined;\n}\n\nexport function headersFromInit(headers: HeadersInit | undefined): Headers | undefined {\n if (!headers) return undefined;\n try {\n return new Headers(headers);\n } catch {\n return undefined;\n }\n}\n\nexport function shouldSample(sampleRate: number, random: () => number): boolean {\n if (sampleRate >= 1) return true;\n if (sampleRate <= 0) return false;\n return random() < sampleRate;\n}\n\nfunction stripSensitiveUrlParts(rawUrl: string): string {\n if (!/^[a-zA-Z][a-zA-Z\\d+.-]*:/.test(rawUrl)) {\n return rawUrl.split(/[?#]/, 1)[0] ?? rawUrl;\n }\n\n try {\n const url = new URL(rawUrl);\n url.search = \"\";\n url.hash = \"\";\n return url.toString();\n } catch {\n return rawUrl.split(/[?#]/, 1)[0] ?? rawUrl;\n }\n}\n","import {\n normalizeValue,\n type CaptureInput,\n type Integration,\n type IntegrationSetupContext,\n type LoggerLevel,\n} from \"@loggerjs/core\";\nimport { durationMs, nowMs, sanitizeHttpUrl, shouldSample } from \"./http-capture-utils\";\n\ntype BrowserWebSocketSendPayload = Parameters<WebSocket[\"send\"]>[0];\n\nexport interface BrowserCapturedWebSocketLike {\n url?: string;\n send?: (data: BrowserWebSocketSendPayload) => void;\n close?: (code?: number, reason?: string) => void;\n addEventListener?: (type: string, listener: EventListenerOrEventListenerObject) => void;\n removeEventListener?: (type: string, listener: EventListenerOrEventListenerObject) => void;\n}\n\nexport interface BrowserCapturedWebSocketConstructor {\n new (url: string | URL, protocols?: string | string[]): BrowserCapturedWebSocketLike;\n prototype?: unknown;\n CONNECTING?: number;\n OPEN?: number;\n CLOSING?: number;\n CLOSED?: number;\n}\n\nexport type BrowserWebSocketDirection = \"incoming\" | \"outgoing\";\n\nexport interface BrowserWebSocketMessagePayload {\n direction: BrowserWebSocketDirection;\n dataType: string;\n byteLength?: number;\n data?: unknown;\n}\n\nexport interface CaptureWebSocketOptions {\n level?: LoggerLevel;\n captureConnect?: boolean;\n captureOpen?: boolean;\n captureClose?: boolean;\n captureError?: boolean;\n captureMessages?: boolean;\n captureSentMessages?: boolean;\n captureMessageData?: boolean;\n sampleRate?: number;\n random?: () => number;\n sanitizeUrl?: (url: string) => string;\n WebSocket?: BrowserCapturedWebSocketConstructor;\n}\n\nconst registryKey = \"browser.WebSocket\";\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 messagePayload(\n direction: BrowserWebSocketDirection,\n data: unknown,\n captureMessageData: boolean,\n): BrowserWebSocketMessagePayload {\n return {\n direction,\n dataType: dataType(data),\n byteLength: dataByteLength(data),\n data: captureMessageData ? normalizeValue(data, { maxDepth: 4, maxObjectKeys: 80 }) : undefined,\n };\n}\n\nfunction closeEventPayload(event: Event): Record<string, unknown> {\n const close = event as CloseEvent;\n return {\n code: close.code,\n reason: close.reason,\n wasClean: close.wasClean,\n };\n}\n\nfunction copyStaticConstants(\n target: BrowserCapturedWebSocketConstructor,\n source: BrowserCapturedWebSocketConstructor,\n) {\n for (const key of [\"CONNECTING\", \"OPEN\", \"CLOSING\", \"CLOSED\"] as const) {\n if (source[key] !== undefined) target[key] = source[key];\n }\n}\n\nexport function captureWebSocketIntegration(options: CaptureWebSocketOptions = {}): Integration {\n const level = options.level ?? \"debug\";\n const captureConnect = options.captureConnect ?? true;\n const captureOpen = options.captureOpen ?? true;\n const captureClose = options.captureClose ?? true;\n const captureError = options.captureError ?? true;\n const captureMessages = options.captureMessages ?? false;\n const captureSentMessages = options.captureSentMessages ?? false;\n const captureMessageData = options.captureMessageData ?? false;\n const sampleRate = options.sampleRate ?? 1;\n const random = options.random ?? Math.random;\n\n return {\n name: \"capture-websocket\",\n setup(api: IntegrationSetupContext) {\n const globalWebSocket = globalThis.WebSocket as\n | BrowserCapturedWebSocketConstructor\n | undefined;\n const current = options.WebSocket ?? globalWebSocket;\n if (!current) return;\n const original =\n api.unpatched.get<BrowserCapturedWebSocketConstructor>(registryKey) ?? current;\n api.unpatched.set(registryKey, original);\n const capture = api.guard((input: CaptureInput) => api.capture(input));\n\n const PatchedWebSocket = function WebSocket(\n this: BrowserCapturedWebSocketLike,\n url: string | URL,\n protocols?: string | string[],\n ) {\n const started = nowMs();\n const sanitizedUrl = sanitizeHttpUrl(String(url), options.sanitizeUrl);\n const socket = new original(url, protocols);\n\n const captureLifecycle = (\n event: \"close\" | \"connect\" | \"error\" | \"open\",\n extra: Record<string, unknown> = {},\n error?: unknown,\n ) => {\n if (!shouldSample(sampleRate, random)) return;\n capture({\n level: event === \"error\" ? \"error\" : level,\n message: `WebSocket ${event} ${sanitizedUrl}`,\n error,\n props: {\n websocket: {\n kind: \"websocket\",\n event,\n url: sanitizedUrl,\n durationMs: durationMs(started),\n ...extra,\n },\n },\n });\n };\n\n if (captureConnect) captureLifecycle(\"connect\");\n\n if (captureOpen) {\n socket.addEventListener?.(\"open\", () => captureLifecycle(\"open\"));\n }\n if (captureClose) {\n socket.addEventListener?.(\"close\", (event) =>\n captureLifecycle(\"close\", closeEventPayload(event)),\n );\n }\n if (captureError) {\n socket.addEventListener?.(\"error\", (event) =>\n captureLifecycle(\"error\", { type: event.type }, event),\n );\n }\n if (captureMessages) {\n socket.addEventListener?.(\"message\", (event) => {\n const message = event as MessageEvent;\n capture({\n level,\n message: `WebSocket message ${sanitizedUrl}`,\n props: {\n websocket: {\n kind: \"websocket-message\",\n url: sanitizedUrl,\n message: messagePayload(\"incoming\", message.data, captureMessageData),\n },\n },\n });\n });\n }\n\n if (captureSentMessages && socket.send) {\n const originalSend = socket.send.bind(socket);\n socket.send = (data: BrowserWebSocketSendPayload) => {\n capture({\n level,\n message: `WebSocket send ${sanitizedUrl}`,\n props: {\n websocket: {\n kind: \"websocket-message\",\n url: sanitizedUrl,\n message: messagePayload(\"outgoing\", data, captureMessageData),\n },\n },\n });\n return originalSend(data);\n };\n }\n\n return socket;\n } as unknown as BrowserCapturedWebSocketConstructor;\n\n PatchedWebSocket.prototype = original.prototype;\n copyStaticConstants(PatchedWebSocket, original);\n globalThis.WebSocket = PatchedWebSocket as unknown as typeof WebSocket;\n\n return () => {\n globalThis.WebSocket = current as unknown as typeof WebSocket;\n };\n },\n };\n}\n"],"mappings":"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"}