@genn-inc/cluebase-frontend-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.
- package/README.md +222 -0
- package/dist/_generated/correlation-attribute-keys.d.ts +2 -0
- package/dist/_generated/correlation-attribute-keys.js +33 -0
- package/dist/_generated/identity-pii-policy.d.ts +38 -0
- package/dist/_generated/identity-pii-policy.js +103 -0
- package/dist/_generated/ingest-privacy-tables.d.ts +120 -0
- package/dist/_generated/ingest-privacy-tables.js +890 -0
- package/dist/_generated/otel-resource-keys.d.ts +21 -0
- package/dist/_generated/otel-resource-keys.js +72 -0
- package/dist/_generated/otel-span-conversion.d.ts +48 -0
- package/dist/_generated/otel-span-conversion.js +69 -0
- package/dist/_generated/parameter-snapshot-leaf.d.ts +11 -0
- package/dist/_generated/parameter-snapshot-leaf.js +37 -0
- package/dist/_generated/sdk-runtime-policy.d.ts +7 -0
- package/dist/_generated/sdk-runtime-policy.js +14 -0
- package/dist/_generated/usage-governor-tables.d.ts +15 -0
- package/dist/_generated/usage-governor-tables.js +44 -0
- package/dist/context/browser-storage.d.ts +15 -0
- package/dist/context/browser-storage.js +38 -0
- package/dist/context/environment.d.ts +37 -0
- package/dist/context/environment.js +229 -0
- package/dist/context/identity.d.ts +16 -0
- package/dist/context/identity.js +139 -0
- package/dist/context/organization.d.ts +16 -0
- package/dist/context/organization.js +60 -0
- package/dist/context/session.d.ts +28 -0
- package/dist/context/session.js +130 -0
- package/dist/context/tab.d.ts +22 -0
- package/dist/context/tab.js +117 -0
- package/dist/context/trace.d.ts +76 -0
- package/dist/context/trace.js +163 -0
- package/dist/exporter/batch.d.ts +94 -0
- package/dist/exporter/batch.js +485 -0
- package/dist/exporter/browser-outbox-records.d.ts +159 -0
- package/dist/exporter/browser-outbox-records.js +341 -0
- package/dist/exporter/browser-outbox.d.ts +112 -0
- package/dist/exporter/browser-outbox.js +876 -0
- package/dist/exporter/client.d.ts +92 -0
- package/dist/exporter/client.js +541 -0
- package/dist/exporter/delivery-response-contract.d.ts +103 -0
- package/dist/exporter/delivery-response-contract.js +147 -0
- package/dist/exporter/delivery-response.d.ts +37 -0
- package/dist/exporter/delivery-response.js +151 -0
- package/dist/exporter/diagnostics.d.ts +42 -0
- package/dist/exporter/diagnostics.js +148 -0
- package/dist/exporter/event-id.d.ts +10 -0
- package/dist/exporter/event-id.js +75 -0
- package/dist/exporter/frontend-resource.d.ts +20 -0
- package/dist/exporter/frontend-resource.js +52 -0
- package/dist/exporter/local-minimization.d.ts +29 -0
- package/dist/exporter/local-minimization.js +87 -0
- package/dist/exporter/observation-source-event-exporter.d.ts +69 -0
- package/dist/exporter/observation-source-event-exporter.js +154 -0
- package/dist/exporter/parameter-snapshot.d.ts +14 -0
- package/dist/exporter/parameter-snapshot.js +215 -0
- package/dist/exporter/path-template.d.ts +1 -0
- package/dist/exporter/path-template.js +67 -0
- package/dist/exporter/raw-event-to-source-signal.d.ts +12 -0
- package/dist/exporter/raw-event-to-source-signal.js +297 -0
- package/dist/exporter/retry.d.ts +8 -0
- package/dist/exporter/retry.js +8 -0
- package/dist/exporter/sanitize.d.ts +30 -0
- package/dist/exporter/sanitize.js +177 -0
- package/dist/exporter/schema-evidence.d.ts +20 -0
- package/dist/exporter/schema-evidence.js +232 -0
- package/dist/exporter/source-event.d.ts +99 -0
- package/dist/exporter/source-event.js +13 -0
- package/dist/exporter/span-to-source-signal.d.ts +8 -0
- package/dist/exporter/span-to-source-signal.js +174 -0
- package/dist/exporter/text-fingerprint.d.ts +9 -0
- package/dist/exporter/text-fingerprint.js +16 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +158 -0
- package/dist/instrumentation/action.d.ts +2 -0
- package/dist/instrumentation/action.js +171 -0
- package/dist/instrumentation/cluebase-authored-attributes.d.ts +4 -0
- package/dist/instrumentation/cluebase-authored-attributes.js +32 -0
- package/dist/instrumentation/common.d.ts +73 -0
- package/dist/instrumentation/common.js +90 -0
- package/dist/instrumentation/dom.d.ts +63 -0
- package/dist/instrumentation/dom.js +640 -0
- package/dist/instrumentation/drag.d.ts +2 -0
- package/dist/instrumentation/drag.js +82 -0
- package/dist/instrumentation/error.d.ts +2 -0
- package/dist/instrumentation/error.js +318 -0
- package/dist/instrumentation/form.d.ts +2 -0
- package/dist/instrumentation/form.js +132 -0
- package/dist/instrumentation/input.d.ts +2 -0
- package/dist/instrumentation/input.js +180 -0
- package/dist/instrumentation/location.d.ts +11 -0
- package/dist/instrumentation/location.js +69 -0
- package/dist/instrumentation/naming-evidence.d.ts +20 -0
- package/dist/instrumentation/naming-evidence.js +11 -0
- package/dist/instrumentation/navigation.d.ts +2 -0
- package/dist/instrumentation/navigation.js +144 -0
- package/dist/instrumentation/network-request.d.ts +103 -0
- package/dist/instrumentation/network-request.js +285 -0
- package/dist/instrumentation/network-span-attributes.d.ts +25 -0
- package/dist/instrumentation/network-span-attributes.js +153 -0
- package/dist/instrumentation/network.d.ts +16 -0
- package/dist/instrumentation/network.js +502 -0
- package/dist/instrumentation/page.d.ts +2 -0
- package/dist/instrumentation/page.js +56 -0
- package/dist/instrumentation/performance.d.ts +2 -0
- package/dist/instrumentation/performance.js +365 -0
- package/dist/integrations/next-router.d.ts +16 -0
- package/dist/integrations/next-router.js +18 -0
- package/dist/integrations/react-router.d.ts +7 -0
- package/dist/integrations/react-router.js +61 -0
- package/dist/internal/internal-log.d.ts +23 -0
- package/dist/internal/internal-log.js +47 -0
- package/dist/internal/metrics.d.ts +10 -0
- package/dist/internal/metrics.js +50 -0
- package/dist/provider/_version.d.ts +1 -0
- package/dist/provider/_version.js +3 -0
- package/dist/provider/config.d.ts +12 -0
- package/dist/provider/config.js +177 -0
- package/dist/provider/constants.d.ts +62 -0
- package/dist/provider/constants.js +118 -0
- package/dist/provider/contracts.d.ts +51 -0
- package/dist/provider/contracts.js +46 -0
- package/dist/provider/privacy-policy-store.d.ts +20 -0
- package/dist/provider/privacy-policy-store.js +56 -0
- package/dist/provider/sampling.d.ts +2 -0
- package/dist/provider/sampling.js +17 -0
- package/dist/provider/sdk-event-pipeline.d.ts +60 -0
- package/dist/provider/sdk-event-pipeline.js +478 -0
- package/dist/provider/sdk-input-normalization.d.ts +7 -0
- package/dist/provider/sdk-input-normalization.js +113 -0
- package/dist/provider/sdk-otel-pipeline.d.ts +14 -0
- package/dist/provider/sdk-otel-pipeline.js +148 -0
- package/dist/provider/sdk-runtime-surfaces.d.ts +49 -0
- package/dist/provider/sdk-runtime-surfaces.js +216 -0
- package/dist/provider/sdk.d.ts +2 -0
- package/dist/provider/sdk.js +614 -0
- package/dist/provider/types.d.ts +323 -0
- package/dist/provider/types.js +1 -0
- package/dist/provider/usage-governor.d.ts +7 -0
- package/dist/provider/usage-governor.js +116 -0
- package/package.json +59 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { buildLocalValueDescriptor } from "../exporter/local-minimization.js";
|
|
2
|
+
import { extractFieldName, extractFieldType, getElementAriaLabel, getElementLabel, getElementPlaceholder, getElementRole, getElementSectionHeading, resolveInteractionScopeDescriptor, resolveStableKeyDescriptor, } from "./dom.js";
|
|
3
|
+
import { buildScreenActionNamingEvidence } from "./naming-evidence.js";
|
|
4
|
+
const readDragPayload = (event) => {
|
|
5
|
+
const dataTransfer = event.dataTransfer;
|
|
6
|
+
if (!dataTransfer) {
|
|
7
|
+
return {
|
|
8
|
+
itemCount: 0,
|
|
9
|
+
fileCount: 0,
|
|
10
|
+
dragTypes: [],
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
const itemCount = dataTransfer.items?.length ?? 0;
|
|
14
|
+
const fileCount = dataTransfer.files?.length ?? 0;
|
|
15
|
+
const dragTypes = Array.from(dataTransfer.types ?? []).filter(Boolean);
|
|
16
|
+
return {
|
|
17
|
+
itemCount,
|
|
18
|
+
fileCount,
|
|
19
|
+
dragTypes,
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
export const startDragCapture = (context) => {
|
|
23
|
+
if (typeof document === "undefined") {
|
|
24
|
+
return { stop: () => undefined };
|
|
25
|
+
}
|
|
26
|
+
// Drop payload summaries are retained as Cluebase-only UX evidence; accepted
|
|
27
|
+
// browser OTel instrumentation does not expose this committed drop signal.
|
|
28
|
+
const dropHandler = (event) => {
|
|
29
|
+
if (context.getConsent() === "denied") {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const target = event.target;
|
|
33
|
+
if (!(target instanceof Element)) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (context.shouldIgnoreElement?.(target)) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
const stableKey = resolveStableKeyDescriptor(target);
|
|
41
|
+
const interactionScope = resolveInteractionScopeDescriptor(target);
|
|
42
|
+
const dragPayload = readDragPayload(event);
|
|
43
|
+
context.onEvent({
|
|
44
|
+
type: "drag_drop_completed",
|
|
45
|
+
occurredAtMs: Date.now(),
|
|
46
|
+
payload: {
|
|
47
|
+
viewId: context.getViewId(),
|
|
48
|
+
elementTag: target.tagName.toLowerCase(),
|
|
49
|
+
elementRole: getElementRole(target),
|
|
50
|
+
stableKey: stableKey.stableKey,
|
|
51
|
+
stableKeyQuality: stableKey.stableKeyQuality,
|
|
52
|
+
formKey: stableKey.formKey,
|
|
53
|
+
interactionScopeKind: interactionScope?.interactionScopeKind ?? null,
|
|
54
|
+
interactionScopeKey: interactionScope?.interactionScopeKey ?? null,
|
|
55
|
+
fieldNameMeta: buildLocalValueDescriptor(extractFieldName(target)),
|
|
56
|
+
elementLabelMeta: buildLocalValueDescriptor(getElementLabel(target)),
|
|
57
|
+
ariaLabelMeta: buildLocalValueDescriptor(getElementAriaLabel(target)),
|
|
58
|
+
placeholderMeta: buildLocalValueDescriptor(getElementPlaceholder(target)),
|
|
59
|
+
fieldName: extractFieldName(target),
|
|
60
|
+
fieldType: extractFieldType(target),
|
|
61
|
+
fieldDisplayLabel: getElementLabel(target),
|
|
62
|
+
fieldAriaLabel: getElementAriaLabel(target),
|
|
63
|
+
fieldPlaceholder: getElementPlaceholder(target),
|
|
64
|
+
sectionHeading: getElementSectionHeading(target),
|
|
65
|
+
...buildScreenActionNamingEvidence(target),
|
|
66
|
+
dragItemCount: dragPayload.itemCount,
|
|
67
|
+
fileCount: dragPayload.fileCount,
|
|
68
|
+
dragTypes: dragPayload.dragTypes,
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
context.onInternalError("capture/drag:drop", error);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
document.addEventListener("drop", dropHandler, true);
|
|
77
|
+
return {
|
|
78
|
+
stop: () => {
|
|
79
|
+
document.removeEventListener("drop", dropHandler, true);
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
};
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import { ErrorsInstrumentation } from "@opentelemetry/browser-instrumentation/experimental/errors";
|
|
2
|
+
const ERROR_BURST_DEBOUNCE_MS = 250;
|
|
3
|
+
const EXCEPTION_EVENT_NAME = "exception";
|
|
4
|
+
const ATTR_EXCEPTION_TYPE = "exception.type";
|
|
5
|
+
const ATTR_EXCEPTION_MESSAGE = "exception.message";
|
|
6
|
+
const ATTR_EXCEPTION_STACKTRACE = "exception.stacktrace";
|
|
7
|
+
const ATTR_CLUEBASE_ERROR_EVENT_TYPE = "cluebase.error.event_type";
|
|
8
|
+
const ATTR_CLUEBASE_ERROR_FINGERPRINT = "cluebase.error.fingerprint";
|
|
9
|
+
const ATTR_CLUEBASE_ERROR_SOURCE_FILE = "cluebase.error.source_file";
|
|
10
|
+
const ATTR_CLUEBASE_ERROR_SOURCE_FILE_HINT = "cluebase.error.source_file_hint";
|
|
11
|
+
const ATTR_CLUEBASE_ERROR_LINE_NUMBER = "cluebase.error.line_number";
|
|
12
|
+
const ATTR_CLUEBASE_ERROR_COLUMN_NUMBER = "cluebase.error.column_number";
|
|
13
|
+
const hashString = (value) => {
|
|
14
|
+
let hash = 2166136261;
|
|
15
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
16
|
+
hash ^= value.charCodeAt(index);
|
|
17
|
+
hash = Math.imul(hash, 16777619);
|
|
18
|
+
}
|
|
19
|
+
return `err_${(hash >>> 0).toString(16).padStart(8, "0")}`;
|
|
20
|
+
};
|
|
21
|
+
const sourceFileHintFromValue = (value) => {
|
|
22
|
+
if (!value) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
const resolved = new URL(value, globalThis.location?.origin);
|
|
27
|
+
const segments = resolved.pathname.split("/").filter(Boolean);
|
|
28
|
+
return segments[segments.length - 1] ?? null;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
const normalized = value.split(/[?#]/, 1)[0] ?? value;
|
|
32
|
+
const segments = normalized.split("/").filter(Boolean);
|
|
33
|
+
return segments[segments.length - 1] ?? (normalized || null);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
const firstStackLine = (stack) => stack
|
|
37
|
+
?.split("\n")
|
|
38
|
+
.map((line) => line.trim())
|
|
39
|
+
.find(Boolean) ?? "";
|
|
40
|
+
const buildFingerprint = (input) => {
|
|
41
|
+
return hashString([
|
|
42
|
+
input.type,
|
|
43
|
+
input.errorType ?? "",
|
|
44
|
+
input.message ?? "",
|
|
45
|
+
sourceFileHintFromValue(input.sourceFile) ?? "",
|
|
46
|
+
input.lineNumber ?? "",
|
|
47
|
+
input.columnNumber ?? "",
|
|
48
|
+
firstStackLine(input.stack),
|
|
49
|
+
].join("|"));
|
|
50
|
+
};
|
|
51
|
+
const readStringAttribute = (logRecord, key) => {
|
|
52
|
+
const value = logRecord.attributes?.[key];
|
|
53
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
54
|
+
};
|
|
55
|
+
const readNumberAttribute = (logRecord, key) => {
|
|
56
|
+
const value = logRecord.attributes?.[key];
|
|
57
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
58
|
+
};
|
|
59
|
+
export const startErrorCapture = (context) => {
|
|
60
|
+
if (typeof window === "undefined") {
|
|
61
|
+
return { stop: () => undefined };
|
|
62
|
+
}
|
|
63
|
+
const pendingByFingerprint = new Map();
|
|
64
|
+
const errorMetadata = new WeakMap();
|
|
65
|
+
const flushFingerprint = (fingerprint) => {
|
|
66
|
+
const bucket = pendingByFingerprint.get(fingerprint);
|
|
67
|
+
if (!bucket) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (bucket.timer) {
|
|
71
|
+
clearTimeout(bucket.timer);
|
|
72
|
+
}
|
|
73
|
+
context.onEvent({
|
|
74
|
+
type: bucket.event.type,
|
|
75
|
+
occurredAtMs: bucket.lastOccurredAtMs,
|
|
76
|
+
payload: {
|
|
77
|
+
...bucket.event.payload,
|
|
78
|
+
repeatCount: bucket.repeatCount,
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
pendingByFingerprint.delete(fingerprint);
|
|
82
|
+
};
|
|
83
|
+
const scheduleFlush = (fingerprint) => {
|
|
84
|
+
const bucket = pendingByFingerprint.get(fingerprint);
|
|
85
|
+
if (!bucket) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (bucket.timer) {
|
|
89
|
+
clearTimeout(bucket.timer);
|
|
90
|
+
}
|
|
91
|
+
bucket.timer = setTimeout(() => {
|
|
92
|
+
flushFingerprint(fingerprint);
|
|
93
|
+
}, ERROR_BURST_DEBOUNCE_MS);
|
|
94
|
+
};
|
|
95
|
+
const queueErrorEnvelope = (type, occurredAtMs, payload) => {
|
|
96
|
+
const fingerprint = payload.errorFingerprint;
|
|
97
|
+
if (!fingerprint) {
|
|
98
|
+
context.onEvent({
|
|
99
|
+
type,
|
|
100
|
+
occurredAtMs,
|
|
101
|
+
payload: {
|
|
102
|
+
...payload,
|
|
103
|
+
repeatCount: 1,
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const existing = pendingByFingerprint.get(fingerprint);
|
|
109
|
+
if (existing) {
|
|
110
|
+
existing.repeatCount += 1;
|
|
111
|
+
existing.lastOccurredAtMs = occurredAtMs;
|
|
112
|
+
existing.event.payload = {
|
|
113
|
+
...existing.event.payload,
|
|
114
|
+
...payload,
|
|
115
|
+
};
|
|
116
|
+
scheduleFlush(fingerprint);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
pendingByFingerprint.set(fingerprint, {
|
|
120
|
+
event: {
|
|
121
|
+
type,
|
|
122
|
+
occurredAtMs,
|
|
123
|
+
payload,
|
|
124
|
+
},
|
|
125
|
+
repeatCount: 1,
|
|
126
|
+
lastOccurredAtMs: occurredAtMs,
|
|
127
|
+
timer: null,
|
|
128
|
+
});
|
|
129
|
+
scheduleFlush(fingerprint);
|
|
130
|
+
};
|
|
131
|
+
const rememberErrorEventMetadata = (event) => {
|
|
132
|
+
if (event.error instanceof Error) {
|
|
133
|
+
errorMetadata.set(event.error, {
|
|
134
|
+
sourceFile: event.filename || null,
|
|
135
|
+
lineNumber: event.lineno || undefined,
|
|
136
|
+
columnNumber: event.colno || undefined,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
const buildCluebaseErrorAttributes = (error) => {
|
|
141
|
+
if (context.getConsent() === "denied") {
|
|
142
|
+
return {};
|
|
143
|
+
}
|
|
144
|
+
const metadata = error instanceof Error ? errorMetadata.get(error) : undefined;
|
|
145
|
+
const type = metadata ? "frontend_error" : "unhandled_rejection";
|
|
146
|
+
const errorType = error instanceof Error ? error.name : "PromiseRejection";
|
|
147
|
+
const message = error instanceof Error ? error.message : error;
|
|
148
|
+
const stack = error instanceof Error ? (error.stack ?? null) : null;
|
|
149
|
+
const sourceFile = metadata?.sourceFile ?? null;
|
|
150
|
+
const fingerprint = buildFingerprint({
|
|
151
|
+
type,
|
|
152
|
+
errorType,
|
|
153
|
+
message,
|
|
154
|
+
sourceFile,
|
|
155
|
+
lineNumber: metadata?.lineNumber,
|
|
156
|
+
columnNumber: metadata?.columnNumber,
|
|
157
|
+
stack,
|
|
158
|
+
});
|
|
159
|
+
const attributes = {
|
|
160
|
+
[ATTR_CLUEBASE_ERROR_EVENT_TYPE]: type,
|
|
161
|
+
[ATTR_CLUEBASE_ERROR_FINGERPRINT]: fingerprint,
|
|
162
|
+
};
|
|
163
|
+
if (!(error instanceof Error)) {
|
|
164
|
+
attributes[ATTR_EXCEPTION_TYPE] = errorType;
|
|
165
|
+
attributes[ATTR_EXCEPTION_MESSAGE] = message;
|
|
166
|
+
}
|
|
167
|
+
const sourceFileHint = sourceFileHintFromValue(sourceFile);
|
|
168
|
+
if (sourceFile) {
|
|
169
|
+
attributes[ATTR_CLUEBASE_ERROR_SOURCE_FILE] = sourceFile;
|
|
170
|
+
}
|
|
171
|
+
if (sourceFileHint) {
|
|
172
|
+
attributes[ATTR_CLUEBASE_ERROR_SOURCE_FILE_HINT] = sourceFileHint;
|
|
173
|
+
}
|
|
174
|
+
if (metadata?.lineNumber !== undefined) {
|
|
175
|
+
attributes[ATTR_CLUEBASE_ERROR_LINE_NUMBER] = metadata.lineNumber;
|
|
176
|
+
}
|
|
177
|
+
if (metadata?.columnNumber !== undefined) {
|
|
178
|
+
attributes[ATTR_CLUEBASE_ERROR_COLUMN_NUMBER] = metadata.columnNumber;
|
|
179
|
+
}
|
|
180
|
+
return attributes;
|
|
181
|
+
};
|
|
182
|
+
const emitFromOtelLogRecord = (logRecord) => {
|
|
183
|
+
if (logRecord.eventName !== EXCEPTION_EVENT_NAME ||
|
|
184
|
+
context.getConsent() === "denied") {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
const type = readStringAttribute(logRecord, ATTR_CLUEBASE_ERROR_EVENT_TYPE) ??
|
|
188
|
+
"unhandled_rejection";
|
|
189
|
+
if (type !== "frontend_error" && type !== "unhandled_rejection") {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
const errorType = readStringAttribute(logRecord, ATTR_EXCEPTION_TYPE) ?? "Error";
|
|
193
|
+
const message = readStringAttribute(logRecord, ATTR_EXCEPTION_MESSAGE);
|
|
194
|
+
const stack = readStringAttribute(logRecord, ATTR_EXCEPTION_STACKTRACE);
|
|
195
|
+
const sourceFile = readStringAttribute(logRecord, ATTR_CLUEBASE_ERROR_SOURCE_FILE);
|
|
196
|
+
const sourceFileHint = readStringAttribute(logRecord, ATTR_CLUEBASE_ERROR_SOURCE_FILE_HINT) ??
|
|
197
|
+
sourceFileHintFromValue(sourceFile);
|
|
198
|
+
const lineNumber = readNumberAttribute(logRecord, ATTR_CLUEBASE_ERROR_LINE_NUMBER);
|
|
199
|
+
const columnNumber = readNumberAttribute(logRecord, ATTR_CLUEBASE_ERROR_COLUMN_NUMBER);
|
|
200
|
+
const fingerprint = readStringAttribute(logRecord, ATTR_CLUEBASE_ERROR_FINGERPRINT) ??
|
|
201
|
+
buildFingerprint({
|
|
202
|
+
type,
|
|
203
|
+
errorType,
|
|
204
|
+
message,
|
|
205
|
+
sourceFile,
|
|
206
|
+
lineNumber,
|
|
207
|
+
columnNumber,
|
|
208
|
+
stack,
|
|
209
|
+
});
|
|
210
|
+
const occurredAtMs = Date.now();
|
|
211
|
+
queueErrorEnvelope(type, occurredAtMs, {
|
|
212
|
+
viewId: context.getViewId(),
|
|
213
|
+
errorType,
|
|
214
|
+
errorFingerprint: fingerprint,
|
|
215
|
+
sourceFileHint,
|
|
216
|
+
message,
|
|
217
|
+
stack,
|
|
218
|
+
sourceFile,
|
|
219
|
+
lineNumber,
|
|
220
|
+
columnNumber,
|
|
221
|
+
});
|
|
222
|
+
};
|
|
223
|
+
const flushAllPending = () => {
|
|
224
|
+
const fingerprints = Array.from(pendingByFingerprint.keys());
|
|
225
|
+
for (const fingerprint of fingerprints) {
|
|
226
|
+
try {
|
|
227
|
+
flushFingerprint(fingerprint);
|
|
228
|
+
}
|
|
229
|
+
catch (error) {
|
|
230
|
+
context.onInternalError("capture/error:flush", error);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
const pagehideHandler = () => {
|
|
235
|
+
flushAllPending();
|
|
236
|
+
};
|
|
237
|
+
const visibilityHandler = () => {
|
|
238
|
+
if (document.visibilityState === "hidden") {
|
|
239
|
+
flushAllPending();
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
const fallbackErrorHandler = (event) => {
|
|
243
|
+
if (context.getConsent() === "denied" ||
|
|
244
|
+
event.error instanceof Error ||
|
|
245
|
+
event.error != null) {
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
if (event.target instanceof Element && !event.message) {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
const message = event.message || null;
|
|
252
|
+
const sourceFile = event.filename || null;
|
|
253
|
+
const lineNumber = event.lineno || undefined;
|
|
254
|
+
const columnNumber = event.colno || undefined;
|
|
255
|
+
const errorType = "ErrorEvent";
|
|
256
|
+
const fingerprint = buildFingerprint({
|
|
257
|
+
type: "frontend_error",
|
|
258
|
+
errorType,
|
|
259
|
+
message,
|
|
260
|
+
sourceFile,
|
|
261
|
+
lineNumber,
|
|
262
|
+
columnNumber,
|
|
263
|
+
stack: null,
|
|
264
|
+
});
|
|
265
|
+
queueErrorEnvelope("frontend_error", Date.now(), {
|
|
266
|
+
viewId: context.getViewId(),
|
|
267
|
+
errorType,
|
|
268
|
+
errorFingerprint: fingerprint,
|
|
269
|
+
sourceFileHint: sourceFileHintFromValue(sourceFile),
|
|
270
|
+
message,
|
|
271
|
+
stack: null,
|
|
272
|
+
sourceFile,
|
|
273
|
+
lineNumber,
|
|
274
|
+
columnNumber,
|
|
275
|
+
});
|
|
276
|
+
};
|
|
277
|
+
window.addEventListener("error", rememberErrorEventMetadata, true);
|
|
278
|
+
window.addEventListener("error", fallbackErrorHandler, true);
|
|
279
|
+
window.addEventListener("pagehide", pagehideHandler, true);
|
|
280
|
+
document.addEventListener("visibilitychange", visibilityHandler, true);
|
|
281
|
+
const instrumentation = new ErrorsInstrumentation({
|
|
282
|
+
applyCustomAttributes: (error) => {
|
|
283
|
+
try {
|
|
284
|
+
return buildCluebaseErrorAttributes(error);
|
|
285
|
+
}
|
|
286
|
+
catch (cause) {
|
|
287
|
+
context.onInternalError("capture/error:otel", cause);
|
|
288
|
+
return {};
|
|
289
|
+
}
|
|
290
|
+
},
|
|
291
|
+
});
|
|
292
|
+
const logger = {
|
|
293
|
+
enabled: () => true,
|
|
294
|
+
emit: (logRecord) => {
|
|
295
|
+
try {
|
|
296
|
+
emitFromOtelLogRecord(logRecord);
|
|
297
|
+
}
|
|
298
|
+
catch (cause) {
|
|
299
|
+
context.onInternalError("capture/error:otel", cause);
|
|
300
|
+
}
|
|
301
|
+
},
|
|
302
|
+
};
|
|
303
|
+
const loggerProvider = {
|
|
304
|
+
getLogger: () => logger,
|
|
305
|
+
};
|
|
306
|
+
instrumentation.setLoggerProvider(loggerProvider);
|
|
307
|
+
instrumentation.enable();
|
|
308
|
+
return {
|
|
309
|
+
stop: () => {
|
|
310
|
+
flushAllPending();
|
|
311
|
+
instrumentation.disable();
|
|
312
|
+
window.removeEventListener("error", rememberErrorEventMetadata, true);
|
|
313
|
+
window.removeEventListener("error", fallbackErrorHandler, true);
|
|
314
|
+
window.removeEventListener("pagehide", pagehideHandler, true);
|
|
315
|
+
document.removeEventListener("visibilitychange", visibilityHandler, true);
|
|
316
|
+
},
|
|
317
|
+
};
|
|
318
|
+
};
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { buildLocalTextDescriptor } from "../exporter/local-minimization.js";
|
|
2
|
+
import { extractFieldName, extractFieldType, getElementAriaLabel, getElementClassList, getElementLabel, getElementPlaceholder, getElementRole, getElementSectionHeading, isEditableElement, resolveClickDisplayTarget, resolveInteractionScopeDescriptor, resolveStableKeyDescriptor, } from "./dom.js";
|
|
3
|
+
import { buildPrimaryActionKey } from "./common.js";
|
|
4
|
+
import { getCurrentPageSnapshot } from "./location.js";
|
|
5
|
+
import { buildScreenActionNamingEvidence } from "./naming-evidence.js";
|
|
6
|
+
import { fingerprintText } from "../exporter/text-fingerprint.js";
|
|
7
|
+
const VALIDATION_REASON_KEYS = [
|
|
8
|
+
"valueMissing",
|
|
9
|
+
"typeMismatch",
|
|
10
|
+
"patternMismatch",
|
|
11
|
+
"tooLong",
|
|
12
|
+
"tooShort",
|
|
13
|
+
"rangeUnderflow",
|
|
14
|
+
"rangeOverflow",
|
|
15
|
+
"stepMismatch",
|
|
16
|
+
"badInput",
|
|
17
|
+
"customError",
|
|
18
|
+
];
|
|
19
|
+
const toSnakeCase = (value) => value.replace(/[A-Z]/g, (char) => `_${char.toLowerCase()}`);
|
|
20
|
+
const isFormControlElement = (element) => element instanceof HTMLInputElement ||
|
|
21
|
+
element instanceof HTMLSelectElement ||
|
|
22
|
+
element instanceof HTMLTextAreaElement;
|
|
23
|
+
const buildValidationEvidence = (form) => {
|
|
24
|
+
const invalidControls = Array.from(form.elements)
|
|
25
|
+
.filter((element) => {
|
|
26
|
+
return isFormControlElement(element) && !element.validity.valid;
|
|
27
|
+
})
|
|
28
|
+
.slice(0, 25);
|
|
29
|
+
const invalidFieldStableKeys = invalidControls
|
|
30
|
+
.map((element) => resolveStableKeyDescriptor(element)?.stableKey ?? null)
|
|
31
|
+
.filter((value) => Boolean(value));
|
|
32
|
+
const validationReasonCodes = [
|
|
33
|
+
...new Set(invalidControls.flatMap((element) => VALIDATION_REASON_KEYS.filter((key) => element.validity[key]).map((key) => toSnakeCase(key)))),
|
|
34
|
+
].sort();
|
|
35
|
+
const validationMessages = invalidControls
|
|
36
|
+
.map((element) => element.validationMessage.trim())
|
|
37
|
+
.filter(Boolean)
|
|
38
|
+
.sort();
|
|
39
|
+
return {
|
|
40
|
+
invalidFieldCount: invalidControls.length,
|
|
41
|
+
invalidFieldStableKeys,
|
|
42
|
+
validationReasonCodes,
|
|
43
|
+
validationMessageFingerprint: validationMessages.length > 0
|
|
44
|
+
? fingerprintText(validationMessages.join("|"))
|
|
45
|
+
: null,
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
export const startFormCapture = (context) => {
|
|
49
|
+
if (typeof document === "undefined") {
|
|
50
|
+
return { stop: () => undefined };
|
|
51
|
+
}
|
|
52
|
+
// OTel browser user-action covers the submitter click; this retains
|
|
53
|
+
// Cluebase-only form validation outcome evidence that OTel does not provide.
|
|
54
|
+
const submitHandler = (event) => {
|
|
55
|
+
if (context.getConsent() === "denied") {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const target = event.target;
|
|
59
|
+
if (!(target instanceof HTMLFormElement)) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (context.shouldIgnoreElement?.(target)) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
const formKey = target.id || target.getAttribute("name") || null;
|
|
67
|
+
const interactionScope = resolveInteractionScopeDescriptor(target);
|
|
68
|
+
const rawSubmitter = event.submitter instanceof Element ? event.submitter : null;
|
|
69
|
+
const submitter = rawSubmitter
|
|
70
|
+
? resolveClickDisplayTarget(rawSubmitter)
|
|
71
|
+
: null;
|
|
72
|
+
const submitterStableKey = submitter
|
|
73
|
+
? resolveStableKeyDescriptor(submitter)
|
|
74
|
+
: null;
|
|
75
|
+
const validationEvidence = buildValidationEvidence(target);
|
|
76
|
+
const submitResult = validationEvidence.invalidFieldCount > 0
|
|
77
|
+
? "validation_blocked"
|
|
78
|
+
: "unknown";
|
|
79
|
+
const resultSource = validationEvidence.invalidFieldCount > 0 ? "browser_validation" : null;
|
|
80
|
+
context.onEvent({
|
|
81
|
+
type: "form_submitted",
|
|
82
|
+
occurredAtMs: Date.now(),
|
|
83
|
+
payload: {
|
|
84
|
+
viewId: context.getViewId(),
|
|
85
|
+
formId: target.id || null,
|
|
86
|
+
formKey,
|
|
87
|
+
primaryActionKey: buildPrimaryActionKey(getCurrentPageSnapshot().screenKey, formKey ?? submitterStableKey?.stableKey ?? null),
|
|
88
|
+
formName: target.getAttribute("name"),
|
|
89
|
+
interactionScopeKind: interactionScope?.interactionScopeKind ?? null,
|
|
90
|
+
interactionScopeKey: interactionScope?.interactionScopeKey ?? null,
|
|
91
|
+
submitResult,
|
|
92
|
+
resultSource,
|
|
93
|
+
invalidFieldCount: validationEvidence.invalidFieldCount,
|
|
94
|
+
invalidFieldStableKeys: validationEvidence.invalidFieldStableKeys,
|
|
95
|
+
validationReasonCodes: validationEvidence.validationReasonCodes,
|
|
96
|
+
validationMessageFingerprint: validationEvidence.validationMessageFingerprint,
|
|
97
|
+
elementTag: submitter?.tagName.toLowerCase() ?? null,
|
|
98
|
+
elementRole: submitter ? getElementRole(submitter) : null,
|
|
99
|
+
stableKey: submitterStableKey?.stableKey ?? null,
|
|
100
|
+
stableKeyQuality: submitterStableKey?.stableKeyQuality ?? null,
|
|
101
|
+
elementId: submitter?.id || null,
|
|
102
|
+
elementClass: submitter ? getElementClassList(submitter) : null,
|
|
103
|
+
elementLabelMeta: buildLocalTextDescriptor(submitter ? getElementLabel(submitter) : null),
|
|
104
|
+
ariaLabelMeta: buildLocalTextDescriptor(submitter ? getElementAriaLabel(submitter) : null),
|
|
105
|
+
placeholderMeta: buildLocalTextDescriptor(submitter ? getElementPlaceholder(submitter) : null),
|
|
106
|
+
clickTargetText: submitter ? getElementLabel(submitter) : null,
|
|
107
|
+
clickTargetAriaLabel: submitter
|
|
108
|
+
? getElementAriaLabel(submitter)
|
|
109
|
+
: null,
|
|
110
|
+
clickTargetTitle: submitter?.getAttribute("title") || null,
|
|
111
|
+
fieldName: submitter ? extractFieldName(submitter) : null,
|
|
112
|
+
fieldType: submitter ? extractFieldType(submitter) : null,
|
|
113
|
+
fieldDisplayLabel: submitter ? getElementLabel(submitter) : null,
|
|
114
|
+
fieldAriaLabel: submitter ? getElementAriaLabel(submitter) : null,
|
|
115
|
+
fieldPlaceholder: submitter ? getElementPlaceholder(submitter) : null,
|
|
116
|
+
isEditable: submitter ? isEditableElement(submitter) : false,
|
|
117
|
+
sectionHeading: getElementSectionHeading(target),
|
|
118
|
+
...buildScreenActionNamingEvidence(submitter ?? target),
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
context.onInternalError("capture/form:submit", error);
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
document.addEventListener("submit", submitHandler, true);
|
|
127
|
+
return {
|
|
128
|
+
stop: () => {
|
|
129
|
+
document.removeEventListener("submit", submitHandler, true);
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
};
|