nuxt-error-tracker 0.1.10 → 0.1.12
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 +10 -2
- package/dist/module.d.mts +6 -0
- package/dist/module.d.ts +6 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +2 -0
- package/dist/runtime/plugin.client.js +72 -28
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -114,6 +114,7 @@ export default defineNuxtConfig({
|
|
|
114
114
|
| `enabled` | `boolean` | `true` | Master switch. `false` = fully disabled. |
|
|
115
115
|
| `captureFetch` | `boolean` | `true` | Wrap `fetch` to breadcrumb requests and report failures/5xx. |
|
|
116
116
|
| `captureConsole` | `boolean` | `true` | Patch `console.error` to capture logged-but-not-thrown errors. |
|
|
117
|
+
| `captureInputs` | `boolean` | `false` | Record form input values as breadcrumbs (on change). Sensitive fields masked. Opt-in — captures user-entered data. |
|
|
117
118
|
| `flushDelay` | `number` | `2000` | Debounce (ms) before a batch is sent. |
|
|
118
119
|
| `maxPerSession` | `number` | `5` | Max occurrences of the *same* error captured per session. |
|
|
119
120
|
| `maxBatch` | `number` | `20` | Max errors per request (server also hard-caps at 20). |
|
|
@@ -126,11 +127,12 @@ The plugin injects two helpers for manual use:
|
|
|
126
127
|
```ts
|
|
127
128
|
const { $trackError, $addBreadcrumb } = useNuxtApp()
|
|
128
129
|
|
|
129
|
-
// Report a handled error yourself
|
|
130
|
+
// Report a handled error yourself — optional severity level
|
|
131
|
+
// ('fatal' | 'error' | 'warning' | 'info' | 'debug', default 'error')
|
|
130
132
|
try {
|
|
131
133
|
await mayFail()
|
|
132
134
|
} catch (e) {
|
|
133
|
-
$trackError(e)
|
|
135
|
+
$trackError(e, 'warning')
|
|
134
136
|
}
|
|
135
137
|
|
|
136
138
|
// Add your own breadcrumb to the trail
|
|
@@ -165,6 +167,12 @@ Click breadcrumbs record the element (tag / id / class / short text) — **never
|
|
|
165
167
|
input values**. Nothing is captured until an error occurs; breadcrumbs live only
|
|
166
168
|
in memory and ship attached to an error.
|
|
167
169
|
|
|
170
|
+
Input values are **only** captured when you opt in with `captureInputs: true`.
|
|
171
|
+
Even then, sensitive fields are masked (stored as `[masked, N chars]`, never in
|
|
172
|
+
the clear): any `type="password"`, plus fields whose `name` / `id` /
|
|
173
|
+
`autocomplete` match `pass`, `secret`, `token`, `otp`, `cvv`, `card`, `iban`,
|
|
174
|
+
`ssn`, `pin`, `auth`, etc. Non-sensitive values are truncated to 100 chars.
|
|
175
|
+
|
|
168
176
|
## How it sends
|
|
169
177
|
|
|
170
178
|
Payloads are encoded as **protobuf** and POSTed as `application/x-protobuf`. On
|
package/dist/module.d.mts
CHANGED
|
@@ -7,6 +7,12 @@ interface ModuleOptions {
|
|
|
7
7
|
captureFetch?: boolean;
|
|
8
8
|
/** Patch console.error to capture logged-but-not-thrown errors. Default true. */
|
|
9
9
|
captureConsole?: boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Record form input values as breadcrumbs (on change/blur). Sensitive fields
|
|
12
|
+
* (password/email/tel + name/id matching card/cvv/token/secret/otp/…) are
|
|
13
|
+
* masked. Default false — opt in, since it can capture user-entered data.
|
|
14
|
+
*/
|
|
15
|
+
captureInputs?: boolean;
|
|
10
16
|
/** Debounce before a batch is flushed, in ms. Default 2000. */
|
|
11
17
|
flushDelay?: number;
|
|
12
18
|
/** Max occurrences of the same error captured per session. Default 5. */
|
package/dist/module.d.ts
CHANGED
|
@@ -7,6 +7,12 @@ interface ModuleOptions {
|
|
|
7
7
|
captureFetch?: boolean;
|
|
8
8
|
/** Patch console.error to capture logged-but-not-thrown errors. Default true. */
|
|
9
9
|
captureConsole?: boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Record form input values as breadcrumbs (on change/blur). Sensitive fields
|
|
12
|
+
* (password/email/tel + name/id matching card/cvv/token/secret/otp/…) are
|
|
13
|
+
* masked. Default false — opt in, since it can capture user-entered data.
|
|
14
|
+
*/
|
|
15
|
+
captureInputs?: boolean;
|
|
10
16
|
/** Debounce before a batch is flushed, in ms. Default 2000. */
|
|
11
17
|
flushDelay?: number;
|
|
12
18
|
/** Max occurrences of the same error captured per session. Default 5. */
|
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -16,6 +16,7 @@ const module = defineNuxtModule({
|
|
|
16
16
|
enabled: true,
|
|
17
17
|
captureFetch: true,
|
|
18
18
|
captureConsole: true,
|
|
19
|
+
captureInputs: false,
|
|
19
20
|
flushDelay: 2e3,
|
|
20
21
|
maxPerSession: 5,
|
|
21
22
|
maxBatch: 20,
|
|
@@ -30,6 +31,7 @@ const module = defineNuxtModule({
|
|
|
30
31
|
appVersion: options.appVersion ?? "unknown",
|
|
31
32
|
captureFetch: options.captureFetch !== false,
|
|
32
33
|
captureConsole: options.captureConsole !== false,
|
|
34
|
+
captureInputs: options.captureInputs === true,
|
|
33
35
|
flushDelay: posInt(options.flushDelay, 2e3),
|
|
34
36
|
maxPerSession: posInt(options.maxPerSession, 5),
|
|
35
37
|
// Server hard-caps batches at 20 — never emit larger, even if misconfigured.
|
|
@@ -40,37 +40,41 @@ export default defineNuxtPlugin((nuxtApp) => {
|
|
|
40
40
|
function sessionKey(message, stack) {
|
|
41
41
|
return `${message.slice(0, 120)}::${(stack.split("\n")[1] ?? "").trim()}`;
|
|
42
42
|
}
|
|
43
|
+
const MAX_QUEUE = 200;
|
|
43
44
|
function flush(useBeacon = false) {
|
|
44
45
|
if (!queue.length) return;
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
46
|
+
try {
|
|
47
|
+
const batch = queue.splice(0, MAX_BATCH);
|
|
48
|
+
const body = {
|
|
49
|
+
meta: {
|
|
50
|
+
device: deviceInfo(),
|
|
51
|
+
userId: resolveUserId(),
|
|
52
|
+
appVersion: config.appVersion
|
|
53
|
+
},
|
|
54
|
+
errors: batch
|
|
55
|
+
};
|
|
56
|
+
const bytes = encodeBatch(body);
|
|
57
|
+
if (useBeacon && navigator.sendBeacon) {
|
|
58
|
+
const url = `${config.endpoint}?key=${encodeURIComponent(config.publicKey)}`;
|
|
59
|
+
const sent = navigator.sendBeacon(
|
|
60
|
+
url,
|
|
61
|
+
new Blob([bytes], { type: "application/x-protobuf" })
|
|
62
|
+
);
|
|
63
|
+
if (sent) return;
|
|
64
|
+
}
|
|
65
|
+
fetch(config.endpoint, {
|
|
66
|
+
method: "POST",
|
|
67
|
+
headers: {
|
|
68
|
+
"Content-Type": "application/x-protobuf",
|
|
69
|
+
"X-Public-Key": config.publicKey
|
|
70
|
+
},
|
|
71
|
+
body: bytes,
|
|
72
|
+
keepalive: true
|
|
73
|
+
}).catch(() => {
|
|
74
|
+
if (queue.length < MAX_QUEUE) queue.unshift(...batch);
|
|
75
|
+
});
|
|
76
|
+
} catch {
|
|
62
77
|
}
|
|
63
|
-
fetch(config.endpoint, {
|
|
64
|
-
method: "POST",
|
|
65
|
-
headers: {
|
|
66
|
-
"Content-Type": "application/x-protobuf",
|
|
67
|
-
"X-Public-Key": config.publicKey
|
|
68
|
-
},
|
|
69
|
-
body: bytes,
|
|
70
|
-
keepalive: true
|
|
71
|
-
}).catch(() => {
|
|
72
|
-
queue.unshift(...batch);
|
|
73
|
-
});
|
|
74
78
|
}
|
|
75
79
|
function scheduleFlush() {
|
|
76
80
|
if (flushTimer) return;
|
|
@@ -128,6 +132,46 @@ export default defineNuxtPlugin((nuxtApp) => {
|
|
|
128
132
|
},
|
|
129
133
|
{ capture: true, passive: true }
|
|
130
134
|
);
|
|
135
|
+
if (config.captureInputs) {
|
|
136
|
+
const SENSITIVE = /pass|secret|token|otp|onetime|one-time|cvv|cvc|card|iban|ssn|pin|auth|security/i;
|
|
137
|
+
const fieldLabel = (el) => el.name || el.id || el.getAttribute("aria-label") || el.getAttribute("placeholder") || el.type || "field";
|
|
138
|
+
const isSensitive = (el) => {
|
|
139
|
+
const type = (el.type || "").toLowerCase();
|
|
140
|
+
if (type === "password") return true;
|
|
141
|
+
const hint = `${el.name || ""} ${el.id || ""} ${el.getAttribute("autocomplete") || ""}`;
|
|
142
|
+
return SENSITIVE.test(hint);
|
|
143
|
+
};
|
|
144
|
+
document.addEventListener(
|
|
145
|
+
"change",
|
|
146
|
+
(event) => {
|
|
147
|
+
try {
|
|
148
|
+
const el = event.target;
|
|
149
|
+
if (!el || !el.tagName) return;
|
|
150
|
+
const tag = el.tagName.toLowerCase();
|
|
151
|
+
if (tag !== "input" && tag !== "textarea" && tag !== "select") return;
|
|
152
|
+
const type = (el.type || "").toLowerCase();
|
|
153
|
+
let value;
|
|
154
|
+
if (type === "checkbox" || type === "radio") {
|
|
155
|
+
value = el.checked ? "checked" : "unchecked";
|
|
156
|
+
} else if (isSensitive(el)) {
|
|
157
|
+
const len = (el.value ?? "").length;
|
|
158
|
+
value = len ? `[masked, ${len} chars]` : "[empty]";
|
|
159
|
+
} else {
|
|
160
|
+
const raw = el.value ?? "";
|
|
161
|
+
if (!raw) return;
|
|
162
|
+
value = raw.length > 100 ? `${raw.slice(0, 100)}\u2026` : raw;
|
|
163
|
+
}
|
|
164
|
+
addCrumb({
|
|
165
|
+
type: "input",
|
|
166
|
+
category: "ui.input",
|
|
167
|
+
message: `${fieldLabel(el)} = ${value}`
|
|
168
|
+
});
|
|
169
|
+
} catch {
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
{ capture: true, passive: true }
|
|
173
|
+
);
|
|
174
|
+
}
|
|
131
175
|
useRouter().afterEach((to, from) => {
|
|
132
176
|
addCrumb({
|
|
133
177
|
type: "navigation",
|