@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 startFormCapture: (context: CaptureModuleContext) => CaptureStopHandle;
@@ -0,0 +1,137 @@
1
+ import { buildLocalTextDescriptor } from "../privacy/local-minimization";
2
+ import { extractFieldName, extractFieldType, getElementAriaLabel, getElementClassList, getElementLabel, getElementPlaceholder, getElementRole, getElementSectionHeading, isEditableElement, resolveClickDisplayTarget, resolveStableKeyDescriptor, } from "./dom";
3
+ const VALIDATION_REASON_KEYS = [
4
+ "valueMissing",
5
+ "typeMismatch",
6
+ "patternMismatch",
7
+ "tooLong",
8
+ "tooShort",
9
+ "rangeUnderflow",
10
+ "rangeOverflow",
11
+ "stepMismatch",
12
+ "badInput",
13
+ "customError",
14
+ ];
15
+ const toSnakeCase = (value) => value.replace(/[A-Z]/g, (char) => `_${char.toLowerCase()}`);
16
+ const isFormControlElement = (element) => element instanceof HTMLInputElement ||
17
+ element instanceof HTMLSelectElement ||
18
+ element instanceof HTMLTextAreaElement;
19
+ const fingerprintText = (value) => {
20
+ let hash = 0x811c9dc5;
21
+ for (let index = 0; index < value.length; index += 1) {
22
+ hash ^= value.charCodeAt(index);
23
+ hash = Math.imul(hash, 0x01000193);
24
+ }
25
+ return `fnv1a:${(hash >>> 0).toString(16).padStart(8, "0")}`;
26
+ };
27
+ const buildValidationEvidence = (form) => {
28
+ const invalidControls = Array.from(form.elements)
29
+ .filter((element) => {
30
+ return isFormControlElement(element) && !element.validity.valid;
31
+ })
32
+ .slice(0, 25);
33
+ const invalidFieldStableKeys = invalidControls
34
+ .map((element) => resolveStableKeyDescriptor(element)?.stableKey ?? null)
35
+ .filter((value) => Boolean(value));
36
+ const validationReasonCodes = [
37
+ ...new Set(invalidControls.flatMap((element) => VALIDATION_REASON_KEYS
38
+ .filter((key) => element.validity[key])
39
+ .map((key) => toSnakeCase(key)))),
40
+ ].sort();
41
+ const validationMessages = invalidControls
42
+ .map((element) => element.validationMessage.trim())
43
+ .filter(Boolean)
44
+ .sort();
45
+ return {
46
+ invalidFieldCount: invalidControls.length,
47
+ invalidFieldStableKeys,
48
+ validationReasonCodes,
49
+ validationMessageFingerprint: validationMessages.length > 0
50
+ ? fingerprintText(validationMessages.join("|"))
51
+ : null,
52
+ };
53
+ };
54
+ export const startFormCapture = (context) => {
55
+ if (typeof document === "undefined") {
56
+ return { stop: () => undefined };
57
+ }
58
+ const submitHandler = (event) => {
59
+ if (context.getConsent() === "denied") {
60
+ return;
61
+ }
62
+ const target = event.target;
63
+ if (!(target instanceof HTMLFormElement)) {
64
+ return;
65
+ }
66
+ if (context.shouldIgnoreElement?.(target)) {
67
+ return;
68
+ }
69
+ try {
70
+ const formKey = target.id ||
71
+ target.getAttribute("name") ||
72
+ null;
73
+ const rawSubmitter = event.submitter instanceof Element ? event.submitter : null;
74
+ const submitter = rawSubmitter
75
+ ? resolveClickDisplayTarget(rawSubmitter)
76
+ : null;
77
+ const submitterStableKey = submitter
78
+ ? resolveStableKeyDescriptor(submitter)
79
+ : null;
80
+ const validationEvidence = buildValidationEvidence(target);
81
+ const submitResult = validationEvidence.invalidFieldCount > 0
82
+ ? "validation_blocked"
83
+ : "unknown";
84
+ const resultSource = validationEvidence.invalidFieldCount > 0 ? "browser_validation" : null;
85
+ context.onEvent({
86
+ type: "form_submitted",
87
+ occurredAtMs: Date.now(),
88
+ payload: {
89
+ viewId: context.getViewId(),
90
+ formId: target.id || null,
91
+ formKey,
92
+ formName: target.getAttribute("name"),
93
+ submitResult,
94
+ resultSource,
95
+ invalidFieldCount: validationEvidence.invalidFieldCount,
96
+ invalidFieldStableKeys: validationEvidence.invalidFieldStableKeys,
97
+ validationReasonCodes: validationEvidence.validationReasonCodes,
98
+ validationMessageFingerprint: validationEvidence.validationMessageFingerprint,
99
+ elementTag: submitter?.tagName.toLowerCase() ?? null,
100
+ elementRole: submitter ? getElementRole(submitter) : null,
101
+ stableKey: submitterStableKey?.stableKey ?? null,
102
+ stableKeyQuality: submitterStableKey?.stableKeyQuality ?? null,
103
+ elementId: submitter?.id || null,
104
+ elementClass: submitter ? getElementClassList(submitter) : null,
105
+ elementLabelMeta: buildLocalTextDescriptor(submitter ? getElementLabel(submitter) : null),
106
+ ariaLabelMeta: buildLocalTextDescriptor(submitter ? getElementAriaLabel(submitter) : null),
107
+ placeholderMeta: buildLocalTextDescriptor(submitter ? getElementPlaceholder(submitter) : null),
108
+ clickTargetText: submitter ? getElementLabel(submitter) : null,
109
+ clickTargetAriaLabel: submitter
110
+ ? getElementAriaLabel(submitter)
111
+ : null,
112
+ clickTargetTitle: submitter?.getAttribute("title") || null,
113
+ fieldName: submitter ? extractFieldName(submitter) : null,
114
+ fieldType: submitter ? extractFieldType(submitter) : null,
115
+ fieldDisplayLabel: submitter ? getElementLabel(submitter) : null,
116
+ fieldAriaLabel: submitter
117
+ ? getElementAriaLabel(submitter)
118
+ : null,
119
+ fieldPlaceholder: submitter
120
+ ? getElementPlaceholder(submitter)
121
+ : null,
122
+ isEditable: submitter ? isEditableElement(submitter) : false,
123
+ sectionHeading: getElementSectionHeading(target),
124
+ },
125
+ });
126
+ }
127
+ catch (error) {
128
+ context.onInternalError("capture/form:submit", error);
129
+ }
130
+ };
131
+ document.addEventListener("submit", submitHandler, true);
132
+ return {
133
+ stop: () => {
134
+ document.removeEventListener("submit", submitHandler, true);
135
+ },
136
+ };
137
+ };
@@ -0,0 +1,2 @@
1
+ import type { CaptureModuleContext, CaptureStopHandle } from "../core/types";
2
+ export declare const startFrustrationCapture: (context: CaptureModuleContext) => CaptureStopHandle;
@@ -0,0 +1,171 @@
1
+ import { buildLocalTextDescriptor } from "../privacy/local-minimization";
2
+ import { getElementAriaLabel, getElementLabel, getElementPlaceholder, getElementRole, resolveClickDisplayTarget, resolveStableKeyDescriptor, } from "./dom";
3
+ const CLICK_WINDOW_MS = 1200;
4
+ const RAGE_CLICK_COUNT = 3;
5
+ const RAGE_DISTANCE_PX = 24;
6
+ const buildDomPath = (element) => {
7
+ const segments = [];
8
+ let current = element;
9
+ while (current && segments.length < 8) {
10
+ const tag = current.tagName.toLowerCase();
11
+ const id = current.id ? `#${current.id}` : "";
12
+ const className = typeof current.className === "string" && current.className.trim()
13
+ ? `.${current.className.trim().split(/\s+/).slice(0, 2).join(".")}`
14
+ : "";
15
+ segments.unshift(`${tag}${id}${className}`);
16
+ current = current.parentElement;
17
+ }
18
+ return segments.join(" > ");
19
+ };
20
+ const isLikelyInteractive = (element) => {
21
+ const interactiveTags = new Set([
22
+ "a",
23
+ "button",
24
+ "input",
25
+ "select",
26
+ "textarea",
27
+ "summary",
28
+ ]);
29
+ const tag = element.tagName.toLowerCase();
30
+ if (interactiveTags.has(tag)) {
31
+ return true;
32
+ }
33
+ if (element.getAttribute("role") === "button") {
34
+ return true;
35
+ }
36
+ if (element.hasAttribute("onclick")) {
37
+ return true;
38
+ }
39
+ return false;
40
+ };
41
+ const isLikelyErrorElement = (element) => {
42
+ if (element.getAttribute("aria-invalid") === "true") {
43
+ return true;
44
+ }
45
+ const cls = typeof element.className === "string"
46
+ ? element.className.toLowerCase()
47
+ : "";
48
+ return cls.includes("error") || cls.includes("invalid");
49
+ };
50
+ const isWithinDistance = (a, b) => {
51
+ const dx = a.x - b.x;
52
+ const dy = a.y - b.y;
53
+ return dx * dx + dy * dy <= RAGE_DISTANCE_PX * RAGE_DISTANCE_PX;
54
+ };
55
+ export const startFrustrationCapture = (context) => {
56
+ if (typeof document === "undefined" || typeof window === "undefined") {
57
+ return { stop: () => undefined };
58
+ }
59
+ const clickSamples = [];
60
+ const timeoutHandles = new Set();
61
+ const clickHandler = (event) => {
62
+ if (context.getConsent() === "denied") {
63
+ return;
64
+ }
65
+ const target = event.target;
66
+ if (!(target instanceof Element)) {
67
+ return;
68
+ }
69
+ if (context.shouldIgnoreElement?.(target)) {
70
+ return;
71
+ }
72
+ const displayTarget = resolveClickDisplayTarget(target);
73
+ if (context.shouldIgnoreElement?.(displayTarget)) {
74
+ return;
75
+ }
76
+ const now = Date.now();
77
+ const domPath = buildDomPath(displayTarget);
78
+ const sample = {
79
+ at: now,
80
+ x: event.clientX,
81
+ y: event.clientY,
82
+ domPath,
83
+ };
84
+ clickSamples.push(sample);
85
+ while (clickSamples.length > 0 &&
86
+ now - clickSamples[0].at > CLICK_WINDOW_MS) {
87
+ clickSamples.shift();
88
+ }
89
+ const recentOnSameTarget = clickSamples.filter((entry) => {
90
+ return (now - entry.at <= CLICK_WINDOW_MS &&
91
+ entry.domPath === domPath &&
92
+ isWithinDistance(entry, sample));
93
+ });
94
+ if (recentOnSameTarget.length >= RAGE_CLICK_COUNT) {
95
+ const stableKey = resolveStableKeyDescriptor(displayTarget);
96
+ context.onEvent({
97
+ type: "rage_click_detected",
98
+ occurredAtMs: now,
99
+ payload: {
100
+ viewId: context.getViewId(),
101
+ stableKey: stableKey.stableKey,
102
+ stableKeyQuality: stableKey.stableKeyQuality,
103
+ formKey: stableKey.formKey,
104
+ elementTag: displayTarget.tagName.toLowerCase(),
105
+ elementRole: getElementRole(displayTarget),
106
+ elementLabelMeta: buildLocalTextDescriptor(getElementLabel(displayTarget)),
107
+ ariaLabelMeta: buildLocalTextDescriptor(getElementAriaLabel(displayTarget)),
108
+ placeholderMeta: buildLocalTextDescriptor(getElementPlaceholder(displayTarget)),
109
+ repeatCount: recentOnSameTarget.length,
110
+ },
111
+ });
112
+ }
113
+ if (isLikelyErrorElement(displayTarget)) {
114
+ const stableKey = resolveStableKeyDescriptor(displayTarget);
115
+ context.onEvent({
116
+ type: "error_click_detected",
117
+ occurredAtMs: now,
118
+ payload: {
119
+ viewId: context.getViewId(),
120
+ stableKey: stableKey.stableKey,
121
+ stableKeyQuality: stableKey.stableKeyQuality,
122
+ formKey: stableKey.formKey,
123
+ elementTag: displayTarget.tagName.toLowerCase(),
124
+ elementRole: getElementRole(displayTarget),
125
+ elementLabelMeta: buildLocalTextDescriptor(getElementLabel(displayTarget)),
126
+ ariaLabelMeta: buildLocalTextDescriptor(getElementAriaLabel(displayTarget)),
127
+ placeholderMeta: buildLocalTextDescriptor(getElementPlaceholder(displayTarget)),
128
+ repeatCount: 1,
129
+ },
130
+ });
131
+ }
132
+ const locationBefore = window.location.href;
133
+ const activeBefore = document.activeElement;
134
+ const interactive = isLikelyInteractive(displayTarget);
135
+ const timeoutHandle = window.setTimeout(() => {
136
+ timeoutHandles.delete(timeoutHandle);
137
+ const noNavigation = window.location.href === locationBefore;
138
+ const noFocusChange = document.activeElement === activeBefore;
139
+ if (!interactive && noNavigation && noFocusChange) {
140
+ const stableKey = resolveStableKeyDescriptor(displayTarget);
141
+ context.onEvent({
142
+ type: "dead_click_detected",
143
+ occurredAtMs: Date.now(),
144
+ payload: {
145
+ viewId: context.getViewId(),
146
+ stableKey: stableKey.stableKey,
147
+ stableKeyQuality: stableKey.stableKeyQuality,
148
+ formKey: stableKey.formKey,
149
+ elementTag: displayTarget.tagName.toLowerCase(),
150
+ elementRole: getElementRole(displayTarget),
151
+ elementLabelMeta: buildLocalTextDescriptor(getElementLabel(displayTarget)),
152
+ ariaLabelMeta: buildLocalTextDescriptor(getElementAriaLabel(displayTarget)),
153
+ placeholderMeta: buildLocalTextDescriptor(getElementPlaceholder(displayTarget)),
154
+ repeatCount: 1,
155
+ },
156
+ });
157
+ }
158
+ }, 900);
159
+ timeoutHandles.add(timeoutHandle);
160
+ };
161
+ document.addEventListener("click", clickHandler, true);
162
+ return {
163
+ stop: () => {
164
+ document.removeEventListener("click", clickHandler, true);
165
+ for (const handle of timeoutHandles) {
166
+ window.clearTimeout(handle);
167
+ }
168
+ timeoutHandles.clear();
169
+ },
170
+ };
171
+ };
@@ -0,0 +1,2 @@
1
+ import type { CaptureModuleContext, CaptureStopHandle } from "../core/types";
2
+ export declare const startInputCapture: (context: CaptureModuleContext) => CaptureStopHandle;
@@ -0,0 +1,109 @@
1
+ import { buildLocalTextDescriptor, buildLocalValueDescriptor, } from "../privacy/local-minimization";
2
+ import { extractFieldName, extractFieldType, getElementAriaLabel, getElementLabel, getElementPlaceholder, getElementRole, getElementSectionHeading, getElementValue, resolveStableKeyDescriptor, } from "./dom";
3
+ const buildFileDescriptors = (input) => {
4
+ const files = input.files ? Array.from(input.files) : [];
5
+ return files.map((file) => ({
6
+ mimeType: file.type || null,
7
+ size: file.size,
8
+ }));
9
+ };
10
+ export const startInputCapture = (context) => {
11
+ if (typeof document === "undefined") {
12
+ return { stop: () => undefined };
13
+ }
14
+ const changeHandler = (event) => {
15
+ if (context.getConsent() === "denied") {
16
+ return;
17
+ }
18
+ const target = event.target;
19
+ if (!(target instanceof HTMLInputElement) &&
20
+ !(target instanceof HTMLTextAreaElement) &&
21
+ !(target instanceof HTMLSelectElement)) {
22
+ return;
23
+ }
24
+ if (context.shouldIgnoreElement?.(target)) {
25
+ return;
26
+ }
27
+ try {
28
+ const stableKey = resolveStableKeyDescriptor(target);
29
+ const commonPayload = {
30
+ viewId: context.getViewId(),
31
+ formId: stableKey.formKey,
32
+ formKey: stableKey.formKey,
33
+ fieldType: extractFieldType(target),
34
+ elementTag: target.tagName.toLowerCase(),
35
+ elementRole: getElementRole(target),
36
+ stableKey: stableKey.stableKey,
37
+ stableKeyQuality: stableKey.stableKeyQuality,
38
+ fieldNameMeta: buildLocalTextDescriptor(extractFieldName(target)),
39
+ elementLabelMeta: buildLocalTextDescriptor(getElementLabel(target)),
40
+ ariaLabelMeta: buildLocalTextDescriptor(getElementAriaLabel(target)),
41
+ placeholderMeta: buildLocalTextDescriptor(getElementPlaceholder(target)),
42
+ fieldName: extractFieldName(target),
43
+ fieldDisplayLabel: getElementLabel(target),
44
+ fieldAriaLabel: getElementAriaLabel(target),
45
+ fieldPlaceholder: getElementPlaceholder(target),
46
+ sectionHeading: getElementSectionHeading(target),
47
+ };
48
+ if (target instanceof HTMLInputElement &&
49
+ (target.type === "checkbox" || target.type === "radio")) {
50
+ context.onEvent({
51
+ type: "toggle_changed",
52
+ occurredAtMs: Date.now(),
53
+ payload: {
54
+ ...commonPayload,
55
+ checked: target.checked,
56
+ inputValueMeta: buildLocalValueDescriptor(target.value),
57
+ },
58
+ });
59
+ return;
60
+ }
61
+ if (target instanceof HTMLSelectElement) {
62
+ const selectedValues = Array.from(target.selectedOptions).map((option) => option.value);
63
+ context.onEvent({
64
+ type: "selection_committed",
65
+ occurredAtMs: Date.now(),
66
+ payload: {
67
+ ...commonPayload,
68
+ selectedCount: selectedValues.length,
69
+ selectedValueMeta: buildLocalValueDescriptor(selectedValues),
70
+ },
71
+ });
72
+ return;
73
+ }
74
+ if (target instanceof HTMLInputElement && target.type === "file") {
75
+ const fileDescriptors = buildFileDescriptors(target);
76
+ context.onEvent({
77
+ type: "file_selected",
78
+ occurredAtMs: Date.now(),
79
+ payload: {
80
+ ...commonPayload,
81
+ fileCount: fileDescriptors.length,
82
+ fileSelectionMeta: buildLocalValueDescriptor({
83
+ fileCount: fileDescriptors.length,
84
+ }),
85
+ fileDescriptors,
86
+ },
87
+ });
88
+ return;
89
+ }
90
+ context.onEvent({
91
+ type: "input_committed",
92
+ occurredAtMs: Date.now(),
93
+ payload: {
94
+ ...commonPayload,
95
+ inputValueMeta: buildLocalValueDescriptor(getElementValue(target)),
96
+ },
97
+ });
98
+ }
99
+ catch (error) {
100
+ context.onInternalError("capture/input:change", error);
101
+ }
102
+ };
103
+ document.addEventListener("change", changeHandler, true);
104
+ return {
105
+ stop: () => {
106
+ document.removeEventListener("change", changeHandler, true);
107
+ },
108
+ };
109
+ };
@@ -0,0 +1,10 @@
1
+ type PageSnapshot = {
2
+ url: string | null;
3
+ path: string | null;
4
+ screenKey: string | null;
5
+ urlCanonical: string | null;
6
+ pageTitleCandidate: string | null;
7
+ primaryHeadingCandidate: string | null;
8
+ };
9
+ export declare const getCurrentPageSnapshot: () => PageSnapshot;
10
+ export {};
@@ -0,0 +1,42 @@
1
+ const buildUrlCanonical = (href) => {
2
+ if (!href) {
3
+ return null;
4
+ }
5
+ try {
6
+ const url = new URL(href, globalThis.location?.origin);
7
+ return `${url.origin}${url.pathname}`;
8
+ }
9
+ catch {
10
+ return null;
11
+ }
12
+ };
13
+ const readPrimaryHeadingCandidate = (documentRef) => {
14
+ const heading = documentRef.querySelector("h1, main h1, [role='heading'][aria-level='1']");
15
+ if (!(heading instanceof Element)) {
16
+ return null;
17
+ }
18
+ const text = heading.textContent?.replace(/\s+/g, " ").trim() ?? "";
19
+ return text.length > 0 ? text.slice(0, 120) : null;
20
+ };
21
+ export const getCurrentPageSnapshot = () => {
22
+ if (typeof window === "undefined" || typeof document === "undefined") {
23
+ return {
24
+ url: null,
25
+ path: null,
26
+ screenKey: null,
27
+ urlCanonical: null,
28
+ pageTitleCandidate: null,
29
+ primaryHeadingCandidate: null,
30
+ };
31
+ }
32
+ const url = window.location.href;
33
+ const path = window.location.pathname || "/";
34
+ return {
35
+ url,
36
+ path,
37
+ screenKey: path,
38
+ urlCanonical: buildUrlCanonical(url),
39
+ pageTitleCandidate: document.title || null,
40
+ primaryHeadingCandidate: readPrimaryHeadingCandidate(document),
41
+ };
42
+ };
@@ -0,0 +1,2 @@
1
+ import type { CaptureModuleContext, CaptureStopHandle } from "../core/types";
2
+ export declare const startNavigationCapture: (context: CaptureModuleContext) => CaptureStopHandle;
@@ -0,0 +1,100 @@
1
+ import { generateViewId } from "../normalize/event-id";
2
+ import { getCurrentPageSnapshot } from "./location";
3
+ export const startNavigationCapture = (context) => {
4
+ if (typeof window === "undefined") {
5
+ return { stop: () => undefined };
6
+ }
7
+ let currentSnapshot = getCurrentPageSnapshot();
8
+ let currentViewId = context.getViewId();
9
+ let viewEnteredAtMs = Date.now();
10
+ const emitRouteChange = () => {
11
+ if (context.getConsent() === "denied") {
12
+ return;
13
+ }
14
+ const nextSnapshot = getCurrentPageSnapshot();
15
+ if (nextSnapshot.path === currentSnapshot.path) {
16
+ return;
17
+ }
18
+ const now = Date.now();
19
+ const previousViewId = currentViewId;
20
+ const nextViewId = generateViewId();
21
+ context.onEvent({
22
+ type: "page_leave",
23
+ occurredAtMs: now,
24
+ payload: {
25
+ viewId: previousViewId,
26
+ screenKey: currentSnapshot.screenKey,
27
+ path: currentSnapshot.path,
28
+ timeOnPageMs: Math.max(0, now - viewEnteredAtMs),
29
+ },
30
+ });
31
+ context.onEvent({
32
+ type: "route_change",
33
+ occurredAtMs: now,
34
+ payload: {
35
+ fromViewId: previousViewId,
36
+ toViewId: nextViewId,
37
+ fromScreenKey: currentSnapshot.screenKey,
38
+ toScreenKey: nextSnapshot.screenKey,
39
+ fromPath: currentSnapshot.path,
40
+ toPath: nextSnapshot.path,
41
+ },
42
+ });
43
+ context.onEvent({
44
+ type: "page_view",
45
+ occurredAtMs: now,
46
+ payload: {
47
+ viewId: nextViewId,
48
+ screenKey: nextSnapshot.screenKey,
49
+ path: nextSnapshot.path,
50
+ urlCanonical: nextSnapshot.urlCanonical,
51
+ pageTitleCandidate: nextSnapshot.pageTitleCandidate,
52
+ routeNameCandidate: null,
53
+ primaryHeadingCandidate: nextSnapshot.primaryHeadingCandidate,
54
+ },
55
+ });
56
+ currentViewId = nextViewId;
57
+ currentSnapshot = nextSnapshot;
58
+ viewEnteredAtMs = now;
59
+ };
60
+ const originalPushState = history.pushState;
61
+ const originalReplaceState = history.replaceState;
62
+ const wrappedPushState = function (...args) {
63
+ const result = originalPushState.apply(this, args);
64
+ try {
65
+ emitRouteChange();
66
+ }
67
+ catch (error) {
68
+ context.onInternalError("capture/navigation:pushState", error);
69
+ }
70
+ return result;
71
+ };
72
+ const wrappedReplaceState = function (...args) {
73
+ const result = originalReplaceState.apply(this, args);
74
+ try {
75
+ emitRouteChange();
76
+ }
77
+ catch (error) {
78
+ context.onInternalError("capture/navigation:replaceState", error);
79
+ }
80
+ return result;
81
+ };
82
+ history.pushState = wrappedPushState;
83
+ history.replaceState = wrappedReplaceState;
84
+ const popstateHandler = () => {
85
+ try {
86
+ emitRouteChange();
87
+ }
88
+ catch (error) {
89
+ context.onInternalError("capture/navigation:popstate", error);
90
+ }
91
+ };
92
+ window.addEventListener("popstate", popstateHandler, true);
93
+ return {
94
+ stop: () => {
95
+ history.pushState = originalPushState;
96
+ history.replaceState = originalReplaceState;
97
+ window.removeEventListener("popstate", popstateHandler, true);
98
+ },
99
+ };
100
+ };
@@ -0,0 +1,13 @@
1
+ import type { CaptureModuleContext, CaptureStopHandle } from "../core/types";
2
+ export declare const startNetworkCapture: (context: CaptureModuleContext, options?: {
3
+ ignoreUrlPrefixes?: string[];
4
+ tracePropagationOrigins?: string[];
5
+ important4xxPathPrefixes?: string[];
6
+ createRequestTraceContext?: () => {
7
+ traceId: string;
8
+ parentSpanId: string | null;
9
+ requestId: string;
10
+ requestSpanId: string;
11
+ interactionId: string | null;
12
+ };
13
+ }) => CaptureStopHandle;