nuxt-error-tracker 0.1.3
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 +142 -0
- package/dist/module.cjs +5 -0
- package/dist/module.d.mts +24 -0
- package/dist/module.d.ts +24 -0
- package/dist/module.json +9 -0
- package/dist/module.mjs +55 -0
- package/dist/runtime/ingest.codec.d.ts +27 -0
- package/dist/runtime/ingest.codec.js +70 -0
- package/dist/runtime/plugin.client.d.ts +2 -0
- package/dist/runtime/plugin.client.js +204 -0
- package/dist/types.d.mts +7 -0
- package/dist/types.d.ts +7 -0
- package/package.json +33 -0
package/README.md
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# @ray_of_goodness_/nuxt-error-tracker
|
|
2
|
+
|
|
3
|
+
Lightweight frontend error tracker for **Nuxt 3 / 4**. Captures unhandled
|
|
4
|
+
errors, failed requests, and a breadcrumb trail of user actions, then ships them
|
|
5
|
+
to your Observer ingest endpoint. Client-only, batched, and built to **never
|
|
6
|
+
break the host app** — if it isn't configured, it does nothing.
|
|
7
|
+
|
|
8
|
+
## Features
|
|
9
|
+
|
|
10
|
+
- Captures Vue errors, `window.onerror`, unhandled promise rejections
|
|
11
|
+
- Optional `fetch` instrumentation — auto-reports network failures and 5xx
|
|
12
|
+
- Optional `console.error` capture
|
|
13
|
+
- **Breadcrumbs**: ring buffer of clicks, route changes, fetches and console
|
|
14
|
+
errors, snapshotted onto each error
|
|
15
|
+
- Batched + debounced sending; force-flush on page hide via `sendBeacon`
|
|
16
|
+
- Per-session dedup so one recurring error can't flood your quota
|
|
17
|
+
- Safe by default: missing keys → plugin is a no-op, never throws
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm i @ray_of_goodness_/nuxt-error-tracker
|
|
23
|
+
# or: bun add @ray_of_goodness_/nuxt-error-tracker
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Setup
|
|
27
|
+
|
|
28
|
+
Add the module and point it at your ingest endpoint with your project's public
|
|
29
|
+
key:
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
// nuxt.config.ts
|
|
33
|
+
export default defineNuxtConfig({
|
|
34
|
+
modules: ['@ray_of_goodness_/nuxt-error-tracker'],
|
|
35
|
+
errorTracker: {
|
|
36
|
+
endpoint: 'https://<your-observer-host>/v1/ingest',
|
|
37
|
+
publicKey: 'pk_xxxxxxxxxxxxxxxxxxxxxxxx',
|
|
38
|
+
},
|
|
39
|
+
})
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
> The public key identifies your project on the ingest side. It's a **public**
|
|
43
|
+
> value — safe to ship in client bundles. Origin allowlisting and rate limiting
|
|
44
|
+
> are enforced server-side.
|
|
45
|
+
|
|
46
|
+
### No keys? No problem
|
|
47
|
+
|
|
48
|
+
If `endpoint` or `publicKey` is empty (or `enabled: false`), the plugin is never
|
|
49
|
+
registered and your app runs untouched. This makes it safe to leave the module
|
|
50
|
+
installed while wiring keys per-environment — dev builds without a key simply
|
|
51
|
+
don't track.
|
|
52
|
+
|
|
53
|
+
## Configuration via environment variables
|
|
54
|
+
|
|
55
|
+
Every option can be driven from the environment instead of hardcoding it, using
|
|
56
|
+
Nuxt's `runtimeConfig` public-env convention:
|
|
57
|
+
|
|
58
|
+
```ini
|
|
59
|
+
NUXT_PUBLIC_ERROR_TRACKER_ENDPOINT=https://<your-observer-host>/v1/ingest
|
|
60
|
+
NUXT_PUBLIC_ERROR_TRACKER_PUBLIC_KEY=pk_xxxxxxxxxxxxxxxxxxxxxxxx
|
|
61
|
+
NUXT_PUBLIC_ERROR_TRACKER_APP_VERSION=1.4.2
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Wire them in `nuxt.config.ts`:
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
export default defineNuxtConfig({
|
|
68
|
+
modules: ['@ray_of_goodness_/nuxt-error-tracker'],
|
|
69
|
+
runtimeConfig: {
|
|
70
|
+
public: {
|
|
71
|
+
errorTracker: {
|
|
72
|
+
endpoint: process.env.NUXT_PUBLIC_ERROR_TRACKER_ENDPOINT,
|
|
73
|
+
publicKey: process.env.NUXT_PUBLIC_ERROR_TRACKER_PUBLIC_KEY,
|
|
74
|
+
appVersion: process.env.NUXT_PUBLIC_ERROR_TRACKER_APP_VERSION,
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
})
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Options
|
|
82
|
+
|
|
83
|
+
| Option | Type | Default | Description |
|
|
84
|
+
| ----------------- | --------- | ----------- | ------------------------------------------------------------------ |
|
|
85
|
+
| `endpoint` | `string` | `''` | Ingest URL (`.../v1/ingest`). Required. |
|
|
86
|
+
| `publicKey` | `string` | `''` | Your project's public key. Required. |
|
|
87
|
+
| `appVersion` | `string` | `'unknown'` | Tag every error with a release/version. |
|
|
88
|
+
| `enabled` | `boolean` | `true` | Master switch. `false` = fully disabled. |
|
|
89
|
+
| `captureFetch` | `boolean` | `true` | Wrap `fetch` to breadcrumb requests and report failures/5xx. |
|
|
90
|
+
| `captureConsole` | `boolean` | `true` | Patch `console.error` to capture logged-but-not-thrown errors. |
|
|
91
|
+
| `flushDelay` | `number` | `2000` | Debounce (ms) before a batch is sent. |
|
|
92
|
+
| `maxPerSession` | `number` | `5` | Max occurrences of the *same* error captured per session. |
|
|
93
|
+
| `maxBatch` | `number` | `20` | Max errors per request (server also hard-caps at 20). |
|
|
94
|
+
| `maxBreadcrumbs` | `number` | `30` | Breadcrumbs kept in the ring buffer / sent per error (max 50). |
|
|
95
|
+
|
|
96
|
+
## Runtime helpers
|
|
97
|
+
|
|
98
|
+
The plugin injects two helpers for manual use:
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
const { $trackError, $addBreadcrumb } = useNuxtApp()
|
|
102
|
+
|
|
103
|
+
// Report a handled error yourself
|
|
104
|
+
try {
|
|
105
|
+
await risky()
|
|
106
|
+
} catch (e) {
|
|
107
|
+
$trackError(e)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Add your own breadcrumb to the trail
|
|
111
|
+
$addBreadcrumb({
|
|
112
|
+
type: 'custom',
|
|
113
|
+
category: 'checkout',
|
|
114
|
+
message: 'user clicked pay',
|
|
115
|
+
level: 'info',
|
|
116
|
+
})
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## What gets sent
|
|
120
|
+
|
|
121
|
+
Per error: `message`, `stack`, `source` (`vue` | `window` | `promise` |
|
|
122
|
+
`fetch` | `console`), `url`, `route`, timestamp, and the breadcrumb trail. Once
|
|
123
|
+
per batch: `device` info (user agent, language, screen/viewport, connection),
|
|
124
|
+
`userId` (read from a `user_id` cookie if present), and `appVersion`. Field
|
|
125
|
+
lengths are truncated client-side to match server limits.
|
|
126
|
+
|
|
127
|
+
### Privacy
|
|
128
|
+
|
|
129
|
+
Click breadcrumbs record the element (tag / id / class / short text) — **never
|
|
130
|
+
input values**. Nothing is captured until an error occurs; breadcrumbs live only
|
|
131
|
+
in memory and ship attached to an error.
|
|
132
|
+
|
|
133
|
+
## How it sends
|
|
134
|
+
|
|
135
|
+
Payloads are encoded as **protobuf** and POSTed as `application/x-protobuf`. On
|
|
136
|
+
page-hide the queue is force-flushed via `navigator.sendBeacon`, falling back to
|
|
137
|
+
`fetch(..., { keepalive: true })`. Binary framing keeps the network panel tidy —
|
|
138
|
+
it is **not** encryption; the schema is public.
|
|
139
|
+
|
|
140
|
+
## License
|
|
141
|
+
|
|
142
|
+
MIT
|
package/dist/module.cjs
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import * as _nuxt_schema from '@nuxt/schema';
|
|
2
|
+
|
|
3
|
+
interface ModuleOptions {
|
|
4
|
+
endpoint: string;
|
|
5
|
+
publicKey: string;
|
|
6
|
+
appVersion?: string;
|
|
7
|
+
enabled?: boolean;
|
|
8
|
+
/** Wrap window.fetch to capture failed requests (network errors + 5xx). Default true. */
|
|
9
|
+
captureFetch?: boolean;
|
|
10
|
+
/** Patch console.error to capture logged-but-not-thrown errors. Default true. */
|
|
11
|
+
captureConsole?: boolean;
|
|
12
|
+
/** Debounce before a batch is flushed, in ms. Default 2000. */
|
|
13
|
+
flushDelay?: number;
|
|
14
|
+
/** Max occurrences of the same error captured per session. Default 5. */
|
|
15
|
+
maxPerSession?: number;
|
|
16
|
+
/** Max errors per flushed batch. Default 20 (server also caps at 20). */
|
|
17
|
+
maxBatch?: number;
|
|
18
|
+
/** Max breadcrumbs kept in the ring buffer / sent per error. Default 30. */
|
|
19
|
+
maxBreadcrumbs?: number;
|
|
20
|
+
}
|
|
21
|
+
declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
|
|
22
|
+
|
|
23
|
+
export { _default as default };
|
|
24
|
+
export type { ModuleOptions };
|
package/dist/module.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import * as _nuxt_schema from '@nuxt/schema';
|
|
2
|
+
|
|
3
|
+
interface ModuleOptions {
|
|
4
|
+
endpoint: string;
|
|
5
|
+
publicKey: string;
|
|
6
|
+
appVersion?: string;
|
|
7
|
+
enabled?: boolean;
|
|
8
|
+
/** Wrap window.fetch to capture failed requests (network errors + 5xx). Default true. */
|
|
9
|
+
captureFetch?: boolean;
|
|
10
|
+
/** Patch console.error to capture logged-but-not-thrown errors. Default true. */
|
|
11
|
+
captureConsole?: boolean;
|
|
12
|
+
/** Debounce before a batch is flushed, in ms. Default 2000. */
|
|
13
|
+
flushDelay?: number;
|
|
14
|
+
/** Max occurrences of the same error captured per session. Default 5. */
|
|
15
|
+
maxPerSession?: number;
|
|
16
|
+
/** Max errors per flushed batch. Default 20 (server also caps at 20). */
|
|
17
|
+
maxBatch?: number;
|
|
18
|
+
/** Max breadcrumbs kept in the ring buffer / sent per error. Default 30. */
|
|
19
|
+
maxBreadcrumbs?: number;
|
|
20
|
+
}
|
|
21
|
+
declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
|
|
22
|
+
|
|
23
|
+
export { _default as default };
|
|
24
|
+
export type { ModuleOptions };
|
package/dist/module.json
ADDED
package/dist/module.mjs
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { defineNuxtModule, extendViteConfig, createResolver, addPlugin } from '@nuxt/kit';
|
|
2
|
+
|
|
3
|
+
function posInt(value, fallback) {
|
|
4
|
+
const n = Number(value);
|
|
5
|
+
return Number.isFinite(n) && n > 0 ? Math.floor(n) : fallback;
|
|
6
|
+
}
|
|
7
|
+
const module = defineNuxtModule({
|
|
8
|
+
meta: {
|
|
9
|
+
name: "@ray_of_goodness_/nuxt-error-tracker",
|
|
10
|
+
configKey: "errorTracker"
|
|
11
|
+
},
|
|
12
|
+
defaults: {
|
|
13
|
+
endpoint: "",
|
|
14
|
+
publicKey: "",
|
|
15
|
+
appVersion: "unknown",
|
|
16
|
+
enabled: true,
|
|
17
|
+
captureFetch: true,
|
|
18
|
+
captureConsole: true,
|
|
19
|
+
flushDelay: 2e3,
|
|
20
|
+
maxPerSession: 5,
|
|
21
|
+
maxBatch: 20,
|
|
22
|
+
maxBreadcrumbs: 30
|
|
23
|
+
},
|
|
24
|
+
setup(options, nuxt) {
|
|
25
|
+
if (!options.enabled || !options.endpoint || !options.publicKey)
|
|
26
|
+
return;
|
|
27
|
+
nuxt.options.runtimeConfig.public.errorTracker = {
|
|
28
|
+
endpoint: options.endpoint,
|
|
29
|
+
publicKey: options.publicKey,
|
|
30
|
+
appVersion: options.appVersion ?? "unknown",
|
|
31
|
+
captureFetch: options.captureFetch !== false,
|
|
32
|
+
captureConsole: options.captureConsole !== false,
|
|
33
|
+
flushDelay: posInt(options.flushDelay, 2e3),
|
|
34
|
+
maxPerSession: posInt(options.maxPerSession, 5),
|
|
35
|
+
// Server hard-caps batches at 20 — never emit larger, even if misconfigured.
|
|
36
|
+
maxBatch: Math.min(posInt(options.maxBatch, 20), 20),
|
|
37
|
+
// Server DTO caps at 50 breadcrumbs — stay at or below.
|
|
38
|
+
maxBreadcrumbs: Math.min(posInt(options.maxBreadcrumbs, 30), 50)
|
|
39
|
+
};
|
|
40
|
+
extendViteConfig((config) => {
|
|
41
|
+
config.optimizeDeps ||= {};
|
|
42
|
+
config.optimizeDeps.include ||= [];
|
|
43
|
+
if (!config.optimizeDeps.include.includes("protobufjs")) {
|
|
44
|
+
config.optimizeDeps.include.push("protobufjs");
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
const resolver = createResolver(import.meta.url);
|
|
48
|
+
addPlugin({
|
|
49
|
+
src: resolver.resolve("./runtime/plugin.client"),
|
|
50
|
+
mode: "client"
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
export { module as default };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export interface Crumb {
|
|
2
|
+
type?: string;
|
|
3
|
+
category?: string;
|
|
4
|
+
message?: string;
|
|
5
|
+
level?: string;
|
|
6
|
+
timestamp?: string;
|
|
7
|
+
data?: Record<string, unknown>;
|
|
8
|
+
}
|
|
9
|
+
export interface FlushError {
|
|
10
|
+
message: string;
|
|
11
|
+
stack: string;
|
|
12
|
+
source: string;
|
|
13
|
+
url: string;
|
|
14
|
+
route: string;
|
|
15
|
+
occurredAt: string;
|
|
16
|
+
breadcrumbs?: Crumb[];
|
|
17
|
+
}
|
|
18
|
+
export interface FlushBody {
|
|
19
|
+
meta: {
|
|
20
|
+
device?: Record<string, unknown>;
|
|
21
|
+
userId?: string | null;
|
|
22
|
+
appVersion?: string;
|
|
23
|
+
};
|
|
24
|
+
errors: FlushError[];
|
|
25
|
+
}
|
|
26
|
+
/** Encode a flush body to protobuf bytes matching the shared Batch schema. */
|
|
27
|
+
export declare function encodeBatch(body: FlushBody): Uint8Array;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import protobuf from "protobufjs";
|
|
2
|
+
const INGEST_PROTO = `
|
|
3
|
+
syntax = "proto3";
|
|
4
|
+
message Breadcrumb {
|
|
5
|
+
string type = 1;
|
|
6
|
+
string category = 2;
|
|
7
|
+
string message = 3;
|
|
8
|
+
string level = 4;
|
|
9
|
+
string timestamp = 5;
|
|
10
|
+
string data = 6;
|
|
11
|
+
}
|
|
12
|
+
message Error {
|
|
13
|
+
string message = 1;
|
|
14
|
+
string stack = 2;
|
|
15
|
+
string source = 3;
|
|
16
|
+
string url = 4;
|
|
17
|
+
string route = 5;
|
|
18
|
+
string userId = 6;
|
|
19
|
+
string appVersion = 7;
|
|
20
|
+
string device = 8;
|
|
21
|
+
string occurredAt = 9;
|
|
22
|
+
repeated Breadcrumb breadcrumbs = 10;
|
|
23
|
+
}
|
|
24
|
+
message Meta {
|
|
25
|
+
string device = 1;
|
|
26
|
+
string userId = 2;
|
|
27
|
+
string appVersion = 3;
|
|
28
|
+
}
|
|
29
|
+
message Batch {
|
|
30
|
+
Meta meta = 1;
|
|
31
|
+
repeated Error errors = 2;
|
|
32
|
+
}
|
|
33
|
+
`;
|
|
34
|
+
const Batch = protobuf.parse(INGEST_PROTO).root.lookupType("Batch");
|
|
35
|
+
function jsonStr(v) {
|
|
36
|
+
if (v === void 0 || v === null) return "";
|
|
37
|
+
try {
|
|
38
|
+
return JSON.stringify(v);
|
|
39
|
+
} catch {
|
|
40
|
+
return "";
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export function encodeBatch(body) {
|
|
44
|
+
const message = {
|
|
45
|
+
meta: {
|
|
46
|
+
device: jsonStr(body.meta.device),
|
|
47
|
+
userId: body.meta.userId ?? "",
|
|
48
|
+
appVersion: body.meta.appVersion ?? ""
|
|
49
|
+
},
|
|
50
|
+
errors: body.errors.map((e) => ({
|
|
51
|
+
message: e.message,
|
|
52
|
+
stack: e.stack,
|
|
53
|
+
source: e.source,
|
|
54
|
+
url: e.url,
|
|
55
|
+
route: e.route,
|
|
56
|
+
appVersion: "",
|
|
57
|
+
device: "",
|
|
58
|
+
occurredAt: e.occurredAt,
|
|
59
|
+
breadcrumbs: (e.breadcrumbs ?? []).map((c) => ({
|
|
60
|
+
type: c.type ?? "",
|
|
61
|
+
category: c.category ?? "",
|
|
62
|
+
message: c.message ?? "",
|
|
63
|
+
level: c.level ?? "",
|
|
64
|
+
timestamp: c.timestamp ?? "",
|
|
65
|
+
data: jsonStr(c.data)
|
|
66
|
+
}))
|
|
67
|
+
}))
|
|
68
|
+
};
|
|
69
|
+
return Batch.encode(Batch.fromObject(message)).finish();
|
|
70
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import {
|
|
2
|
+
defineNuxtPlugin,
|
|
3
|
+
useCookie,
|
|
4
|
+
useRoute,
|
|
5
|
+
useRouter,
|
|
6
|
+
useRuntimeConfig
|
|
7
|
+
} from "#imports";
|
|
8
|
+
import { encodeBatch } from "./ingest.codec.js";
|
|
9
|
+
export default defineNuxtPlugin((nuxtApp) => {
|
|
10
|
+
const config = useRuntimeConfig().public.errorTracker;
|
|
11
|
+
if (!config || !config.endpoint || !config.publicKey) return;
|
|
12
|
+
const route = useRoute();
|
|
13
|
+
const FLUSH_DELAY = config.flushDelay ?? 2e3;
|
|
14
|
+
const MAX_PER_SESSION = config.maxPerSession ?? 5;
|
|
15
|
+
const MAX_BATCH = config.maxBatch ?? 20;
|
|
16
|
+
const MAX_CRUMBS = config.maxBreadcrumbs ?? 30;
|
|
17
|
+
const queue = [];
|
|
18
|
+
const sessionCounts = /* @__PURE__ */ new Map();
|
|
19
|
+
let flushTimer = null;
|
|
20
|
+
const crumbs = [];
|
|
21
|
+
function addCrumb(c) {
|
|
22
|
+
crumbs.push({ ...c, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
23
|
+
if (crumbs.length > MAX_CRUMBS) crumbs.shift();
|
|
24
|
+
}
|
|
25
|
+
function deviceInfo() {
|
|
26
|
+
return {
|
|
27
|
+
userAgent: navigator.userAgent,
|
|
28
|
+
language: navigator.language,
|
|
29
|
+
platform: navigator.platform,
|
|
30
|
+
screen: `${window.screen.width}x${window.screen.height}`,
|
|
31
|
+
viewport: `${window.innerWidth}x${window.innerHeight}`,
|
|
32
|
+
online: navigator.onLine,
|
|
33
|
+
memory: navigator.deviceMemory ?? null,
|
|
34
|
+
connection: navigator.connection?.effectiveType ?? null
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function resolveUserId() {
|
|
38
|
+
return useCookie("user_id").value ?? null;
|
|
39
|
+
}
|
|
40
|
+
function sessionKey(message, stack) {
|
|
41
|
+
return `${message.slice(0, 120)}::${(stack.split("\n")[1] ?? "").trim()}`;
|
|
42
|
+
}
|
|
43
|
+
function flush(useBeacon = false) {
|
|
44
|
+
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;
|
|
62
|
+
}
|
|
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
|
+
}
|
|
75
|
+
function scheduleFlush() {
|
|
76
|
+
if (flushTimer) return;
|
|
77
|
+
flushTimer = setTimeout(() => {
|
|
78
|
+
flushTimer = null;
|
|
79
|
+
flush();
|
|
80
|
+
}, FLUSH_DELAY);
|
|
81
|
+
}
|
|
82
|
+
function capture(err, source) {
|
|
83
|
+
try {
|
|
84
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
85
|
+
if (error.message === "Script error.") return;
|
|
86
|
+
const key = sessionKey(error.message, error.stack ?? "");
|
|
87
|
+
const count = sessionCounts.get(key) ?? 0;
|
|
88
|
+
if (count >= MAX_PER_SESSION) return;
|
|
89
|
+
sessionCounts.set(key, count + 1);
|
|
90
|
+
queue.push({
|
|
91
|
+
message: error.message.slice(0, 1e3),
|
|
92
|
+
stack: (error.stack ?? "").slice(0, 8e3),
|
|
93
|
+
source,
|
|
94
|
+
url: window.location.href.slice(0, 2e3),
|
|
95
|
+
route: route.fullPath.slice(0, 500),
|
|
96
|
+
occurredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
97
|
+
// Snapshot the trail leading up to this error (copy, not reference).
|
|
98
|
+
breadcrumbs: crumbs.slice(-MAX_CRUMBS)
|
|
99
|
+
});
|
|
100
|
+
scheduleFlush();
|
|
101
|
+
} catch {
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
nuxtApp.hook("vue:error", (err) => capture(err, "vue"));
|
|
105
|
+
window.addEventListener(
|
|
106
|
+
"error",
|
|
107
|
+
(event) => capture(event.error ?? event.message, "window")
|
|
108
|
+
);
|
|
109
|
+
window.addEventListener(
|
|
110
|
+
"unhandledrejection",
|
|
111
|
+
(event) => capture(event.reason, "promise")
|
|
112
|
+
);
|
|
113
|
+
document.addEventListener(
|
|
114
|
+
"click",
|
|
115
|
+
(event) => {
|
|
116
|
+
const el = event.target;
|
|
117
|
+
if (!el || !el.tagName) return;
|
|
118
|
+
const tag = el.tagName.toLowerCase();
|
|
119
|
+
const id = el.id ? `#${el.id}` : "";
|
|
120
|
+
const cls = typeof el.className === "string" && el.className ? `.${el.className.trim().split(/\s+/).slice(0, 2).join(".")}` : "";
|
|
121
|
+
const text = (el.textContent ?? "").trim().slice(0, 40);
|
|
122
|
+
addCrumb({
|
|
123
|
+
type: "click",
|
|
124
|
+
category: "ui.click",
|
|
125
|
+
message: `${tag}${id}${cls}${text ? ` "${text}"` : ""}`
|
|
126
|
+
});
|
|
127
|
+
},
|
|
128
|
+
{ capture: true, passive: true }
|
|
129
|
+
);
|
|
130
|
+
useRouter().afterEach((to, from) => {
|
|
131
|
+
addCrumb({
|
|
132
|
+
type: "navigation",
|
|
133
|
+
category: "navigation",
|
|
134
|
+
message: `${from.fullPath} \u2192 ${to.fullPath}`
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
if (config.captureFetch && typeof window.fetch === "function") {
|
|
138
|
+
const originalFetch = window.fetch.bind(window);
|
|
139
|
+
window.fetch = async (input, init) => {
|
|
140
|
+
const method = (init?.method ?? (input instanceof Request ? input.method : "GET")).toUpperCase();
|
|
141
|
+
const reqUrl = input instanceof Request ? input.url : String(input);
|
|
142
|
+
const isOwn = reqUrl.startsWith(config.endpoint);
|
|
143
|
+
try {
|
|
144
|
+
const res = await originalFetch(input, init);
|
|
145
|
+
if (!isOwn) {
|
|
146
|
+
addCrumb({
|
|
147
|
+
type: "fetch",
|
|
148
|
+
category: "http",
|
|
149
|
+
level: res.status >= 400 ? "error" : "info",
|
|
150
|
+
message: `${method} ${reqUrl} \u2192 ${res.status}`
|
|
151
|
+
});
|
|
152
|
+
if (res.status >= 500) {
|
|
153
|
+
capture(
|
|
154
|
+
new Error(`HTTP ${res.status} ${method} ${reqUrl}`),
|
|
155
|
+
"fetch"
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return res;
|
|
160
|
+
} catch (err) {
|
|
161
|
+
if (!isOwn) {
|
|
162
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
163
|
+
addCrumb({
|
|
164
|
+
type: "fetch",
|
|
165
|
+
category: "http",
|
|
166
|
+
level: "error",
|
|
167
|
+
message: `${method} ${reqUrl} \u2192 failed: ${reason}`
|
|
168
|
+
});
|
|
169
|
+
capture(
|
|
170
|
+
new Error(`Fetch failed ${method} ${reqUrl}: ${reason}`),
|
|
171
|
+
"fetch"
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
throw err;
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
if (config.captureConsole) {
|
|
179
|
+
const originalError = console.error.bind(console);
|
|
180
|
+
console.error = (...args) => {
|
|
181
|
+
const errArg = args.find((a) => a instanceof Error);
|
|
182
|
+
const message = errArg?.message ?? args.map((a) => typeof a === "string" ? a : String(a)).join(" ").slice(0, 1e3);
|
|
183
|
+
if (message) {
|
|
184
|
+
addCrumb({
|
|
185
|
+
type: "console",
|
|
186
|
+
category: "console",
|
|
187
|
+
level: "error",
|
|
188
|
+
message
|
|
189
|
+
});
|
|
190
|
+
capture(errArg ?? new Error(message), "console");
|
|
191
|
+
}
|
|
192
|
+
originalError(...args);
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
document.addEventListener("visibilitychange", () => {
|
|
196
|
+
if (document.visibilityState === "hidden") flush(true);
|
|
197
|
+
});
|
|
198
|
+
return {
|
|
199
|
+
provide: {
|
|
200
|
+
trackError: (err) => capture(err, "window"),
|
|
201
|
+
addBreadcrumb: (c) => addCrumb(c)
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
});
|
package/dist/types.d.mts
ADDED
package/dist/types.d.ts
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nuxt-error-tracker",
|
|
3
|
+
"version": "0.1.3",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"exports": {
|
|
6
|
+
".": {
|
|
7
|
+
"types": "./dist/types.d.ts",
|
|
8
|
+
"import": "./dist/module.mjs"
|
|
9
|
+
}
|
|
10
|
+
},
|
|
11
|
+
"main": "./dist/module.mjs",
|
|
12
|
+
"files": [
|
|
13
|
+
"dist"
|
|
14
|
+
],
|
|
15
|
+
"publishConfig": {
|
|
16
|
+
"access": "public"
|
|
17
|
+
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "nuxt-module-build build",
|
|
20
|
+
"prepublishOnly": "nuxt-module-build build"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"protobufjs": "^8.6.6"
|
|
24
|
+
},
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"@nuxt/kit": "^3.14.0 || ^4.0.0"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@nuxt/kit": "^3.14.0",
|
|
30
|
+
"@nuxt/module-builder": "^0.8.0",
|
|
31
|
+
"nuxt": "^3.14.0"
|
|
32
|
+
}
|
|
33
|
+
}
|