@clue-ai/browser-sdk 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/README.md +100 -0
  2. package/dist/authoring/overlay.d.ts +12 -0
  3. package/dist/authoring/overlay.js +468 -0
  4. package/dist/authoring/recording.d.ts +125 -0
  5. package/dist/authoring/recording.js +481 -0
  6. package/dist/authoring/service-logo.d.ts +1 -0
  7. package/dist/authoring/service-logo.generated.d.ts +1 -0
  8. package/dist/authoring/service-logo.generated.js +3 -0
  9. package/dist/authoring/service-logo.js +1 -0
  10. package/dist/authoring/session.d.ts +23 -0
  11. package/dist/authoring/session.js +127 -0
  12. package/dist/authoring/surface.d.ts +11 -0
  13. package/dist/authoring/surface.js +63 -0
  14. package/dist/authoring/toolbar-constants.d.ts +23 -0
  15. package/dist/authoring/toolbar-constants.js +42 -0
  16. package/dist/authoring/toolbar-drag.d.ts +29 -0
  17. package/dist/authoring/toolbar-drag.js +270 -0
  18. package/dist/authoring/toolbar-view.d.ts +21 -0
  19. package/dist/authoring/toolbar-view.js +2584 -0
  20. package/dist/capture/action.d.ts +2 -0
  21. package/dist/capture/action.js +62 -0
  22. package/dist/capture/dom.d.ts +23 -0
  23. package/dist/capture/dom.js +329 -0
  24. package/dist/capture/drag.d.ts +2 -0
  25. package/dist/capture/drag.js +75 -0
  26. package/dist/capture/error.d.ts +2 -0
  27. package/dist/capture/error.js +193 -0
  28. package/dist/capture/form.d.ts +2 -0
  29. package/dist/capture/form.js +137 -0
  30. package/dist/capture/frustration.d.ts +2 -0
  31. package/dist/capture/frustration.js +171 -0
  32. package/dist/capture/input.d.ts +2 -0
  33. package/dist/capture/input.js +109 -0
  34. package/dist/capture/location.d.ts +10 -0
  35. package/dist/capture/location.js +42 -0
  36. package/dist/capture/navigation.d.ts +2 -0
  37. package/dist/capture/navigation.js +100 -0
  38. package/dist/capture/network.d.ts +13 -0
  39. package/dist/capture/network.js +903 -0
  40. package/dist/capture/page.d.ts +2 -0
  41. package/dist/capture/page.js +78 -0
  42. package/dist/capture/performance.d.ts +2 -0
  43. package/dist/capture/performance.js +268 -0
  44. package/dist/context/account.d.ts +12 -0
  45. package/dist/context/account.js +129 -0
  46. package/dist/context/environment.d.ts +42 -0
  47. package/dist/context/environment.js +208 -0
  48. package/dist/context/identity.d.ts +14 -0
  49. package/dist/context/identity.js +123 -0
  50. package/dist/context/session.d.ts +28 -0
  51. package/dist/context/session.js +155 -0
  52. package/dist/context/tab.d.ts +22 -0
  53. package/dist/context/tab.js +142 -0
  54. package/dist/context/trace.d.ts +32 -0
  55. package/dist/context/trace.js +65 -0
  56. package/dist/core/config.d.ts +4 -0
  57. package/dist/core/config.js +199 -0
  58. package/dist/core/constants.d.ts +43 -0
  59. package/dist/core/constants.js +109 -0
  60. package/dist/core/contracts.d.ts +58 -0
  61. package/dist/core/contracts.js +53 -0
  62. package/dist/core/sdk.d.ts +2 -0
  63. package/dist/core/sdk.js +831 -0
  64. package/dist/core/types.d.ts +413 -0
  65. package/dist/core/types.js +1 -0
  66. package/dist/core/usage-governor.d.ts +7 -0
  67. package/dist/core/usage-governor.js +127 -0
  68. package/dist/index.d.ts +17 -0
  69. package/dist/index.js +36 -0
  70. package/dist/integrations/next-router.d.ts +16 -0
  71. package/dist/integrations/next-router.js +18 -0
  72. package/dist/integrations/react-router.d.ts +7 -0
  73. package/dist/integrations/react-router.js +37 -0
  74. package/dist/internal/metrics.d.ts +9 -0
  75. package/dist/internal/metrics.js +38 -0
  76. package/dist/normalize/builders.d.ts +15 -0
  77. package/dist/normalize/builders.js +786 -0
  78. package/dist/normalize/canonical.d.ts +13 -0
  79. package/dist/normalize/canonical.js +77 -0
  80. package/dist/normalize/event-id.d.ts +8 -0
  81. package/dist/normalize/event-id.js +39 -0
  82. package/dist/normalize/path-template.d.ts +1 -0
  83. package/dist/normalize/path-template.js +33 -0
  84. package/dist/privacy/local-minimization.d.ts +29 -0
  85. package/dist/privacy/local-minimization.js +88 -0
  86. package/dist/privacy/mask.d.ts +7 -0
  87. package/dist/privacy/mask.js +60 -0
  88. package/dist/privacy/parameter-snapshot.d.ts +14 -0
  89. package/dist/privacy/parameter-snapshot.js +206 -0
  90. package/dist/privacy/sanitize.d.ts +11 -0
  91. package/dist/privacy/sanitize.js +145 -0
  92. package/dist/privacy/schema-evidence.d.ts +20 -0
  93. package/dist/privacy/schema-evidence.js +238 -0
  94. package/dist/transport/batch.d.ts +37 -0
  95. package/dist/transport/batch.js +182 -0
  96. package/dist/transport/client.d.ts +61 -0
  97. package/dist/transport/client.js +267 -0
  98. package/dist/transport/queue.d.ts +22 -0
  99. package/dist/transport/queue.js +56 -0
  100. package/dist/transport/retry.d.ts +14 -0
  101. package/dist/transport/retry.js +46 -0
  102. package/package.json +38 -0
@@ -0,0 +1,2 @@
1
+ import type { CaptureModuleContext, CaptureStopHandle } from "../core/types";
2
+ export declare const startActionCapture: (context: CaptureModuleContext) => CaptureStopHandle;
@@ -0,0 +1,62 @@
1
+ import { buildLocalTextDescriptor } from "../privacy/local-minimization";
2
+ import { extractFieldName, extractFieldType, getElementAriaLabel, getElementClassList, getElementLabel, getElementPlaceholder, getElementRole, getElementSectionHeading, isEditableElement, resolveClickDisplayTarget, resolveStableKeyDescriptor, } from "./dom";
3
+ export const startActionCapture = (context) => {
4
+ if (typeof document === "undefined") {
5
+ return { stop: () => undefined };
6
+ }
7
+ const clickHandler = (event) => {
8
+ if (context.getConsent() === "denied") {
9
+ return;
10
+ }
11
+ const target = event.target;
12
+ if (!(target instanceof Element)) {
13
+ return;
14
+ }
15
+ if (context.shouldIgnoreElement?.(target)) {
16
+ return;
17
+ }
18
+ try {
19
+ const displayTarget = resolveClickDisplayTarget(target);
20
+ if (context.shouldIgnoreElement?.(displayTarget)) {
21
+ return;
22
+ }
23
+ const stableKey = resolveStableKeyDescriptor(displayTarget);
24
+ context.onEvent({
25
+ type: "element_clicked",
26
+ occurredAtMs: Date.now(),
27
+ payload: {
28
+ viewId: context.getViewId(),
29
+ elementTag: displayTarget.tagName.toLowerCase(),
30
+ elementRole: getElementRole(displayTarget),
31
+ stableKey: stableKey.stableKey,
32
+ stableKeyQuality: stableKey.stableKeyQuality,
33
+ formKey: stableKey.formKey,
34
+ elementId: displayTarget.id || null,
35
+ elementClass: getElementClassList(displayTarget),
36
+ elementLabelMeta: buildLocalTextDescriptor(getElementLabel(displayTarget)),
37
+ ariaLabelMeta: buildLocalTextDescriptor(getElementAriaLabel(displayTarget)),
38
+ placeholderMeta: buildLocalTextDescriptor(getElementPlaceholder(displayTarget)),
39
+ clickTargetText: getElementLabel(displayTarget),
40
+ clickTargetAriaLabel: getElementAriaLabel(displayTarget),
41
+ clickTargetTitle: displayTarget.getAttribute("title") || null,
42
+ fieldName: extractFieldName(displayTarget),
43
+ fieldType: extractFieldType(displayTarget),
44
+ fieldDisplayLabel: getElementLabel(displayTarget),
45
+ fieldAriaLabel: getElementAriaLabel(displayTarget),
46
+ fieldPlaceholder: getElementPlaceholder(displayTarget),
47
+ isEditable: isEditableElement(displayTarget),
48
+ sectionHeading: getElementSectionHeading(displayTarget),
49
+ },
50
+ });
51
+ }
52
+ catch (error) {
53
+ context.onInternalError("capture/action:click", error);
54
+ }
55
+ };
56
+ document.addEventListener("click", clickHandler, true);
57
+ return {
58
+ stop: () => {
59
+ document.removeEventListener("click", clickHandler, true);
60
+ },
61
+ };
62
+ };
@@ -0,0 +1,23 @@
1
+ export declare const buildDomPath: (element: Element) => string;
2
+ export declare const getTextPreview: (element: Element) => string | null;
3
+ export declare const getElementClassList: (element: Element) => string[] | null;
4
+ export declare const getElementRole: (element: Element) => string | null;
5
+ export declare const getElementPlaceholder: (element: Element) => string | null;
6
+ export declare const getElementAriaLabel: (element: Element) => string | null;
7
+ export declare const getElementSectionHeading: (element: Element) => string | null;
8
+ export declare const getElementLabel: (element: Element) => string | null;
9
+ export declare const resolveClickDisplayTarget: (element: Element) => Element;
10
+ export type StableKeyQuality = "official" | "compat" | "structural";
11
+ export type StableKeyDescriptor = {
12
+ stableKey: string;
13
+ stableKeyQuality: StableKeyQuality;
14
+ formKey: string | null;
15
+ };
16
+ export declare const getFormKey: (element: Element) => string | null;
17
+ export declare const resolveStableKeyDescriptor: (element: Element) => StableKeyDescriptor;
18
+ export declare const getStableKey: (element: Element) => string;
19
+ export declare const extractFieldName: (element: Element) => string | null;
20
+ export declare const extractFieldType: (element: Element) => string | null;
21
+ export declare const findParentFormId: (element: Element) => string | null;
22
+ export declare const isEditableElement: (element: Element) => boolean;
23
+ export declare const getElementValue: (element: Element) => unknown;
@@ -0,0 +1,329 @@
1
+ export const buildDomPath = (element) => {
2
+ const segments = [];
3
+ let current = element;
4
+ while (current && segments.length < 8) {
5
+ const tag = current.tagName.toLowerCase();
6
+ const id = current.id ? `#${current.id}` : "";
7
+ const className = typeof current.className === "string" && current.className.trim()
8
+ ? `.${current.className.trim().split(/\s+/).slice(0, 2).join(".")}`
9
+ : "";
10
+ segments.unshift(`${tag}${id}${className}`);
11
+ current = current.parentElement;
12
+ }
13
+ return segments.join(" > ");
14
+ };
15
+ export const getTextPreview = (element) => {
16
+ const text = element.textContent?.trim() ?? "";
17
+ if (!text) {
18
+ return null;
19
+ }
20
+ return text.slice(0, 120);
21
+ };
22
+ export const getElementClassList = (element) => {
23
+ if (typeof element.className !== "string") {
24
+ return null;
25
+ }
26
+ const classList = element.className.trim().split(/\s+/).filter(Boolean);
27
+ return classList.length > 0 ? classList : null;
28
+ };
29
+ export const getElementRole = (element) => {
30
+ return element.getAttribute("role") || null;
31
+ };
32
+ export const getElementPlaceholder = (element) => {
33
+ if (element instanceof HTMLInputElement ||
34
+ element instanceof HTMLTextAreaElement) {
35
+ return element.placeholder || null;
36
+ }
37
+ return null;
38
+ };
39
+ export const getElementAriaLabel = (element) => {
40
+ return element.getAttribute("aria-label") || null;
41
+ };
42
+ const SECTION_CONTAINER_SELECTOR = [
43
+ "section",
44
+ "article",
45
+ "aside",
46
+ "nav",
47
+ "main",
48
+ "form",
49
+ "fieldset",
50
+ "dialog",
51
+ "[role='dialog']",
52
+ "[role='region']",
53
+ ].join(",");
54
+ const SECTION_HEADING_SELECTOR = [
55
+ "h1",
56
+ "h2",
57
+ "h3",
58
+ "h4",
59
+ "h5",
60
+ "h6",
61
+ "legend",
62
+ ].join(",");
63
+ const readSectionHeadingLabel = (container) => {
64
+ const labelledBy = container.getAttribute("aria-labelledby");
65
+ if (labelledBy) {
66
+ const labelNode = container.ownerDocument?.getElementById(labelledBy);
67
+ if (labelNode instanceof Element) {
68
+ const text = getTextPreview(labelNode);
69
+ if (text) {
70
+ return text;
71
+ }
72
+ }
73
+ }
74
+ const headingNode = container.querySelector(SECTION_HEADING_SELECTOR);
75
+ if (headingNode instanceof Element) {
76
+ const text = getTextPreview(headingNode);
77
+ if (text) {
78
+ return text;
79
+ }
80
+ }
81
+ return (container.getAttribute("aria-label") ||
82
+ container.getAttribute("title") ||
83
+ null);
84
+ };
85
+ export const getElementSectionHeading = (element) => {
86
+ let container = element.closest(SECTION_CONTAINER_SELECTOR);
87
+ while (container) {
88
+ const label = readSectionHeadingLabel(container);
89
+ if (label) {
90
+ return label;
91
+ }
92
+ container = container.parentElement?.closest(SECTION_CONTAINER_SELECTOR) ?? null;
93
+ }
94
+ return null;
95
+ };
96
+ export const getElementLabel = (element) => {
97
+ if (element instanceof HTMLInputElement ||
98
+ element instanceof HTMLTextAreaElement ||
99
+ element instanceof HTMLSelectElement) {
100
+ const label = element.labels?.[0]?.textContent?.trim();
101
+ return label || null;
102
+ }
103
+ const ownText = Array.from(element.childNodes)
104
+ .filter((node) => node.nodeType === Node.TEXT_NODE)
105
+ .map((node) => node.textContent?.trim() ?? "")
106
+ .filter((value) => value.length > 0)
107
+ .join(" ")
108
+ .trim();
109
+ if (ownText) {
110
+ return ownText.slice(0, 120);
111
+ }
112
+ const isInteractiveDisplayTarget = element instanceof HTMLButtonElement ||
113
+ element instanceof HTMLAnchorElement ||
114
+ element.getAttribute("role") === "button" ||
115
+ element.getAttribute("role") === "link";
116
+ const descendantCandidates = Array.from(element.querySelectorAll("label, p, span, strong, em, small, button, a, [role='button'], [role='link']"))
117
+ .map((candidate) => candidate.textContent?.trim() ?? "")
118
+ .map((value) => value.replace(/\s+/g, " ").trim())
119
+ .filter((value) => value.length > 0 && value.length <= 80);
120
+ if (isInteractiveDisplayTarget && descendantCandidates.length > 0) {
121
+ return descendantCandidates[0] ?? null;
122
+ }
123
+ if (descendantCandidates.length > 0) {
124
+ descendantCandidates.sort((left, right) => {
125
+ const leftWordCount = left.split(/\s+/).length;
126
+ const rightWordCount = right.split(/\s+/).length;
127
+ if (leftWordCount !== rightWordCount) {
128
+ return leftWordCount - rightWordCount;
129
+ }
130
+ return left.length - right.length;
131
+ });
132
+ return descendantCandidates[0] ?? null;
133
+ }
134
+ return getTextPreview(element);
135
+ };
136
+ const CLICKABLE_DISPLAY_SELECTOR = [
137
+ "a[href]",
138
+ "button",
139
+ "input",
140
+ "textarea",
141
+ "select",
142
+ "[role='button']",
143
+ "[role='link']",
144
+ ].join(",");
145
+ export const resolveClickDisplayTarget = (element) => {
146
+ const matched = element.closest(CLICKABLE_DISPLAY_SELECTOR);
147
+ return matched ?? element;
148
+ };
149
+ const SAFE_FALLBACK_STABLE_IDENTIFIER = /^[a-z0-9._:-]{1,80}$/i;
150
+ const toSafeFallbackStableSegment = (value) => {
151
+ if (!value) {
152
+ return null;
153
+ }
154
+ const normalized = value.trim();
155
+ if (!normalized || !SAFE_FALLBACK_STABLE_IDENTIFIER.test(normalized)) {
156
+ return null;
157
+ }
158
+ return normalized.toLowerCase();
159
+ };
160
+ const buildCoarsePosition = (element) => {
161
+ const segments = [];
162
+ let current = element;
163
+ let depth = 0;
164
+ while (current && depth < 4) {
165
+ const parent = current.parentElement;
166
+ if (!parent) {
167
+ break;
168
+ }
169
+ const siblings = Array.from(parent.children);
170
+ const currentTagName = current.tagName;
171
+ const sameTagSiblings = siblings.filter((child) => child.tagName === currentTagName);
172
+ const positionIndex = siblings.indexOf(current) + 1;
173
+ const sameTagIndex = sameTagSiblings.indexOf(current) + 1;
174
+ segments.unshift(`p${positionIndex}-t${sameTagIndex}`);
175
+ current = parent;
176
+ depth += 1;
177
+ }
178
+ if (segments.length === 0) {
179
+ return "root";
180
+ }
181
+ return segments.join("/");
182
+ };
183
+ const buildStructuralStableKey = (element) => {
184
+ const tag = element.tagName.toLowerCase();
185
+ const role = toSafeFallbackStableSegment(getElementRole(element)) || "none";
186
+ const inputType = (element instanceof HTMLInputElement && element.type) ||
187
+ (element instanceof HTMLTextAreaElement && "textarea") ||
188
+ (element instanceof HTMLSelectElement && "select") ||
189
+ (element instanceof HTMLElement &&
190
+ element.isContentEditable &&
191
+ "contenteditable") ||
192
+ "na";
193
+ return `struct:${tag}:${role}:${inputType}:${buildCoarsePosition(element)}`;
194
+ };
195
+ export const getFormKey = (element) => {
196
+ return toSafeFallbackStableSegment(findParentFormId(element));
197
+ };
198
+ export const resolveStableKeyDescriptor = (element) => {
199
+ const dataTestId = toSafeFallbackStableSegment(element.getAttribute("data-testid"));
200
+ if (dataTestId) {
201
+ return {
202
+ stableKey: `dtid:${dataTestId}`,
203
+ stableKeyQuality: "compat",
204
+ formKey: getFormKey(element),
205
+ };
206
+ }
207
+ const dataQa = toSafeFallbackStableSegment(element.getAttribute("data-qa"));
208
+ if (dataQa) {
209
+ return {
210
+ stableKey: `dqa:${dataQa}`,
211
+ stableKeyQuality: "compat",
212
+ formKey: getFormKey(element),
213
+ };
214
+ }
215
+ const safeName = toSafeFallbackStableSegment(element.getAttribute("name"));
216
+ if (safeName) {
217
+ return {
218
+ stableKey: `name:${safeName}`,
219
+ stableKeyQuality: "structural",
220
+ formKey: getFormKey(element),
221
+ };
222
+ }
223
+ const safeAriaLabel = toSafeFallbackStableSegment(getElementAriaLabel(element));
224
+ if (safeAriaLabel) {
225
+ return {
226
+ stableKey: `aria:${safeAriaLabel}`,
227
+ stableKeyQuality: "structural",
228
+ formKey: getFormKey(element),
229
+ };
230
+ }
231
+ const safeId = toSafeFallbackStableSegment(element.id);
232
+ if (safeId) {
233
+ return {
234
+ stableKey: `id:${safeId}`,
235
+ stableKeyQuality: "structural",
236
+ formKey: getFormKey(element),
237
+ };
238
+ }
239
+ return {
240
+ stableKey: buildStructuralStableKey(element),
241
+ stableKeyQuality: "structural",
242
+ formKey: getFormKey(element),
243
+ };
244
+ };
245
+ export const getStableKey = (element) => {
246
+ return resolveStableKeyDescriptor(element).stableKey;
247
+ };
248
+ export const extractFieldName = (element) => {
249
+ if (element instanceof HTMLInputElement ||
250
+ element instanceof HTMLTextAreaElement ||
251
+ element instanceof HTMLSelectElement) {
252
+ return (element.getAttribute("name") ||
253
+ element.getAttribute("id") ||
254
+ element.getAttribute("aria-label") ||
255
+ null);
256
+ }
257
+ if (element instanceof HTMLElement && element.isContentEditable) {
258
+ return (element.getAttribute("data-field-name") ||
259
+ element.getAttribute("aria-label") ||
260
+ element.getAttribute("id") ||
261
+ null);
262
+ }
263
+ return null;
264
+ };
265
+ export const extractFieldType = (element) => {
266
+ if (element instanceof HTMLInputElement) {
267
+ return element.type || "input";
268
+ }
269
+ if (element instanceof HTMLTextAreaElement) {
270
+ return "textarea";
271
+ }
272
+ if (element instanceof HTMLSelectElement) {
273
+ return "select";
274
+ }
275
+ if (element instanceof HTMLElement && element.isContentEditable) {
276
+ return "contenteditable";
277
+ }
278
+ return null;
279
+ };
280
+ export const findParentFormId = (element) => {
281
+ if (element instanceof HTMLInputElement ||
282
+ element instanceof HTMLTextAreaElement ||
283
+ element instanceof HTMLSelectElement) {
284
+ const form = element.form;
285
+ return form ? form.id || form.getAttribute("name") || null : null;
286
+ }
287
+ if (element instanceof HTMLElement) {
288
+ const form = element.closest("form");
289
+ return form instanceof HTMLFormElement
290
+ ? form.id || form.getAttribute("name") || null
291
+ : null;
292
+ }
293
+ return null;
294
+ };
295
+ export const isEditableElement = (element) => {
296
+ return Boolean(element instanceof HTMLInputElement ||
297
+ element instanceof HTMLTextAreaElement ||
298
+ element instanceof HTMLSelectElement ||
299
+ (element instanceof HTMLElement && element.isContentEditable));
300
+ };
301
+ export const getElementValue = (element) => {
302
+ if (element instanceof HTMLInputElement) {
303
+ if (element.type === "checkbox") {
304
+ return element.checked;
305
+ }
306
+ if (element.type === "radio") {
307
+ return element.checked ? element.value : null;
308
+ }
309
+ if (element.type === "file") {
310
+ return {
311
+ fileCount: element.files?.length ?? 0,
312
+ };
313
+ }
314
+ return element.value;
315
+ }
316
+ if (element instanceof HTMLTextAreaElement) {
317
+ return element.value;
318
+ }
319
+ if (element instanceof HTMLSelectElement) {
320
+ if (element.multiple) {
321
+ return Array.from(element.selectedOptions).map((option) => option.value);
322
+ }
323
+ return element.value;
324
+ }
325
+ if (element instanceof HTMLElement && element.isContentEditable) {
326
+ return element.textContent?.trim() ?? null;
327
+ }
328
+ return null;
329
+ };
@@ -0,0 +1,2 @@
1
+ import type { CaptureModuleContext, CaptureStopHandle } from "../core/types";
2
+ export declare const startDragCapture: (context: CaptureModuleContext) => CaptureStopHandle;
@@ -0,0 +1,75 @@
1
+ import { buildLocalValueDescriptor } from "../privacy/local-minimization";
2
+ import { extractFieldName, extractFieldType, getElementAriaLabel, getElementLabel, getElementPlaceholder, getElementRole, getElementSectionHeading, resolveStableKeyDescriptor, } from "./dom";
3
+ const readDragPayload = (event) => {
4
+ const dataTransfer = event.dataTransfer;
5
+ if (!dataTransfer) {
6
+ return {
7
+ itemCount: 0,
8
+ fileCount: 0,
9
+ dragTypes: [],
10
+ };
11
+ }
12
+ const itemCount = dataTransfer.items?.length ?? 0;
13
+ const fileCount = dataTransfer.files?.length ?? 0;
14
+ const dragTypes = Array.from(dataTransfer.types ?? []).filter(Boolean);
15
+ return {
16
+ itemCount,
17
+ fileCount,
18
+ dragTypes,
19
+ };
20
+ };
21
+ export const startDragCapture = (context) => {
22
+ if (typeof document === "undefined") {
23
+ return { stop: () => undefined };
24
+ }
25
+ const dropHandler = (event) => {
26
+ if (context.getConsent() === "denied") {
27
+ return;
28
+ }
29
+ const target = event.target;
30
+ if (!(target instanceof Element)) {
31
+ return;
32
+ }
33
+ if (context.shouldIgnoreElement?.(target)) {
34
+ return;
35
+ }
36
+ try {
37
+ const stableKey = resolveStableKeyDescriptor(target);
38
+ const dragPayload = readDragPayload(event);
39
+ context.onEvent({
40
+ type: "drag_drop_completed",
41
+ occurredAtMs: Date.now(),
42
+ payload: {
43
+ viewId: context.getViewId(),
44
+ elementTag: target.tagName.toLowerCase(),
45
+ elementRole: getElementRole(target),
46
+ stableKey: stableKey.stableKey,
47
+ stableKeyQuality: stableKey.stableKeyQuality,
48
+ formKey: stableKey.formKey,
49
+ fieldNameMeta: buildLocalValueDescriptor(extractFieldName(target)),
50
+ elementLabelMeta: buildLocalValueDescriptor(getElementLabel(target)),
51
+ ariaLabelMeta: buildLocalValueDescriptor(getElementAriaLabel(target)),
52
+ placeholderMeta: buildLocalValueDescriptor(getElementPlaceholder(target)),
53
+ fieldName: extractFieldName(target),
54
+ fieldType: extractFieldType(target),
55
+ fieldDisplayLabel: getElementLabel(target),
56
+ fieldAriaLabel: getElementAriaLabel(target),
57
+ fieldPlaceholder: getElementPlaceholder(target),
58
+ sectionHeading: getElementSectionHeading(target),
59
+ dragItemCount: dragPayload.itemCount,
60
+ fileCount: dragPayload.fileCount,
61
+ dragTypes: dragPayload.dragTypes,
62
+ },
63
+ });
64
+ }
65
+ catch (error) {
66
+ context.onInternalError("capture/drag:drop", error);
67
+ }
68
+ };
69
+ document.addEventListener("drop", dropHandler, true);
70
+ return {
71
+ stop: () => {
72
+ document.removeEventListener("drop", dropHandler, true);
73
+ },
74
+ };
75
+ };
@@ -0,0 +1,2 @@
1
+ import type { CaptureModuleContext, CaptureStopHandle } from "../core/types";
2
+ export declare const startErrorCapture: (context: CaptureModuleContext) => CaptureStopHandle;
@@ -0,0 +1,193 @@
1
+ const ERROR_BURST_DEBOUNCE_MS = 250;
2
+ const hashString = (value) => {
3
+ let hash = 2166136261;
4
+ for (let index = 0; index < value.length; index += 1) {
5
+ hash ^= value.charCodeAt(index);
6
+ hash = Math.imul(hash, 16777619);
7
+ }
8
+ return `err_${(hash >>> 0).toString(16).padStart(8, "0")}`;
9
+ };
10
+ const sourceFileHintFromValue = (value) => {
11
+ if (!value) {
12
+ return null;
13
+ }
14
+ try {
15
+ const resolved = new URL(value, globalThis.location?.origin);
16
+ const segments = resolved.pathname.split("/").filter(Boolean);
17
+ return segments[segments.length - 1] ?? null;
18
+ }
19
+ catch {
20
+ const normalized = value.split(/[?#]/, 1)[0] ?? value;
21
+ const segments = normalized.split("/").filter(Boolean);
22
+ return segments[segments.length - 1] ?? (normalized || null);
23
+ }
24
+ };
25
+ const buildFingerprint = (input) => {
26
+ const firstStackLine = input.stack
27
+ ?.split("\n")
28
+ .map((line) => line.trim())
29
+ .find(Boolean) ?? "";
30
+ return hashString([
31
+ input.type,
32
+ input.errorType ?? "",
33
+ input.message ?? "",
34
+ sourceFileHintFromValue(input.sourceFile) ?? "",
35
+ input.lineNumber ?? "",
36
+ input.columnNumber ?? "",
37
+ firstStackLine,
38
+ ].join("|"));
39
+ };
40
+ export const startErrorCapture = (context) => {
41
+ if (typeof window === "undefined") {
42
+ return { stop: () => undefined };
43
+ }
44
+ const pendingByFingerprint = new Map();
45
+ const flushFingerprint = (fingerprint) => {
46
+ const bucket = pendingByFingerprint.get(fingerprint);
47
+ if (!bucket) {
48
+ return;
49
+ }
50
+ if (bucket.timer) {
51
+ clearTimeout(bucket.timer);
52
+ }
53
+ context.onEvent({
54
+ type: bucket.event.type,
55
+ occurredAtMs: bucket.lastOccurredAtMs,
56
+ payload: {
57
+ ...bucket.event.payload,
58
+ repeatCount: bucket.repeatCount,
59
+ },
60
+ });
61
+ pendingByFingerprint.delete(fingerprint);
62
+ };
63
+ const scheduleFlush = (fingerprint) => {
64
+ const bucket = pendingByFingerprint.get(fingerprint);
65
+ if (!bucket) {
66
+ return;
67
+ }
68
+ if (bucket.timer) {
69
+ clearTimeout(bucket.timer);
70
+ }
71
+ bucket.timer = setTimeout(() => {
72
+ flushFingerprint(fingerprint);
73
+ }, ERROR_BURST_DEBOUNCE_MS);
74
+ };
75
+ const queueErrorEnvelope = (type, occurredAtMs, payload) => {
76
+ const fingerprint = String(payload.errorFingerprint ?? "");
77
+ if (!fingerprint) {
78
+ context.onEvent({
79
+ type,
80
+ occurredAtMs,
81
+ payload: {
82
+ ...payload,
83
+ repeatCount: 1,
84
+ },
85
+ });
86
+ return;
87
+ }
88
+ const existing = pendingByFingerprint.get(fingerprint);
89
+ if (existing) {
90
+ existing.repeatCount += 1;
91
+ existing.lastOccurredAtMs = occurredAtMs;
92
+ existing.event.payload = {
93
+ ...existing.event.payload,
94
+ ...payload,
95
+ };
96
+ scheduleFlush(fingerprint);
97
+ return;
98
+ }
99
+ pendingByFingerprint.set(fingerprint, {
100
+ event: {
101
+ type,
102
+ occurredAtMs,
103
+ payload,
104
+ },
105
+ repeatCount: 1,
106
+ lastOccurredAtMs: occurredAtMs,
107
+ timer: null,
108
+ });
109
+ scheduleFlush(fingerprint);
110
+ };
111
+ const errorHandler = (event) => {
112
+ if (context.getConsent() === "denied") {
113
+ return;
114
+ }
115
+ try {
116
+ const occurredAtMs = Date.now();
117
+ const sourceFile = event.filename || null;
118
+ const fingerprint = buildFingerprint({
119
+ type: "frontend_error",
120
+ errorType: event.error?.name || event.type || "Error",
121
+ message: event.message || null,
122
+ sourceFile,
123
+ lineNumber: event.lineno || undefined,
124
+ columnNumber: event.colno || undefined,
125
+ stack: event.error && typeof event.error.stack === "string"
126
+ ? event.error.stack
127
+ : null,
128
+ });
129
+ queueErrorEnvelope("frontend_error", occurredAtMs, {
130
+ viewId: context.getViewId(),
131
+ errorType: event.error?.name || event.type || "Error",
132
+ errorFingerprint: fingerprint,
133
+ sourceFileHint: sourceFileHintFromValue(sourceFile),
134
+ message: event.message || null,
135
+ stack: event.error && typeof event.error.stack === "string"
136
+ ? event.error.stack
137
+ : null,
138
+ sourceFile,
139
+ lineNumber: event.lineno || undefined,
140
+ columnNumber: event.colno || undefined,
141
+ });
142
+ }
143
+ catch (error) {
144
+ context.onInternalError("capture/error:error", error);
145
+ }
146
+ };
147
+ const rejectionHandler = (event) => {
148
+ if (context.getConsent() === "denied") {
149
+ return;
150
+ }
151
+ try {
152
+ const reason = event.reason;
153
+ const message = reason instanceof Error
154
+ ? reason.message
155
+ : typeof reason === "string"
156
+ ? reason
157
+ : null;
158
+ const stack = reason instanceof Error ? (reason.stack ?? null) : null;
159
+ const errorType = reason instanceof Error ? reason.name : "PromiseRejection";
160
+ const occurredAtMs = Date.now();
161
+ const fingerprint = buildFingerprint({
162
+ type: "unhandled_rejection",
163
+ errorType,
164
+ message,
165
+ sourceFile: null,
166
+ stack,
167
+ });
168
+ queueErrorEnvelope("unhandled_rejection", occurredAtMs, {
169
+ viewId: context.getViewId(),
170
+ errorType,
171
+ errorFingerprint: fingerprint,
172
+ sourceFileHint: null,
173
+ message,
174
+ stack,
175
+ sourceFile: null,
176
+ });
177
+ }
178
+ catch (error) {
179
+ context.onInternalError("capture/error:unhandledrejection", error);
180
+ }
181
+ };
182
+ window.addEventListener("error", errorHandler);
183
+ window.addEventListener("unhandledrejection", rejectionHandler);
184
+ return {
185
+ stop: () => {
186
+ for (const fingerprint of pendingByFingerprint.keys()) {
187
+ flushFingerprint(fingerprint);
188
+ }
189
+ window.removeEventListener("error", errorHandler);
190
+ window.removeEventListener("unhandledrejection", rejectionHandler);
191
+ },
192
+ };
193
+ };