nuxt-error-tracker 0.1.9 → 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.9",
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.
@@ -57,8 +59,9 @@ interface Breadcrumb {
57
59
  timestamp?: string
58
60
  data?: Record<string, unknown>
59
61
  }
62
+ type ErrorLevel = 'fatal' | 'error' | 'warning' | 'info' | 'debug'
60
63
  interface ErrorTrackerHelpers {
61
- $trackError: (err: unknown) => void
64
+ $trackError: (err: unknown, level?: ErrorLevel) => void
62
65
  $addBreadcrumb: (crumb: Breadcrumb) => void
63
66
  }
64
67
  declare module '#app' {
@@ -6,6 +6,7 @@ export interface Crumb {
6
6
  timestamp?: string;
7
7
  data?: Record<string, unknown>;
8
8
  }
9
+ export type ErrorLevel = 'fatal' | 'error' | 'warning' | 'info' | 'debug';
9
10
  export interface FlushError {
10
11
  message: string;
11
12
  stack: string;
@@ -13,6 +14,7 @@ export interface FlushError {
13
14
  url: string;
14
15
  route: string;
15
16
  occurredAt: string;
17
+ level?: ErrorLevel;
16
18
  breadcrumbs?: Crumb[];
17
19
  }
18
20
  export interface FlushBody {
@@ -48,6 +48,7 @@ function encodeError(e) {
48
48
  for (const c of e.breadcrumbs ?? []) {
49
49
  pushLenField(out, 10, encodeBreadcrumb(c));
50
50
  }
51
+ pushString(out, 11, e.level ?? "");
51
52
  return out;
52
53
  }
53
54
  export function encodeBatch(body) {
@@ -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;
@@ -79,7 +83,7 @@ export default defineNuxtPlugin((nuxtApp) => {
79
83
  flush();
80
84
  }, FLUSH_DELAY);
81
85
  }
82
- function capture(err, source) {
86
+ function capture(err, source, level = "error") {
83
87
  try {
84
88
  const error = err instanceof Error ? err : new Error(String(err));
85
89
  if (error.message === "Script error.") return;
@@ -91,6 +95,7 @@ export default defineNuxtPlugin((nuxtApp) => {
91
95
  message: error.message.slice(0, 1e3),
92
96
  stack: (error.stack ?? "").slice(0, 8e3),
93
97
  source,
98
+ level,
94
99
  url: window.location.href.slice(0, 2e3),
95
100
  route: route.fullPath.slice(0, 500),
96
101
  occurredAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -127,6 +132,46 @@ export default defineNuxtPlugin((nuxtApp) => {
127
132
  },
128
133
  { capture: true, passive: true }
129
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
+ }
130
175
  useRouter().afterEach((to, from) => {
131
176
  addCrumb({
132
177
  type: "navigation",
@@ -197,7 +242,7 @@ export default defineNuxtPlugin((nuxtApp) => {
197
242
  });
198
243
  return {
199
244
  provide: {
200
- trackError: (err) => capture(err, "window"),
245
+ trackError: (err, level = "error") => capture(err, "window", level),
201
246
  addBreadcrumb: (c) => addCrumb(c)
202
247
  }
203
248
  };
@@ -1,7 +1,7 @@
1
1
  import { defineNuxtPlugin } from "#imports";
2
2
  export default defineNuxtPlugin(() => ({
3
3
  provide: {
4
- trackError: (_err) => {
4
+ trackError: (_err, _level) => {
5
5
  },
6
6
  addBreadcrumb: (_c) => {
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nuxt-error-tracker",
3
- "version": "0.1.9",
3
+ "version": "0.1.12",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {