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 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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nuxt-error-tracker",
3
3
  "configKey": "errorTracker",
4
- "version": "0.1.10",
4
+ "version": "0.1.12",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "0.8.4",
7
7
  "unbuild": "2.0.0"
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
- const batch = queue.splice(0, MAX_BATCH);
46
- const body = {
47
- meta: {
48
- device: deviceInfo(),
49
- userId: resolveUserId(),
50
- appVersion: config.appVersion
51
- },
52
- errors: batch
53
- };
54
- const bytes = encodeBatch(body);
55
- if (useBeacon && navigator.sendBeacon) {
56
- const url = `${config.endpoint}?key=${encodeURIComponent(config.publicKey)}`;
57
- const sent = navigator.sendBeacon(
58
- url,
59
- new Blob([bytes], { type: "application/x-protobuf" })
60
- );
61
- if (sent) return;
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",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nuxt-error-tracker",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {