@oconde/radar 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,173 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/browser/index.ts
21
+ var browser_exports = {};
22
+ __export(browser_exports, {
23
+ DEFAULT_CLIENT_ERRORS_ENDPOINT: () => DEFAULT_CLIENT_ERRORS_ENDPOINT,
24
+ handleErrorWithRadar: () => handleErrorWithRadar,
25
+ listenForClientErrors: () => listenForClientErrors,
26
+ reportClientError: () => reportClientError
27
+ });
28
+ module.exports = __toCommonJS(browser_exports);
29
+ var DEFAULT_CLIENT_ERRORS_ENDPOINT = "/api/radar/client-errors";
30
+ var MAX_REPORTS = 10;
31
+ var MAX_MESSAGE = 2e3;
32
+ var MAX_STACK = 12e3;
33
+ var ACTION_WINDOW_MS = 1e4;
34
+ var MAX_LABEL = 60;
35
+ var MAX_TEXT_READ = 500;
36
+ var MAX_LINK_TEXT = 40;
37
+ var LINK_TAGS = /* @__PURE__ */ new Set(["a", "label"]);
38
+ var LINK_ROLES = /* @__PURE__ */ new Set(["link"]);
39
+ var INTERACTIVE = 'button, a, [role="button"], [role="link"], [role="menuitem"], [role="tab"], [role="checkbox"], [role="switch"], input, select, textarea, summary, label';
40
+ var BUTTON_INPUTS = /* @__PURE__ */ new Set(["button", "submit", "reset", "image"]);
41
+ var FIELDS = /* @__PURE__ */ new Set(["input", "select", "textarea"]);
42
+ var ELEMENT_NAMES = { a: "link", button: "button", input: "input", select: "select", textarea: "textarea", summary: "summary", label: "label", form: "form" };
43
+ var sent = { count: 0, keys: /* @__PURE__ */ new Set() };
44
+ var lastAction = null;
45
+ function browser() {
46
+ return globalThis;
47
+ }
48
+ function describe(error, path) {
49
+ if (error instanceof Error) {
50
+ return { name: error.name || "Error", message: String(error.message ?? "").slice(0, MAX_MESSAGE), ...error.stack ? { stack: error.stack.slice(0, MAX_STACK) } : {}, path };
51
+ }
52
+ if (typeof error === "object" && error !== null) {
53
+ const record = error;
54
+ const name = typeof record.name === "string" && record.name ? record.name : "Error";
55
+ const message = typeof record.message === "string" ? record.message : safeString(error);
56
+ return { name, message: message.slice(0, MAX_MESSAGE), ...typeof record.stack === "string" ? { stack: record.stack.slice(0, MAX_STACK) } : {}, path };
57
+ }
58
+ return { name: "Error", message: String(error).slice(0, MAX_MESSAGE), path };
59
+ }
60
+ function attribute(node, name) {
61
+ try {
62
+ const value = node.getAttribute?.(name);
63
+ return typeof value === "string" && value.trim() ? value : null;
64
+ } catch {
65
+ return null;
66
+ }
67
+ }
68
+ function cleanLabel(text) {
69
+ return (text ?? "").replace(/\s+/g, " ").trim().slice(0, MAX_LABEL);
70
+ }
71
+ function describeElement(target, kind) {
72
+ const node = target;
73
+ if (!node || typeof node !== "object" || typeof node.tagName !== "string") return null;
74
+ const found = kind === "click" && typeof node.closest === "function" ? node.closest(INTERACTIVE) : node;
75
+ const element = found ?? node;
76
+ const control = found !== null;
77
+ const tag = (element.tagName ?? "").toLowerCase();
78
+ const type = (attribute(element, "type") ?? "").toLowerCase();
79
+ const role = attribute(element, "role");
80
+ const name = role ?? (tag === "input" && BUTTON_INPUTS.has(type) ? "button" : ELEMENT_NAMES[tag] ?? (/^[a-z][a-z0-9-]*$/.test(tag) ? tag : "element"));
81
+ const named = attribute(element, "aria-label") ?? attribute(element, "title");
82
+ const fieldLabel = FIELDS.has(tag) && !BUTTON_INPUTS.has(type) ? attribute(element, "name") ?? attribute(element, "placeholder") ?? attribute(element, "id") : null;
83
+ const buttonValue = tag === "input" && BUTTON_INPUTS.has(type) ? attribute(element, "value") : null;
84
+ const rawText = !control || FIELDS.has(tag) || tag === "form" ? null : cleanLabel((element.textContent ?? "").slice(0, MAX_TEXT_READ));
85
+ const linkLike = LINK_TAGS.has(tag) || role !== null && LINK_ROLES.has(role);
86
+ const text = rawText && linkLike && (rawText.length > MAX_LINK_TEXT || /[\d@]/.test(rawText)) ? null : rawText;
87
+ const formLabel = tag === "form" ? attribute(element, "name") ?? attribute(element, "id") : null;
88
+ const label = cleanLabel(named ?? fieldLabel ?? buttonValue ?? formLabel ?? (cleanLabel(text) || attribute(element, "id")));
89
+ return { element: name.slice(0, 24), label };
90
+ }
91
+ function recordAction(kind) {
92
+ return (event) => {
93
+ try {
94
+ const described = describeElement(event?.target, kind);
95
+ if (described) lastAction = { kind, ...described, at: Date.now() };
96
+ } catch {
97
+ return;
98
+ }
99
+ };
100
+ }
101
+ function recentAction() {
102
+ if (!lastAction) return void 0;
103
+ const msBefore = Date.now() - lastAction.at;
104
+ if (msBefore < 0 || msBefore > ACTION_WINDOW_MS) return void 0;
105
+ return { kind: lastAction.kind, element: lastAction.element, label: lastAction.label, msBefore };
106
+ }
107
+ function safeString(value) {
108
+ try {
109
+ return JSON.stringify(value) ?? String(value);
110
+ } catch {
111
+ return String(value);
112
+ }
113
+ }
114
+ function send(endpoint, body) {
115
+ const globals = browser();
116
+ if (typeof globals.fetch === "function") {
117
+ globals.fetch(endpoint, { method: "POST", headers: { "content-type": "application/json" }, body, keepalive: true, credentials: "same-origin" }).catch(() => void 0);
118
+ return;
119
+ }
120
+ if (typeof globals.navigator?.sendBeacon === "function") globals.navigator.sendBeacon(endpoint, new Blob([body], { type: "application/json" }));
121
+ }
122
+ function reportClientError(error, options = {}) {
123
+ try {
124
+ const globals = browser();
125
+ if (!globals.window || !globals.location) return;
126
+ if (sent.count >= MAX_REPORTS) return;
127
+ const action = recentAction();
128
+ const payload = { ...describe(error, `${globals.location.pathname}${globals.location.search}`), ...action ? { action } : {} };
129
+ const key = `${payload.name}|${payload.message}|${(payload.stack ?? "").slice(0, 300)}`;
130
+ if (sent.keys.has(key)) return;
131
+ sent.keys.add(key);
132
+ sent.count += 1;
133
+ send(options.endpoint ?? DEFAULT_CLIENT_ERRORS_ENDPOINT, JSON.stringify(payload));
134
+ } catch {
135
+ return;
136
+ }
137
+ }
138
+ function handleErrorWithRadar(handler, options = {}) {
139
+ return (input) => {
140
+ const unexpected = (input.status ?? 500) >= 500;
141
+ if (unexpected) reportClientError(input.error, options);
142
+ if (handler) return handler(input);
143
+ if (unexpected) console.error(input.error);
144
+ return void 0;
145
+ };
146
+ }
147
+ function listenForClientErrors(options = {}) {
148
+ const target = browser().window;
149
+ if (!target) return () => void 0;
150
+ const onClick = recordAction("click");
151
+ const onSubmit = recordAction("submit");
152
+ const captureActions = options.captureActions !== false;
153
+ const onError = (event) => {
154
+ const record = event ?? {};
155
+ reportClientError(record.error ?? record.message, options);
156
+ };
157
+ const onRejection = (event) => reportClientError((event ?? {}).reason, options);
158
+ target.addEventListener("error", onError);
159
+ target.addEventListener("unhandledrejection", onRejection);
160
+ if (captureActions) {
161
+ target.addEventListener("click", onClick, true);
162
+ target.addEventListener("submit", onSubmit, true);
163
+ }
164
+ return () => {
165
+ target.removeEventListener("error", onError);
166
+ target.removeEventListener("unhandledrejection", onRejection);
167
+ if (captureActions) {
168
+ target.removeEventListener("click", onClick, true);
169
+ target.removeEventListener("submit", onSubmit, true);
170
+ }
171
+ };
172
+ }
173
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/browser/index.ts"],"sourcesContent":["export type ReportOptions = { endpoint?: string };\nexport type ListenOptions = ReportOptions & { captureActions?: boolean };\n\nexport type UiAction = { kind: 'click' | 'submit'; element: string; label: string; msBefore: number };\nexport type ClientErrorPayload = { name: string; message: string; stack?: string; path: string; action?: UiAction };\n\nexport const DEFAULT_CLIENT_ERRORS_ENDPOINT = '/api/radar/client-errors';\n\ntype Listener = (event: unknown) => void;\n\ntype ElementLike = { tagName?: string; textContent?: string | null; closest?(selector: string): ElementLike | null; getAttribute?(name: string): string | null };\n\ntype BrowserGlobals = {\n window?: { addEventListener(type: string, listener: Listener, capture?: boolean): void; removeEventListener(type: string, listener: Listener, capture?: boolean): void };\n location?: { pathname: string; search: string };\n navigator?: { sendBeacon?: (url: string, data: Blob) => boolean };\n fetch?: (input: string, init: RequestInit) => Promise<unknown>;\n};\n\ntype ErrorHandlerInput = { error: unknown; status?: number; message: string };\n\nconst MAX_REPORTS = 10;\nconst MAX_MESSAGE = 2000;\nconst MAX_STACK = 12_000;\nconst ACTION_WINDOW_MS = 10_000;\nconst MAX_LABEL = 60;\nconst MAX_TEXT_READ = 500;\nconst MAX_LINK_TEXT = 40;\nconst LINK_TAGS = new Set(['a', 'label']);\nconst LINK_ROLES = new Set(['link']);\nconst INTERACTIVE = 'button, a, [role=\"button\"], [role=\"link\"], [role=\"menuitem\"], [role=\"tab\"], [role=\"checkbox\"], [role=\"switch\"], input, select, textarea, summary, label';\nconst BUTTON_INPUTS = new Set(['button', 'submit', 'reset', 'image']);\nconst FIELDS = new Set(['input', 'select', 'textarea']);\nconst ELEMENT_NAMES: Record<string, string> = { a: 'link', button: 'button', input: 'input', select: 'select', textarea: 'textarea', summary: 'summary', label: 'label', form: 'form' };\n\nconst sent = { count: 0, keys: new Set<string>() };\nlet lastAction: { kind: UiAction['kind']; element: string; label: string; at: number } | null = null;\n\nfunction browser(): BrowserGlobals {\n return globalThis as unknown as BrowserGlobals;\n}\n\nfunction describe(error: unknown, path: string): ClientErrorPayload {\n if (error instanceof Error) {\n return { name: error.name || 'Error', message: String(error.message ?? '').slice(0, MAX_MESSAGE), ...(error.stack ? { stack: error.stack.slice(0, MAX_STACK) } : {}), path };\n }\n if (typeof error === 'object' && error !== null) {\n const record = error as Record<string, unknown>;\n const name = typeof record.name === 'string' && record.name ? record.name : 'Error';\n const message = typeof record.message === 'string' ? record.message : safeString(error);\n return { name, message: message.slice(0, MAX_MESSAGE), ...(typeof record.stack === 'string' ? { stack: record.stack.slice(0, MAX_STACK) } : {}), path };\n }\n return { name: 'Error', message: String(error).slice(0, MAX_MESSAGE), path };\n}\n\nfunction attribute(node: ElementLike, name: string): string | null {\n try {\n const value = node.getAttribute?.(name);\n return typeof value === 'string' && value.trim() ? value : null;\n } catch {\n return null;\n }\n}\n\nfunction cleanLabel(text: string | null | undefined): string {\n return (text ?? '').replace(/\\s+/g, ' ').trim().slice(0, MAX_LABEL);\n}\n\nfunction describeElement(target: unknown, kind: UiAction['kind']): { element: string; label: string } | null {\n const node = target as ElementLike | null;\n if (!node || typeof node !== 'object' || typeof node.tagName !== 'string') return null;\n const found = kind === 'click' && typeof node.closest === 'function' ? node.closest(INTERACTIVE) : node;\n const element = found ?? node;\n const control = found !== null;\n const tag = (element.tagName ?? '').toLowerCase();\n const type = (attribute(element, 'type') ?? '').toLowerCase();\n const role = attribute(element, 'role');\n const name = role ?? (tag === 'input' && BUTTON_INPUTS.has(type) ? 'button' : (ELEMENT_NAMES[tag] ?? (/^[a-z][a-z0-9-]*$/.test(tag) ? tag : 'element')));\n const named = attribute(element, 'aria-label') ?? attribute(element, 'title');\n const fieldLabel = FIELDS.has(tag) && !BUTTON_INPUTS.has(type) ? (attribute(element, 'name') ?? attribute(element, 'placeholder') ?? attribute(element, 'id')) : null;\n const buttonValue = tag === 'input' && BUTTON_INPUTS.has(type) ? attribute(element, 'value') : null;\n const rawText = !control || FIELDS.has(tag) || tag === 'form' ? null : cleanLabel((element.textContent ?? '').slice(0, MAX_TEXT_READ));\n const linkLike = LINK_TAGS.has(tag) || (role !== null && LINK_ROLES.has(role));\n const text = rawText && linkLike && (rawText.length > MAX_LINK_TEXT || /[\\d@]/.test(rawText)) ? null : rawText;\n const formLabel = tag === 'form' ? (attribute(element, 'name') ?? attribute(element, 'id')) : null;\n const label = cleanLabel(named ?? fieldLabel ?? buttonValue ?? formLabel ?? (cleanLabel(text) || attribute(element, 'id')));\n return { element: name.slice(0, 24), label };\n}\n\nfunction recordAction(kind: UiAction['kind']): Listener {\n return (event) => {\n try {\n const described = describeElement((event as { target?: unknown } | null)?.target, kind);\n if (described) lastAction = { kind, ...described, at: Date.now() };\n } catch {\n return;\n }\n };\n}\n\nfunction recentAction(): UiAction | undefined {\n if (!lastAction) return undefined;\n const msBefore = Date.now() - lastAction.at;\n if (msBefore < 0 || msBefore > ACTION_WINDOW_MS) return undefined;\n return { kind: lastAction.kind, element: lastAction.element, label: lastAction.label, msBefore };\n}\n\nfunction safeString(value: unknown): string {\n try {\n return JSON.stringify(value) ?? String(value);\n } catch {\n return String(value);\n }\n}\n\nfunction send(endpoint: string, body: string): void {\n const globals = browser();\n if (typeof globals.fetch === 'function') {\n globals.fetch(endpoint, { method: 'POST', headers: { 'content-type': 'application/json' }, body, keepalive: true, credentials: 'same-origin' }).catch(() => undefined);\n return;\n }\n if (typeof globals.navigator?.sendBeacon === 'function') globals.navigator.sendBeacon(endpoint, new Blob([body], { type: 'application/json' }));\n}\n\nexport function reportClientError(error: unknown, options: ReportOptions = {}): void {\n try {\n const globals = browser();\n if (!globals.window || !globals.location) return;\n if (sent.count >= MAX_REPORTS) return;\n const action = recentAction();\n const payload = { ...describe(error, `${globals.location.pathname}${globals.location.search}`), ...(action ? { action } : {}) };\n const key = `${payload.name}|${payload.message}|${(payload.stack ?? '').slice(0, 300)}`;\n if (sent.keys.has(key)) return;\n sent.keys.add(key);\n sent.count += 1;\n send(options.endpoint ?? DEFAULT_CLIENT_ERRORS_ENDPOINT, JSON.stringify(payload));\n } catch {\n return;\n }\n}\n\nexport function handleErrorWithRadar<Input extends ErrorHandlerInput, Output>(handler?: (input: Input) => Output, options: ReportOptions = {}): (input: Input) => Output | undefined {\n return (input) => {\n const unexpected = (input.status ?? 500) >= 500;\n if (unexpected) reportClientError(input.error, options);\n if (handler) return handler(input);\n if (unexpected) console.error(input.error);\n return undefined;\n };\n}\n\nexport function listenForClientErrors(options: ListenOptions = {}): () => void {\n const target = browser().window;\n if (!target) return () => undefined;\n const onClick = recordAction('click');\n const onSubmit = recordAction('submit');\n const captureActions = options.captureActions !== false;\n const onError: Listener = (event) => {\n const record = (event ?? {}) as { error?: unknown; message?: unknown };\n reportClientError(record.error ?? record.message, options);\n };\n const onRejection: Listener = (event) => reportClientError(((event ?? {}) as { reason?: unknown }).reason, options);\n target.addEventListener('error', onError);\n target.addEventListener('unhandledrejection', onRejection);\n if (captureActions) {\n target.addEventListener('click', onClick, true);\n target.addEventListener('submit', onSubmit, true);\n }\n return () => {\n target.removeEventListener('error', onError);\n target.removeEventListener('unhandledrejection', onRejection);\n if (captureActions) {\n target.removeEventListener('click', onClick, true);\n target.removeEventListener('submit', onSubmit, true);\n }\n };\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,30 @@
1
+ type ReportOptions = {
2
+ endpoint?: string;
3
+ };
4
+ type ListenOptions = ReportOptions & {
5
+ captureActions?: boolean;
6
+ };
7
+ type UiAction = {
8
+ kind: 'click' | 'submit';
9
+ element: string;
10
+ label: string;
11
+ msBefore: number;
12
+ };
13
+ type ClientErrorPayload = {
14
+ name: string;
15
+ message: string;
16
+ stack?: string;
17
+ path: string;
18
+ action?: UiAction;
19
+ };
20
+ declare const DEFAULT_CLIENT_ERRORS_ENDPOINT = "/api/radar/client-errors";
21
+ type ErrorHandlerInput = {
22
+ error: unknown;
23
+ status?: number;
24
+ message: string;
25
+ };
26
+ declare function reportClientError(error: unknown, options?: ReportOptions): void;
27
+ declare function handleErrorWithRadar<Input extends ErrorHandlerInput, Output>(handler?: (input: Input) => Output, options?: ReportOptions): (input: Input) => Output | undefined;
28
+ declare function listenForClientErrors(options?: ListenOptions): () => void;
29
+
30
+ export { type ClientErrorPayload, DEFAULT_CLIENT_ERRORS_ENDPOINT, type ListenOptions, type ReportOptions, type UiAction, handleErrorWithRadar, listenForClientErrors, reportClientError };
@@ -0,0 +1,30 @@
1
+ type ReportOptions = {
2
+ endpoint?: string;
3
+ };
4
+ type ListenOptions = ReportOptions & {
5
+ captureActions?: boolean;
6
+ };
7
+ type UiAction = {
8
+ kind: 'click' | 'submit';
9
+ element: string;
10
+ label: string;
11
+ msBefore: number;
12
+ };
13
+ type ClientErrorPayload = {
14
+ name: string;
15
+ message: string;
16
+ stack?: string;
17
+ path: string;
18
+ action?: UiAction;
19
+ };
20
+ declare const DEFAULT_CLIENT_ERRORS_ENDPOINT = "/api/radar/client-errors";
21
+ type ErrorHandlerInput = {
22
+ error: unknown;
23
+ status?: number;
24
+ message: string;
25
+ };
26
+ declare function reportClientError(error: unknown, options?: ReportOptions): void;
27
+ declare function handleErrorWithRadar<Input extends ErrorHandlerInput, Output>(handler?: (input: Input) => Output, options?: ReportOptions): (input: Input) => Output | undefined;
28
+ declare function listenForClientErrors(options?: ListenOptions): () => void;
29
+
30
+ export { type ClientErrorPayload, DEFAULT_CLIENT_ERRORS_ENDPOINT, type ListenOptions, type ReportOptions, type UiAction, handleErrorWithRadar, listenForClientErrors, reportClientError };
@@ -0,0 +1,152 @@
1
+ // src/browser/index.ts
2
+ var DEFAULT_CLIENT_ERRORS_ENDPOINT = "/api/radar/client-errors";
3
+ var MAX_REPORTS = 10;
4
+ var MAX_MESSAGE = 2e3;
5
+ var MAX_STACK = 12e3;
6
+ var ACTION_WINDOW_MS = 1e4;
7
+ var MAX_LABEL = 60;
8
+ var MAX_TEXT_READ = 500;
9
+ var MAX_LINK_TEXT = 40;
10
+ var LINK_TAGS = /* @__PURE__ */ new Set(["a", "label"]);
11
+ var LINK_ROLES = /* @__PURE__ */ new Set(["link"]);
12
+ var INTERACTIVE = 'button, a, [role="button"], [role="link"], [role="menuitem"], [role="tab"], [role="checkbox"], [role="switch"], input, select, textarea, summary, label';
13
+ var BUTTON_INPUTS = /* @__PURE__ */ new Set(["button", "submit", "reset", "image"]);
14
+ var FIELDS = /* @__PURE__ */ new Set(["input", "select", "textarea"]);
15
+ var ELEMENT_NAMES = { a: "link", button: "button", input: "input", select: "select", textarea: "textarea", summary: "summary", label: "label", form: "form" };
16
+ var sent = { count: 0, keys: /* @__PURE__ */ new Set() };
17
+ var lastAction = null;
18
+ function browser() {
19
+ return globalThis;
20
+ }
21
+ function describe(error, path) {
22
+ if (error instanceof Error) {
23
+ return { name: error.name || "Error", message: String(error.message ?? "").slice(0, MAX_MESSAGE), ...error.stack ? { stack: error.stack.slice(0, MAX_STACK) } : {}, path };
24
+ }
25
+ if (typeof error === "object" && error !== null) {
26
+ const record = error;
27
+ const name = typeof record.name === "string" && record.name ? record.name : "Error";
28
+ const message = typeof record.message === "string" ? record.message : safeString(error);
29
+ return { name, message: message.slice(0, MAX_MESSAGE), ...typeof record.stack === "string" ? { stack: record.stack.slice(0, MAX_STACK) } : {}, path };
30
+ }
31
+ return { name: "Error", message: String(error).slice(0, MAX_MESSAGE), path };
32
+ }
33
+ function attribute(node, name) {
34
+ try {
35
+ const value = node.getAttribute?.(name);
36
+ return typeof value === "string" && value.trim() ? value : null;
37
+ } catch {
38
+ return null;
39
+ }
40
+ }
41
+ function cleanLabel(text) {
42
+ return (text ?? "").replace(/\s+/g, " ").trim().slice(0, MAX_LABEL);
43
+ }
44
+ function describeElement(target, kind) {
45
+ const node = target;
46
+ if (!node || typeof node !== "object" || typeof node.tagName !== "string") return null;
47
+ const found = kind === "click" && typeof node.closest === "function" ? node.closest(INTERACTIVE) : node;
48
+ const element = found ?? node;
49
+ const control = found !== null;
50
+ const tag = (element.tagName ?? "").toLowerCase();
51
+ const type = (attribute(element, "type") ?? "").toLowerCase();
52
+ const role = attribute(element, "role");
53
+ const name = role ?? (tag === "input" && BUTTON_INPUTS.has(type) ? "button" : ELEMENT_NAMES[tag] ?? (/^[a-z][a-z0-9-]*$/.test(tag) ? tag : "element"));
54
+ const named = attribute(element, "aria-label") ?? attribute(element, "title");
55
+ const fieldLabel = FIELDS.has(tag) && !BUTTON_INPUTS.has(type) ? attribute(element, "name") ?? attribute(element, "placeholder") ?? attribute(element, "id") : null;
56
+ const buttonValue = tag === "input" && BUTTON_INPUTS.has(type) ? attribute(element, "value") : null;
57
+ const rawText = !control || FIELDS.has(tag) || tag === "form" ? null : cleanLabel((element.textContent ?? "").slice(0, MAX_TEXT_READ));
58
+ const linkLike = LINK_TAGS.has(tag) || role !== null && LINK_ROLES.has(role);
59
+ const text = rawText && linkLike && (rawText.length > MAX_LINK_TEXT || /[\d@]/.test(rawText)) ? null : rawText;
60
+ const formLabel = tag === "form" ? attribute(element, "name") ?? attribute(element, "id") : null;
61
+ const label = cleanLabel(named ?? fieldLabel ?? buttonValue ?? formLabel ?? (cleanLabel(text) || attribute(element, "id")));
62
+ return { element: name.slice(0, 24), label };
63
+ }
64
+ function recordAction(kind) {
65
+ return (event) => {
66
+ try {
67
+ const described = describeElement(event?.target, kind);
68
+ if (described) lastAction = { kind, ...described, at: Date.now() };
69
+ } catch {
70
+ return;
71
+ }
72
+ };
73
+ }
74
+ function recentAction() {
75
+ if (!lastAction) return void 0;
76
+ const msBefore = Date.now() - lastAction.at;
77
+ if (msBefore < 0 || msBefore > ACTION_WINDOW_MS) return void 0;
78
+ return { kind: lastAction.kind, element: lastAction.element, label: lastAction.label, msBefore };
79
+ }
80
+ function safeString(value) {
81
+ try {
82
+ return JSON.stringify(value) ?? String(value);
83
+ } catch {
84
+ return String(value);
85
+ }
86
+ }
87
+ function send(endpoint, body) {
88
+ const globals = browser();
89
+ if (typeof globals.fetch === "function") {
90
+ globals.fetch(endpoint, { method: "POST", headers: { "content-type": "application/json" }, body, keepalive: true, credentials: "same-origin" }).catch(() => void 0);
91
+ return;
92
+ }
93
+ if (typeof globals.navigator?.sendBeacon === "function") globals.navigator.sendBeacon(endpoint, new Blob([body], { type: "application/json" }));
94
+ }
95
+ function reportClientError(error, options = {}) {
96
+ try {
97
+ const globals = browser();
98
+ if (!globals.window || !globals.location) return;
99
+ if (sent.count >= MAX_REPORTS) return;
100
+ const action = recentAction();
101
+ const payload = { ...describe(error, `${globals.location.pathname}${globals.location.search}`), ...action ? { action } : {} };
102
+ const key = `${payload.name}|${payload.message}|${(payload.stack ?? "").slice(0, 300)}`;
103
+ if (sent.keys.has(key)) return;
104
+ sent.keys.add(key);
105
+ sent.count += 1;
106
+ send(options.endpoint ?? DEFAULT_CLIENT_ERRORS_ENDPOINT, JSON.stringify(payload));
107
+ } catch {
108
+ return;
109
+ }
110
+ }
111
+ function handleErrorWithRadar(handler, options = {}) {
112
+ return (input) => {
113
+ const unexpected = (input.status ?? 500) >= 500;
114
+ if (unexpected) reportClientError(input.error, options);
115
+ if (handler) return handler(input);
116
+ if (unexpected) console.error(input.error);
117
+ return void 0;
118
+ };
119
+ }
120
+ function listenForClientErrors(options = {}) {
121
+ const target = browser().window;
122
+ if (!target) return () => void 0;
123
+ const onClick = recordAction("click");
124
+ const onSubmit = recordAction("submit");
125
+ const captureActions = options.captureActions !== false;
126
+ const onError = (event) => {
127
+ const record = event ?? {};
128
+ reportClientError(record.error ?? record.message, options);
129
+ };
130
+ const onRejection = (event) => reportClientError((event ?? {}).reason, options);
131
+ target.addEventListener("error", onError);
132
+ target.addEventListener("unhandledrejection", onRejection);
133
+ if (captureActions) {
134
+ target.addEventListener("click", onClick, true);
135
+ target.addEventListener("submit", onSubmit, true);
136
+ }
137
+ return () => {
138
+ target.removeEventListener("error", onError);
139
+ target.removeEventListener("unhandledrejection", onRejection);
140
+ if (captureActions) {
141
+ target.removeEventListener("click", onClick, true);
142
+ target.removeEventListener("submit", onSubmit, true);
143
+ }
144
+ };
145
+ }
146
+ export {
147
+ DEFAULT_CLIENT_ERRORS_ENDPOINT,
148
+ handleErrorWithRadar,
149
+ listenForClientErrors,
150
+ reportClientError
151
+ };
152
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/browser/index.ts"],"sourcesContent":["export type ReportOptions = { endpoint?: string };\nexport type ListenOptions = ReportOptions & { captureActions?: boolean };\n\nexport type UiAction = { kind: 'click' | 'submit'; element: string; label: string; msBefore: number };\nexport type ClientErrorPayload = { name: string; message: string; stack?: string; path: string; action?: UiAction };\n\nexport const DEFAULT_CLIENT_ERRORS_ENDPOINT = '/api/radar/client-errors';\n\ntype Listener = (event: unknown) => void;\n\ntype ElementLike = { tagName?: string; textContent?: string | null; closest?(selector: string): ElementLike | null; getAttribute?(name: string): string | null };\n\ntype BrowserGlobals = {\n window?: { addEventListener(type: string, listener: Listener, capture?: boolean): void; removeEventListener(type: string, listener: Listener, capture?: boolean): void };\n location?: { pathname: string; search: string };\n navigator?: { sendBeacon?: (url: string, data: Blob) => boolean };\n fetch?: (input: string, init: RequestInit) => Promise<unknown>;\n};\n\ntype ErrorHandlerInput = { error: unknown; status?: number; message: string };\n\nconst MAX_REPORTS = 10;\nconst MAX_MESSAGE = 2000;\nconst MAX_STACK = 12_000;\nconst ACTION_WINDOW_MS = 10_000;\nconst MAX_LABEL = 60;\nconst MAX_TEXT_READ = 500;\nconst MAX_LINK_TEXT = 40;\nconst LINK_TAGS = new Set(['a', 'label']);\nconst LINK_ROLES = new Set(['link']);\nconst INTERACTIVE = 'button, a, [role=\"button\"], [role=\"link\"], [role=\"menuitem\"], [role=\"tab\"], [role=\"checkbox\"], [role=\"switch\"], input, select, textarea, summary, label';\nconst BUTTON_INPUTS = new Set(['button', 'submit', 'reset', 'image']);\nconst FIELDS = new Set(['input', 'select', 'textarea']);\nconst ELEMENT_NAMES: Record<string, string> = { a: 'link', button: 'button', input: 'input', select: 'select', textarea: 'textarea', summary: 'summary', label: 'label', form: 'form' };\n\nconst sent = { count: 0, keys: new Set<string>() };\nlet lastAction: { kind: UiAction['kind']; element: string; label: string; at: number } | null = null;\n\nfunction browser(): BrowserGlobals {\n return globalThis as unknown as BrowserGlobals;\n}\n\nfunction describe(error: unknown, path: string): ClientErrorPayload {\n if (error instanceof Error) {\n return { name: error.name || 'Error', message: String(error.message ?? '').slice(0, MAX_MESSAGE), ...(error.stack ? { stack: error.stack.slice(0, MAX_STACK) } : {}), path };\n }\n if (typeof error === 'object' && error !== null) {\n const record = error as Record<string, unknown>;\n const name = typeof record.name === 'string' && record.name ? record.name : 'Error';\n const message = typeof record.message === 'string' ? record.message : safeString(error);\n return { name, message: message.slice(0, MAX_MESSAGE), ...(typeof record.stack === 'string' ? { stack: record.stack.slice(0, MAX_STACK) } : {}), path };\n }\n return { name: 'Error', message: String(error).slice(0, MAX_MESSAGE), path };\n}\n\nfunction attribute(node: ElementLike, name: string): string | null {\n try {\n const value = node.getAttribute?.(name);\n return typeof value === 'string' && value.trim() ? value : null;\n } catch {\n return null;\n }\n}\n\nfunction cleanLabel(text: string | null | undefined): string {\n return (text ?? '').replace(/\\s+/g, ' ').trim().slice(0, MAX_LABEL);\n}\n\nfunction describeElement(target: unknown, kind: UiAction['kind']): { element: string; label: string } | null {\n const node = target as ElementLike | null;\n if (!node || typeof node !== 'object' || typeof node.tagName !== 'string') return null;\n const found = kind === 'click' && typeof node.closest === 'function' ? node.closest(INTERACTIVE) : node;\n const element = found ?? node;\n const control = found !== null;\n const tag = (element.tagName ?? '').toLowerCase();\n const type = (attribute(element, 'type') ?? '').toLowerCase();\n const role = attribute(element, 'role');\n const name = role ?? (tag === 'input' && BUTTON_INPUTS.has(type) ? 'button' : (ELEMENT_NAMES[tag] ?? (/^[a-z][a-z0-9-]*$/.test(tag) ? tag : 'element')));\n const named = attribute(element, 'aria-label') ?? attribute(element, 'title');\n const fieldLabel = FIELDS.has(tag) && !BUTTON_INPUTS.has(type) ? (attribute(element, 'name') ?? attribute(element, 'placeholder') ?? attribute(element, 'id')) : null;\n const buttonValue = tag === 'input' && BUTTON_INPUTS.has(type) ? attribute(element, 'value') : null;\n const rawText = !control || FIELDS.has(tag) || tag === 'form' ? null : cleanLabel((element.textContent ?? '').slice(0, MAX_TEXT_READ));\n const linkLike = LINK_TAGS.has(tag) || (role !== null && LINK_ROLES.has(role));\n const text = rawText && linkLike && (rawText.length > MAX_LINK_TEXT || /[\\d@]/.test(rawText)) ? null : rawText;\n const formLabel = tag === 'form' ? (attribute(element, 'name') ?? attribute(element, 'id')) : null;\n const label = cleanLabel(named ?? fieldLabel ?? buttonValue ?? formLabel ?? (cleanLabel(text) || attribute(element, 'id')));\n return { element: name.slice(0, 24), label };\n}\n\nfunction recordAction(kind: UiAction['kind']): Listener {\n return (event) => {\n try {\n const described = describeElement((event as { target?: unknown } | null)?.target, kind);\n if (described) lastAction = { kind, ...described, at: Date.now() };\n } catch {\n return;\n }\n };\n}\n\nfunction recentAction(): UiAction | undefined {\n if (!lastAction) return undefined;\n const msBefore = Date.now() - lastAction.at;\n if (msBefore < 0 || msBefore > ACTION_WINDOW_MS) return undefined;\n return { kind: lastAction.kind, element: lastAction.element, label: lastAction.label, msBefore };\n}\n\nfunction safeString(value: unknown): string {\n try {\n return JSON.stringify(value) ?? String(value);\n } catch {\n return String(value);\n }\n}\n\nfunction send(endpoint: string, body: string): void {\n const globals = browser();\n if (typeof globals.fetch === 'function') {\n globals.fetch(endpoint, { method: 'POST', headers: { 'content-type': 'application/json' }, body, keepalive: true, credentials: 'same-origin' }).catch(() => undefined);\n return;\n }\n if (typeof globals.navigator?.sendBeacon === 'function') globals.navigator.sendBeacon(endpoint, new Blob([body], { type: 'application/json' }));\n}\n\nexport function reportClientError(error: unknown, options: ReportOptions = {}): void {\n try {\n const globals = browser();\n if (!globals.window || !globals.location) return;\n if (sent.count >= MAX_REPORTS) return;\n const action = recentAction();\n const payload = { ...describe(error, `${globals.location.pathname}${globals.location.search}`), ...(action ? { action } : {}) };\n const key = `${payload.name}|${payload.message}|${(payload.stack ?? '').slice(0, 300)}`;\n if (sent.keys.has(key)) return;\n sent.keys.add(key);\n sent.count += 1;\n send(options.endpoint ?? DEFAULT_CLIENT_ERRORS_ENDPOINT, JSON.stringify(payload));\n } catch {\n return;\n }\n}\n\nexport function handleErrorWithRadar<Input extends ErrorHandlerInput, Output>(handler?: (input: Input) => Output, options: ReportOptions = {}): (input: Input) => Output | undefined {\n return (input) => {\n const unexpected = (input.status ?? 500) >= 500;\n if (unexpected) reportClientError(input.error, options);\n if (handler) return handler(input);\n if (unexpected) console.error(input.error);\n return undefined;\n };\n}\n\nexport function listenForClientErrors(options: ListenOptions = {}): () => void {\n const target = browser().window;\n if (!target) return () => undefined;\n const onClick = recordAction('click');\n const onSubmit = recordAction('submit');\n const captureActions = options.captureActions !== false;\n const onError: Listener = (event) => {\n const record = (event ?? {}) as { error?: unknown; message?: unknown };\n reportClientError(record.error ?? record.message, options);\n };\n const onRejection: Listener = (event) => reportClientError(((event ?? {}) as { reason?: unknown }).reason, options);\n target.addEventListener('error', onError);\n target.addEventListener('unhandledrejection', onRejection);\n if (captureActions) {\n target.addEventListener('click', onClick, true);\n target.addEventListener('submit', onSubmit, true);\n }\n return () => {\n target.removeEventListener('error', onError);\n target.removeEventListener('unhandledrejection', onRejection);\n if (captureActions) {\n target.removeEventListener('click', onClick, true);\n target.removeEventListener('submit', onSubmit, true);\n }\n };\n}\n"],"mappings":"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","names":[]}
@@ -1,10 +1,11 @@
1
- import { UserInfo, LogLevel } from './protocol/index.js';
1
+ import { UserInfo, LogLevel, RequestInfo, RuntimeInfo, CheckInStatus, ExceptionInfo } from './protocol/index.cjs';
2
2
 
3
3
  type HeaderValue = string | string[] | number | undefined;
4
4
  type RequestLike = {
5
5
  method?: string;
6
6
  url?: string;
7
7
  originalUrl?: string;
8
+ baseUrl?: string;
8
9
  route?: {
9
10
  path?: unknown;
10
11
  };
@@ -65,11 +66,29 @@ type ResolvedOptions = {
65
66
  debug: boolean;
66
67
  };
67
68
 
69
+ type MapLocator = (file: string) => string | null;
70
+ type EnrichOptions = {
71
+ mapLocator?: MapLocator;
72
+ inferFunctionNames?: boolean;
73
+ };
74
+
68
75
  type Attributes = Record<string, unknown>;
69
76
  type LogRequestOptions = {
70
77
  level?: LogLevel;
71
78
  redact?: RedactMode;
72
79
  };
80
+ type CheckInOptions = {
81
+ checkInId?: string;
82
+ durationMs?: number;
83
+ };
84
+ type CaptureExceptionOptions = {
85
+ handled?: boolean;
86
+ level?: 'error' | 'fatal';
87
+ request?: RequestInfo | null;
88
+ attrs?: Attributes;
89
+ runtime?: RuntimeInfo | null;
90
+ enrich?: EnrichOptions;
91
+ };
73
92
  declare class RadarClient {
74
93
  private options;
75
94
  private transport;
@@ -88,17 +107,24 @@ declare class RadarClient {
88
107
  captureUnhandled(error: unknown, level: 'error' | 'fatal'): void;
89
108
  setUser(user: UserInfo): void;
90
109
  track<T>(name: string, fn: () => T | Promise<T>, data?: Attributes): Promise<T>;
110
+ withContext<T>(fn: () => T, options?: {
111
+ requestId?: string;
112
+ }): T;
113
+ checkIn(slug: string, status: CheckInStatus, options?: CheckInOptions): Promise<void>;
114
+ cron<T>(slug: string, fn: () => T | Promise<T>): Promise<T>;
91
115
  middleware(): RadarMiddleware;
92
116
  shouldLogRequest(req: RequestLike): boolean;
93
117
  logHttpRequest(context: RadarContext, status: number): void;
94
118
  flush(timeoutMs?: number): Promise<void>;
95
119
  close(timeoutMs?: number): Promise<void>;
96
120
  private emitLog;
121
+ captureException(exception: ExceptionInfo, options?: CaptureExceptionOptions): void;
97
122
  private emitError;
123
+ private emitException;
98
124
  private print;
99
125
  private safely;
100
126
  private debugWarn;
101
127
  private writeWarning;
102
128
  }
103
129
 
104
- export { type Attributes as A, type LogRequestOptions as L, RadarClient as R, type RadarMiddleware as a, type RadarOptions as b, type RedactMode as c, type RequestLike as d, type ResponseLike as e };
130
+ export { type Attributes as A, type CaptureExceptionOptions as C, type EnrichOptions as E, type LogRequestOptions as L, type MapLocator as M, RadarClient as R, type CheckInOptions as a, type RadarMiddleware as b, type RadarOptions as c, type RedactMode as d, type RequestLike as e, type ResponseLike as f };
@@ -1,10 +1,11 @@
1
- import { UserInfo, LogLevel } from './protocol/index.cjs';
1
+ import { UserInfo, LogLevel, RequestInfo, RuntimeInfo, CheckInStatus, ExceptionInfo } from './protocol/index.js';
2
2
 
3
3
  type HeaderValue = string | string[] | number | undefined;
4
4
  type RequestLike = {
5
5
  method?: string;
6
6
  url?: string;
7
7
  originalUrl?: string;
8
+ baseUrl?: string;
8
9
  route?: {
9
10
  path?: unknown;
10
11
  };
@@ -65,11 +66,29 @@ type ResolvedOptions = {
65
66
  debug: boolean;
66
67
  };
67
68
 
69
+ type MapLocator = (file: string) => string | null;
70
+ type EnrichOptions = {
71
+ mapLocator?: MapLocator;
72
+ inferFunctionNames?: boolean;
73
+ };
74
+
68
75
  type Attributes = Record<string, unknown>;
69
76
  type LogRequestOptions = {
70
77
  level?: LogLevel;
71
78
  redact?: RedactMode;
72
79
  };
80
+ type CheckInOptions = {
81
+ checkInId?: string;
82
+ durationMs?: number;
83
+ };
84
+ type CaptureExceptionOptions = {
85
+ handled?: boolean;
86
+ level?: 'error' | 'fatal';
87
+ request?: RequestInfo | null;
88
+ attrs?: Attributes;
89
+ runtime?: RuntimeInfo | null;
90
+ enrich?: EnrichOptions;
91
+ };
73
92
  declare class RadarClient {
74
93
  private options;
75
94
  private transport;
@@ -88,17 +107,24 @@ declare class RadarClient {
88
107
  captureUnhandled(error: unknown, level: 'error' | 'fatal'): void;
89
108
  setUser(user: UserInfo): void;
90
109
  track<T>(name: string, fn: () => T | Promise<T>, data?: Attributes): Promise<T>;
110
+ withContext<T>(fn: () => T, options?: {
111
+ requestId?: string;
112
+ }): T;
113
+ checkIn(slug: string, status: CheckInStatus, options?: CheckInOptions): Promise<void>;
114
+ cron<T>(slug: string, fn: () => T | Promise<T>): Promise<T>;
91
115
  middleware(): RadarMiddleware;
92
116
  shouldLogRequest(req: RequestLike): boolean;
93
117
  logHttpRequest(context: RadarContext, status: number): void;
94
118
  flush(timeoutMs?: number): Promise<void>;
95
119
  close(timeoutMs?: number): Promise<void>;
96
120
  private emitLog;
121
+ captureException(exception: ExceptionInfo, options?: CaptureExceptionOptions): void;
97
122
  private emitError;
123
+ private emitException;
98
124
  private print;
99
125
  private safely;
100
126
  private debugWarn;
101
127
  private writeWarning;
102
128
  }
103
129
 
104
- export { type Attributes as A, type LogRequestOptions as L, RadarClient as R, type RadarMiddleware as a, type RadarOptions as b, type RedactMode as c, type RequestLike as d, type ResponseLike as e };
130
+ export { type Attributes as A, type CaptureExceptionOptions as C, type EnrichOptions as E, type LogRequestOptions as L, type MapLocator as M, RadarClient as R, type CheckInOptions as a, type RadarMiddleware as b, type RadarOptions as c, type RedactMode as d, type RequestLike as e, type ResponseLike as f };