nuxt-error-tracker 0.1.17 → 0.1.19

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/LICENSE ADDED
@@ -0,0 +1,91 @@
1
+ # PolyForm Shield License 1.0.0
2
+
3
+ <https://polyformproject.org/licenses/shield/1.0.0>
4
+
5
+ Required Notice: Copyright ray_of_goodness_
6
+
7
+ ## Acceptance
8
+
9
+ In order to get any license under these terms, you must agree to them as both strict obligations and conditions to all your licenses.
10
+
11
+ ## Copyright License
12
+
13
+ The licensor grants you a copyright license for the software to do everything you might do with the software that would otherwise infringe the licensor's copyright in it for any permitted purpose. However, you may only distribute the software according to [Distribution License](#distribution-license) and make changes or new works based on the software according to [Changes and New Works License](#changes-and-new-works-license).
14
+
15
+ ## Distribution License
16
+
17
+ The licensor grants you an additional copyright license to distribute copies of the software. Your license to distribute covers distributing the software with changes and new works permitted by [Changes and New Works License](#changes-and-new-works-license).
18
+
19
+ ## Notices
20
+
21
+ You must ensure that anyone who gets a copy of any part of the software from you also gets a copy of these terms or the URL for them above, as well as copies of any plain-text lines beginning with `Required Notice:` that the licensor provided with the software. For example:
22
+
23
+ > Required Notice: Copyright Yoyodyne, Inc. (http://example.com)
24
+
25
+ ## Changes and New Works License
26
+
27
+ The licensor grants you an additional copyright license to make changes and new works based on the software for any permitted purpose.
28
+
29
+ ## Patent License
30
+
31
+ The licensor grants you a patent license for the software that covers patent claims the licensor can license, or becomes able to license, that you would infringe by using the software.
32
+
33
+ ## Noncompete
34
+
35
+ Any purpose is a permitted purpose, except for providing any product that competes with the software or any product the licensor or any of its affiliates provides using the software.
36
+
37
+ ## Competition
38
+
39
+ Goods and services compete even when they provide functionality through different kinds of interfaces or for different technical platforms. Applications can compete with services, libraries with plugins, frameworks with development tools, and so on, even if they're written in different programming languages or for different computer architectures. Goods and services compete even when provided free of charge. If you market a product as a practical substitute for the software or another product, it definitely competes.
40
+
41
+ ## New Products
42
+
43
+ If you are using the software to provide a product that does not compete, but the licensor or any of its affiliates brings your product into competition by providing a new version of the software or another product using the software, you may continue using versions of the software available under these terms beforehand to provide your competing product, but not any later versions.
44
+
45
+ ## Discontinued Products
46
+
47
+ You may begin using the software to compete with a product or service that the licensor or any of its affiliates has stopped providing, unless the licensor includes a plain-text line beginning with `Licensor Line of Business:` with the software that mentions that line of business. For example:
48
+
49
+ > Licensor Line of Business: YoyodyneCMS Content Management System (http://example.com/cms)
50
+
51
+ ## Sales of Business
52
+
53
+ If the licensor or any of its affiliates sells a line of business developing the software or using the software to provide a product, the buyer can also enforce [Noncompete](#noncompete) for that product.
54
+
55
+ ## Fair Use
56
+
57
+ You may have "fair use" rights for the software under the law. These terms do not limit them.
58
+
59
+ ## No Other Rights
60
+
61
+ These terms do not allow you to sublicense or transfer any of your licenses to anyone else, or prevent the licensor from granting licenses to anyone else. These terms do not imply any other licenses.
62
+
63
+ ## Patent Defense
64
+
65
+ If you make any written claim that the software infringes or contributes to infringement of any patent, your patent license for the software granted under these terms ends immediately. If your company makes such a claim, your patent license ends immediately for work on behalf of your company.
66
+
67
+ ## Violations
68
+
69
+ The first time you are notified in writing that you have violated any of these terms, or done anything with the software not covered by your licenses, your licenses can nonetheless continue if you come into full compliance with these terms, and take practical steps to correct past violations, within 32 days of receiving notice. Otherwise, all your licenses end immediately.
70
+
71
+ ## No Liability
72
+
73
+ ***As far as the law allows, the software comes as is, without any warranty or condition, and the licensor will not be liable to you for any damages arising out of these terms or the use or nature of the software, under any kind of legal claim.***
74
+
75
+ ## Definitions
76
+
77
+ The **licensor** is the individual or entity offering these terms, and the **software** is the software the licensor makes available under these terms.
78
+
79
+ A **product** can be a good or service, or a combination of them.
80
+
81
+ **You** refers to the individual or entity agreeing to these terms.
82
+
83
+ **Your company** is any legal entity, sole proprietorship, or other kind of organization that you work for, plus all its affiliates.
84
+
85
+ **Affiliates** means the other organizations than an organization has control over, is under the control of, or is under common control with.
86
+
87
+ **Control** means ownership of substantially all the assets of an entity, or the power to direct its management and policies by vote, contract, or otherwise. Control can be direct or indirect.
88
+
89
+ **Your licenses** are all the licenses granted to you for the software under these terms.
90
+
91
+ **Use** means anything you do with the software requiring one of your licenses.
package/README.md CHANGED
@@ -1,20 +1,67 @@
1
1
  # nuxt-error-tracker
2
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
3
+ **English** · [Українська](./README.uk.md)
9
4
 
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
5
+ Lightweight frontend error tracker for **Nuxt 3 / 4**. It captures unhandled
6
+ errors, failed requests, and a breadcrumb trail of user actions, then ships them
7
+ to **your own** Observer ingest endpoint. Client-only, batched, and built to
8
+ **never break the host app** if it isn't configured, it does nothing.
9
+
10
+ You bring the URL and a public key; the package does the capturing. Everything
11
+ you send lands under your project in your Observer dashboard, where you view,
12
+ search and manage the logs yourself.
13
+
14
+ ---
15
+
16
+ ## Table of contents
17
+
18
+ - [How it works (the moving parts)](#how-it-works-the-moving-parts)
19
+ - [Install](#install)
20
+ - [Getting your endpoint and key](#getting-your-endpoint-and-key)
21
+ - [Setup](#setup)
22
+ - [Configuration via environment variables](#configuration-via-environment-variables)
23
+ - [Options](#options)
24
+ - [Runtime helpers](#runtime-helpers)
25
+ - [What gets sent to the API](#what-gets-sent-to-the-api)
26
+ - [Wire format & data types](#wire-format--data-types)
27
+ - [Viewing and controlling your logs](#viewing-and-controlling-your-logs)
28
+ - [Privacy](#privacy)
29
+ - [License](#license)
30
+
31
+ ---
32
+
33
+ ## How it works (the moving parts)
34
+
35
+ There are three pieces. This package is only the first one.
36
+
37
+ | Piece | What it is | Responsibility |
38
+ | --- | --- | --- |
39
+ | **This package** (`nuxt-error-tracker`) | A Nuxt module you install in your app | Hooks into the browser, captures errors + breadcrumbs, batches them, and POSTs to your endpoint. Runs **client-side only**. |
40
+ | **Your ingest endpoint** (`.../v1/ingest`) | The Observer API you host (or are given access to) | Authenticates the request by public key + origin, validates and rate-limits it, deduplicates errors by fingerprint, and stores them. |
41
+ | **Your dashboard** | The Observer console UI | Where **you** browse, search, and manage the logs — stack traces, occurrences, breadcrumb trails, retention. |
42
+
43
+ The package never decides *where* the data goes — **you do**, by pointing
44
+ `endpoint` at your host. It ships no default URL. Swap the host and your logs
45
+ follow. This is what makes it "bring your own backend": the same package works
46
+ against localhost during development and your production Observer instance in
47
+ release, just by changing the endpoint.
48
+
49
+ Inside the package itself:
50
+
51
+ - **`module.ts`** — the Nuxt module. Reads your options, and **only registers
52
+ the capture plugin if `enabled` + `endpoint` + `publicKey` are all present**.
53
+ Missing any of them → the plugin is never added and your app runs untouched.
54
+ It also normalizes options (clamps `maxBatch ≤ 20`, `maxBreadcrumbs ≤ 50` to
55
+ match server limits) and injects TypeScript types for the helpers.
56
+ - **`plugin.client.ts`** — the actual capture engine (browser only). Listens for
57
+ errors, wraps `fetch`, patches `console.error`, records breadcrumbs, and
58
+ manages the send queue.
59
+ - **`plugin.server.ts`** — a tiny SSR stub. Provides no-op `$trackError` /
60
+ `$addBreadcrumb` so calling them during server-side rendering never throws.
61
+ - **`ingest.codec.ts`** — a hand-rolled protobuf encoder (no dependencies) that
62
+ turns a batch into the binary wire format the API expects.
63
+
64
+ ---
18
65
 
19
66
  ## Install
20
67
 
@@ -23,31 +70,47 @@ npm i nuxt-error-tracker
23
70
  # or: bun add nuxt-error-tracker
24
71
  ```
25
72
 
26
- ## Getting your keys
73
+ ---
27
74
 
28
- The module needs two values — an **ingest endpoint** and a **public key**. Both
29
- come from your Observer dashboard. Everything after step 4 is what you paste into
30
- the app; from then on your errors show up in the same dashboard.
75
+ ## Getting your endpoint and key
76
+
77
+ The module needs exactly two things to start reporting: an **ingest endpoint**
78
+ (your URL) and a **public key** (which project the errors belong to). Both come
79
+ from your Observer dashboard.
31
80
 
32
81
  1. Open the dashboard at `https://<your-observer-host>/dashboard`.
33
- 2. **Create account** — enter an email and a password (min 8 chars), submit. You
34
- stay signed in via a secure cookie.
82
+ 2. **Create an account** — enter an email and a password (min 8 chars), submit.
83
+ You stay signed in via a secure httpOnly cookie.
35
84
  3. Click **New project**, give it a name (e.g. `my-app`), and optionally list the
36
- origins your site runs on (comma-separated, e.g. `https://my-app.com,
37
- http://localhost:3000`). Empty = accept any origin. This is the server-side
38
- allowlist — requests from other origins are rejected.
39
- 4. The project now shows a **public key** (`pk_…`) and a ready-made
40
- `nuxt.config` snippet. Use **Copy key** / **Copy config**.
85
+ origins your site runs on (comma-separated, e.g.
86
+ `https://my-app.com, http://localhost:3000`). Empty = accept any origin. This
87
+ is the **server-side allowlist** — requests from other origins are rejected.
88
+ 4. The project now shows a **public key** (`pk_…`) and a ready-made `nuxt.config`
89
+ snippet. Use **Copy key** / **Copy config**.
41
90
  5. The **endpoint** is your host + `/v1/ingest`, i.e.
42
91
  `https://<your-observer-host>/v1/ingest`.
43
92
 
44
- Then configure the package ([Setup](#setup)) and deploy. Captured errors appear
45
- under that project in the dashboard — open **Logs** on the project to browse
46
- them, with stack traces, recent occurrences and the breadcrumb trail.
93
+ ### What the two values actually do
94
+
95
+ - **`endpoint`** *where* to send. This is your URL. Point it at whatever host
96
+ runs the Observer API. There is no hosted default; you are always sending to
97
+ your own backend.
98
+ - **`publicKey`** — *whose project* the errors are. On every request the API
99
+ looks up the project by this key (`X-Public-Key` header, or `?key=` query for
100
+ the beacon path), then enforces that project's origin allowlist and rate
101
+ limit. It is a **public** value — safe to ship in client bundles. It grants
102
+ *write-only* ingest for one project; it does **not** let anyone read your logs.
103
+ Reading requires your dashboard login.
104
+
105
+ > **Origins matter.** If you set an allowlist, it must include **every** origin
106
+ > your app is served from — production domains **and** `http://localhost:3000`
107
+ > for local dev — or the browser's requests get a `401`. Leave it empty while
108
+ > experimenting, tighten it for production.
47
109
 
48
- > Origins matter: if you set an allowlist, it must include every origin your app
49
- > is served from (prod domains **and** `http://localhost:3000` for local dev),
50
- > or the browser's requests get a 401.
110
+ Once configured (below) and deployed, captured errors appear under that project
111
+ in the dashboard. Open **Logs** on the project to browse them.
112
+
113
+ ---
51
114
 
52
115
  ## Setup
53
116
 
@@ -65,16 +128,16 @@ export default defineNuxtConfig({
65
128
  })
66
129
  ```
67
130
 
68
- > The public key identifies your project on the ingest side. It's a **public**
69
- > value — safe to ship in client bundles. Origin allowlisting and rate limiting
70
- > are enforced server-side.
131
+ That's the minimum. Everything else has sensible defaults.
71
132
 
72
133
  ### No keys? No problem
73
134
 
74
135
  If `endpoint` or `publicKey` is empty (or `enabled: false`), the plugin is never
75
136
  registered and your app runs untouched. This makes it safe to leave the module
76
137
  installed while wiring keys per-environment — dev builds without a key simply
77
- don't track.
138
+ don't track. There is no crash, no console noise, no network call.
139
+
140
+ ---
78
141
 
79
142
  ## Configuration via environment variables
80
143
 
@@ -104,21 +167,32 @@ export default defineNuxtConfig({
104
167
  })
105
168
  ```
106
169
 
170
+ This is the recommended way to point the same build at a **different endpoint**
171
+ per environment (staging vs. production) without touching code.
172
+
173
+ ---
174
+
107
175
  ## Options
108
176
 
109
- | Option | Type | Default | Description |
110
- | ----------------- | --------- | ----------- | ------------------------------------------------------------------ |
111
- | `endpoint` | `string` | `''` | Ingest URL (`.../v1/ingest`). Required. |
112
- | `publicKey` | `string` | `''` | Your project's public key. Required. |
113
- | `appVersion` | `string` | `'unknown'` | Tag every error with a release/version. |
114
- | `enabled` | `boolean` | `true` | Master switch. `false` = fully disabled. |
115
- | `captureFetch` | `boolean` | `true` | Wrap `fetch` to breadcrumb requests and report failures/5xx. |
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. |
118
- | `flushDelay` | `number` | `2000` | Debounce (ms) before a batch is sent. |
119
- | `maxPerSession` | `number` | `5` | Max occurrences of the *same* error captured per session. |
120
- | `maxBatch` | `number` | `20` | Max errors per request (server also hard-caps at 20). |
121
- | `maxBreadcrumbs` | `number` | `30` | Breadcrumbs kept in the ring buffer / sent per error (max 50). |
177
+ | Option | Type | Default | Description |
178
+ | --------------------- | --------- | ----------- | ------------------------------------------------------------------ |
179
+ | `endpoint` | `string` | `''` | Ingest URL (`.../v1/ingest`). **Required** — your host. |
180
+ | `publicKey` | `string` | `''` | Your project's public key (`pk_…`). **Required**. |
181
+ | `appVersion` | `string` | `'unknown'` | Tags every batch with a release/version so you can filter by it. |
182
+ | `enabled` | `boolean` | `true` | Master switch. `false` = fully disabled, plugin not registered. |
183
+ | `captureFetch` | `boolean` | `true` | Wrap `fetch` to breadcrumb requests and report failures / 5xx. |
184
+ | `captureFetchDetails` | `boolean` | `true` | Attach full request/response detail (method, status, timing, headers, request payload, response body) to each fetch breadcrumb. Sensitive headers redacted, bodies truncated to 2 KB. Set `false` to keep fetch crumbs message-only (avoids capturing payloads / PII). |
185
+ | `captureConsole` | `boolean` | `true` | Patch `console.error` to capture logged-but-not-thrown errors. |
186
+ | `captureInputs` | `boolean` | `false` | Record form input values as breadcrumbs (on change/blur). Sensitive fields masked. **Opt-in** — captures user-entered data. |
187
+ | `flushDelay` | `number` | `2000` | Debounce (ms) before a queued batch is sent. |
188
+ | `minFlushInterval` | `number` | `0` | Floor (ms) between consecutive flushes — throttles request rate during an error storm. `0` = off. |
189
+ | `sampleRate` | `number` | `1` | Fraction (0..1) of captured errors actually sent; the rest are dropped at capture. `0.3` keep 30%. Thins volume before the server rate limit. |
190
+ | `maxPerMinute` | `number` | `0` | Client-side cap on errors queued per rolling minute (fixed window); excess dropped. Mirrors the server per-project rate limit. `0` = unlimited. |
191
+ | `maxPerSession` | `number` | `5` | Max occurrences of the *same* error captured per session. |
192
+ | `maxBatch` | `number` | `20` | Max errors per request (clamped to server hard cap of 20). |
193
+ | `maxBreadcrumbs` | `number` | `30` | Breadcrumbs kept in the ring buffer / sent per error (clamped to 50). |
194
+
195
+ ---
122
196
 
123
197
  ## Runtime helpers
124
198
 
@@ -153,19 +227,141 @@ augmentation for `NuxtApp` and Vue's `ComponentCustomProperties`, so both
153
227
  `useNuxtApp()` and template usage resolve them as functions (no `unknown` /
154
228
  TS18046). No manual `.d.ts` needed.
155
229
 
156
- ## What gets sent
230
+ ---
231
+
232
+ ## What gets sent to the API
233
+
234
+ Nothing is sent until an error occurs. The package keeps an in-memory breadcrumb
235
+ ring buffer as the user interacts; when an error is captured, it snapshots that
236
+ trail, queues the error, and (after a debounce) POSTs a **batch**.
237
+
238
+ A batch has two parts: **`meta`** (sent once per batch, applies to all errors in
239
+ it) and **`errors`** (the array of captured errors).
240
+
241
+ ### `meta` (once per batch)
242
+
243
+ | Field | Type | Source | Notes |
244
+ | ------------ | -------- | ------ | ----- |
245
+ | `device` | `object` | `navigator` / `window` | `userAgent`, `language`, `platform`, `screen` (`WxH`), `viewport` (`WxH`), `online` (bool), `memory` (`deviceMemory`, GB or `null`), `connection` (`effectiveType` or `null`). Server caps the JSON at 2 KB. |
246
+ | `userId` | `string` \| `null` | `user_id` cookie | Read from a cookie named `user_id` if present, else `null`. The package never invents an id. Max 100 chars. |
247
+ | `appVersion` | `string` | your `appVersion` option | Defaults to `'unknown'`. Max 50 chars. |
248
+
249
+ ### `errors[]` (per captured error)
250
+
251
+ | Field | Type | Description | Limit |
252
+ | ------------- | --------- | ----------- | ----- |
253
+ | `message` | `string` | Error message. | ≤ 1000 chars |
254
+ | `stack` | `string` | Stack trace. | ≤ 8000 chars |
255
+ | `source` | `string` | Where it came from: `vue`, `window`, `promise`, `fetch`, or `console`. | ≤ 20 chars |
256
+ | `level` | `string` | Severity: `fatal` \| `error` \| `warning` \| `info` \| `debug`. | one of |
257
+ | `url` | `string` | `window.location.href` at capture time. | ≤ 2000 chars |
258
+ | `route` | `string` | Nuxt route `fullPath`. | ≤ 500 chars |
259
+ | `occurredAt` | `string` | ISO-8601 timestamp. | ISO-8601 |
260
+ | `breadcrumbs` | `Breadcrumb[]` | The trail leading up to the error (see below). | ≤ 50 items |
261
+
262
+ Each **`Breadcrumb`**:
263
+
264
+ | Field | Type | Description | Limit |
265
+ | ----------- | -------- | ----------- | ----- |
266
+ | `type` | `string` | `click`, `input`, `navigation`, `fetch`, `console`, or your custom type. | ≤ 30 chars |
267
+ | `category` | `string` | e.g. `ui.click`, `ui.input`, `navigation`, `http`, `console`. | ≤ 60 chars |
268
+ | `message` | `string` | Human-readable line, e.g. `POST /api/pay → 500`. | ≤ 500 chars |
269
+ | `level` | `string` | `info` \| `error` \| … | ≤ 20 chars |
270
+ | `timestamp` | `string` | ISO-8601. | ISO-8601 |
271
+ | `data` | `object` | Optional structured payload (fetch details: method, status, timing, headers, request/response bodies). | JSON |
272
+
273
+ **Batch caps:** at most **20 errors per request**; the client also re-queues
274
+ failed batches (bounded to 200 in memory) and dedups the *same* error to
275
+ `maxPerSession` (5) occurrences per session, so one recurring error can't flood
276
+ your quota.
277
+
278
+ ---
279
+
280
+ ## Wire format & data types
281
+
282
+ Payloads are encoded as **protobuf** (proto3) and POSTed as
283
+ `application/x-protobuf`. The encoder is hand-rolled and has **zero
284
+ dependencies** — no `protobufjs`, so consumers need no Vite `optimizeDeps` or
285
+ CJS-interop config.
286
+
287
+ The schema (field numbers are load-bearing — they must match the API decoder):
288
+
289
+ ```proto
290
+ message Breadcrumb {
291
+ string type = 1;
292
+ string category = 2;
293
+ string message = 3;
294
+ string level = 4;
295
+ string timestamp = 5;
296
+ string data = 6; // JSON-stringified object
297
+ }
298
+
299
+ message Error {
300
+ string message = 1;
301
+ string stack = 2;
302
+ string source = 3;
303
+ string url = 4;
304
+ string route = 5;
305
+ string userId = 6; // per-error override (unused by this client)
306
+ string appVersion = 7; // per-error override (unused by this client)
307
+ string device = 8; // per-error override (unused by this client)
308
+ string occurredAt = 9;
309
+ repeated Breadcrumb breadcrumbs = 10;
310
+ string level = 11;
311
+ }
312
+
313
+ message Meta { string device = 1; string userId = 2; string appVersion = 3; }
314
+ message Batch { Meta meta = 1; repeated Error errors = 2; }
315
+ ```
316
+
317
+ `device` and breadcrumb `data` are objects, so they travel as
318
+ **JSON-stringified strings** inside the protobuf. Empty strings are omitted
319
+ (proto3 default). The binary framing just keeps the network panel tidy — it is
320
+ **not** encryption; the schema is public, so anyone with it can decode a payload.
321
+
322
+ ### How it's transmitted
323
+
324
+ - Normal path: `fetch(endpoint, { method: 'POST', headers: { 'Content-Type':
325
+ 'application/x-protobuf', 'X-Public-Key': <key> }, body: <bytes>, keepalive:
326
+ true })`.
327
+ - Page-hide path: the queue is force-flushed via `navigator.sendBeacon(endpoint
328
+ + '?key=<key>', Blob)` — because a beacon can't set headers, the key rides in
329
+ the `?key=` query param. Falls back to `fetch(..., { keepalive: true })` if
330
+ `sendBeacon` is unavailable or refuses.
331
+
332
+ The API also accepts plain JSON at the same endpoint (same validation), which is
333
+ handy for scripted test posts, but the SDK always sends protobuf.
334
+
335
+ ---
336
+
337
+ ## Viewing and controlling your logs
338
+
339
+ The point of bringing your own endpoint: after the package sends, **you** own the
340
+ data and control it entirely from your dashboard — no third-party console.
341
+
342
+ - **Browse** — open **Logs** on the project to see error groups, each with its
343
+ message, count, first/last seen, affected routes, and per-occurrence stack
344
+ trace + breadcrumb trail (including captured fetch payloads/responses when
345
+ `captureFetchDetails` is on).
346
+ - **Deduplication** — the API groups occurrences by a **fingerprint** (a hash of
347
+ the normalized message + top stack frames), so a recurring error is one row
348
+ with a count, not thousands of duplicates.
349
+ - **Rate limit & origins** — each project has its own per-minute rate limit and
350
+ origin allowlist. Tighten origins to reject stray traffic; the key alone is not
351
+ enough to get data in from an unlisted origin.
352
+ - **Retention** — occurrences are purged after a retention window (7 days by
353
+ default on the server), so storage stays bounded without any action from you.
354
+
355
+ Because the key is write-only and reading is gated by your login, shipping the
356
+ key in your client bundle exposes no stored data.
157
357
 
158
- Per error: `message`, `stack`, `source` (`vue` | `window` | `promise` |
159
- `fetch` | `console`), `url`, `route`, timestamp, and the breadcrumb trail. Once
160
- per batch: `device` info (user agent, language, screen/viewport, connection),
161
- `userId` (read from a `user_id` cookie if present), and `appVersion`. Field
162
- lengths are truncated client-side to match server limits.
358
+ ---
163
359
 
164
- ### Privacy
360
+ ## Privacy
165
361
 
166
- Click breadcrumbs record the element (tag / id / class / short text) **never
167
- input values**. Nothing is captured until an error occurs; breadcrumbs live only
168
- in memory and ship attached to an error.
362
+ Click breadcrumbs record the element (tag / id / class / short text / `data-*`
363
+ attributes) — **never input values**. Nothing is captured until an error occurs;
364
+ breadcrumbs live only in memory and ship attached to an error.
169
365
 
170
366
  Input values are **only** captured when you opt in with `captureInputs: true`.
171
367
  Even then, sensitive fields are masked (stored as `[masked, N chars]`, never in
@@ -173,13 +369,16 @@ the clear): any `type="password"`, plus fields whose `name` / `id` /
173
369
  `autocomplete` match `pass`, `secret`, `token`, `otp`, `cvv`, `card`, `iban`,
174
370
  `ssn`, `pin`, `auth`, etc. Non-sensitive values are truncated to 100 chars.
175
371
 
176
- ## How it sends
372
+ Fetch detail capture (`captureFetchDetails`, on by default) redacts sensitive
373
+ request/response headers (`authorization`, `cookie`, `x-api-key`, CSRF tokens, …)
374
+ and truncates bodies to 2 KB. Set it to `false` if you don't want request
375
+ payloads or response bodies leaving the browser at all.
177
376
 
178
- Payloads are encoded as **protobuf** and POSTed as `application/x-protobuf`. On
179
- page-hide the queue is force-flushed via `navigator.sendBeacon`, falling back to
180
- `fetch(..., { keepalive: true })`. Binary framing keeps the network panel tidy —
181
- it is **not** encryption; the schema is public.
377
+ ---
182
378
 
183
379
  ## License
184
380
 
185
- MIT
381
+ [PolyForm Shield 1.0.0](./LICENSE) — source-available. Free to install and use
382
+ for any purpose **except** building a product that competes with this software or
383
+ with what the licensor offers using it. You may not fork it into a rival. Full
384
+ terms in [`LICENSE`](./LICENSE); copyright remains with the author.
package/README.uk.md ADDED
@@ -0,0 +1,388 @@
1
+ # nuxt-error-tracker
2
+
3
+ [English](./README.md) · **Українська**
4
+
5
+ Легкий трекер frontend-помилок для **Nuxt 3 / 4**. Перехоплює неопрацьовані
6
+ помилки, невдалі запити та ланцюжок дій користувача (breadcrumbs), після чого
7
+ відправляє їх на **ваш власний** Observer ingest-endpoint. Працює лише на
8
+ клієнті, батчами, і збудований так, щоб **ніколи не ламати застосунок-хост** —
9
+ якщо його не налаштовано, він не робить нічого.
10
+
11
+ Ви даєте URL і публічний ключ; пакет робить перехоплення. Усе відправлене
12
+ потрапляє під ваш проєкт у вашому Observer-дашборді, де ви самі переглядаєте,
13
+ шукаєте та керуєте логами.
14
+
15
+ ---
16
+
17
+ ## Зміст
18
+
19
+ - [Як це працює (складові)](#як-це-працює-складові)
20
+ - [Встановлення](#встановлення)
21
+ - [Отримати endpoint і ключ](#отримати-endpoint-і-ключ)
22
+ - [Налаштування](#налаштування)
23
+ - [Конфігурація через змінні середовища](#конфігурація-через-змінні-середовища)
24
+ - [Опції](#опції)
25
+ - [Runtime-хелпери](#runtime-хелпери)
26
+ - [Що відправляється на API](#що-відправляється-на-api)
27
+ - [Формат передачі та типи даних](#формат-передачі-та-типи-даних)
28
+ - [Перегляд і контроль ваших логів](#перегляд-і-контроль-ваших-логів)
29
+ - [Приватність](#приватність)
30
+ - [Ліцензія](#ліцензія)
31
+
32
+ ---
33
+
34
+ ## Як це працює (складові)
35
+
36
+ Складових три. Цей пакет — лише перша.
37
+
38
+ | Складова | Що це | За що відповідає |
39
+ | --- | --- | --- |
40
+ | **Цей пакет** (`nuxt-error-tracker`) | Nuxt-модуль, який ви ставите у застосунок | Підключається до браузера, перехоплює помилки + breadcrumbs, батчить їх і POSTить на ваш endpoint. Працює **лише на клієнті**. |
41
+ | **Ваш ingest-endpoint** (`.../v1/ingest`) | Observer API, який ви хостите (або маєте доступ) | Автентифікує запит за публічним ключем + origin, валідує та rate-лімітує, дедуплікує помилки за fingerprint і зберігає. |
42
+ | **Ваш дашборд** | Консоль Observer | Де **ви** переглядаєте, шукаєте та керуєте логами — стек-трейси, occurrences, breadcrumb-ланцюжки, retention. |
43
+
44
+ Пакет ніколи не вирішує, *куди* йдуть дані — **вирішуєте ви**, вказавши
45
+ `endpoint` на свій хост. Дефолтного URL немає. Змініть хост — логи підуть за
46
+ ним. Це і є «bring your own backend»: той самий пакет працює проти localhost під
47
+ час розробки та проти вашого прод-Observer у релізі, лише зміною endpoint.
48
+
49
+ Усередині самого пакета:
50
+
51
+ - **`module.ts`** — Nuxt-модуль. Читає ваші опції та **реєструє плагін
52
+ перехоплення лише коли `enabled` + `endpoint` + `publicKey` присутні всі**.
53
+ Немає будь-чого → плагін не додається, застосунок працює недоторканим. Також
54
+ нормалізує опції (обрізає `maxBatch ≤ 20`, `maxBreadcrumbs ≤ 50` під серверні
55
+ ліміти) і додає TypeScript-типи для хелперів.
56
+ - **`plugin.client.ts`** — власне рушій перехоплення (лише браузер). Слухає
57
+ помилки, обгортає `fetch`, патчить `console.error`, пише breadcrumbs і керує
58
+ чергою відправки.
59
+ - **`plugin.server.ts`** — маленька SSR-заглушка. Дає no-op `$trackError` /
60
+ `$addBreadcrumb`, щоб виклик під час серверного рендеру ніколи не кидав
61
+ помилку.
62
+ - **`ingest.codec.ts`** — саморобний protobuf-енкодер (без залежностей), що
63
+ перетворює батч на бінарний формат, який очікує API.
64
+
65
+ ---
66
+
67
+ ## Встановлення
68
+
69
+ ```bash
70
+ npm i nuxt-error-tracker
71
+ # або: bun add nuxt-error-tracker
72
+ ```
73
+
74
+ ---
75
+
76
+ ## Отримати endpoint і ключ
77
+
78
+ Модулю потрібні рівно дві речі, щоб почати звітувати: **ingest-endpoint** (ваш
79
+ URL) і **публічний ключ** (якому проєкту належать помилки). Обидва беруться з
80
+ вашого Observer-дашборда.
81
+
82
+ 1. Відкрийте дашборд за адресою `https://<ваш-observer-хост>/dashboard`.
83
+ 2. **Створіть акаунт** — введіть email і пароль (мін. 8 символів), надішліть.
84
+ Ви лишаєтесь залогіненим через захищений httpOnly-cookie.
85
+ 3. Натисніть **New project**, дайте назву (напр. `my-app`) і за бажанням
86
+ перелічіть origins, на яких працює ваш сайт (через кому, напр.
87
+ `https://my-app.com, http://localhost:3000`). Порожньо = приймати будь-який
88
+ origin. Це **серверний allowlist** — запити з інших origins відхиляються.
89
+ 4. Проєкт тепер показує **публічний ключ** (`pk_…`) і готовий сніпет
90
+ `nuxt.config`. Використайте **Copy key** / **Copy config**.
91
+ 5. **Endpoint** — це ваш хост + `/v1/ingest`, тобто
92
+ `https://<ваш-observer-хост>/v1/ingest`.
93
+
94
+ ### Що насправді роблять ці два значення
95
+
96
+ - **`endpoint`** — *куди* відправляти. Це ваш URL. Вказуйте на будь-який хост, де
97
+ крутиться Observer API. Хостованого дефолту немає; ви завжди шлете на свій
98
+ бекенд.
99
+ - **`publicKey`** — *чий проєкт* помилки. На кожному запиті API шукає проєкт за
100
+ цим ключем (заголовок `X-Public-Key` або query `?key=` для beacon-шляху), потім
101
+ застосовує allowlist origins і rate-ліміт цього проєкту. Це **публічне**
102
+ значення — безпечно класти у клієнтські бандли. Воно дає *лише запис* (ingest)
103
+ в один проєкт; воно **не** дає нікому читати ваші логи. Читання вимагає логіну в
104
+ дашборд.
105
+
106
+ > **Origins важливі.** Якщо ви задали allowlist, він мусить містити **кожен**
107
+ > origin, з якого віддається застосунок — прод-домени **та** `http://localhost:3000`
108
+ > для локальної розробки — інакше запити браузера отримають `401`. Лишіть порожнім
109
+ > під час експериментів, звузьте для продакшену.
110
+
111
+ Після налаштування (нижче) і деплою перехоплені помилки з'являються під цим
112
+ проєктом у дашборді. Відкрийте **Logs** на проєкті, щоб їх переглянути.
113
+
114
+ ---
115
+
116
+ ## Налаштування
117
+
118
+ Додайте модуль і вкажіть йому ingest-endpoint з публічним ключем вашого проєкту:
119
+
120
+ ```ts
121
+ // nuxt.config.ts
122
+ export default defineNuxtConfig({
123
+ modules: ['nuxt-error-tracker'],
124
+ errorTracker: {
125
+ endpoint: 'https://<ваш-observer-хост>/v1/ingest',
126
+ publicKey: 'pk_xxxxxxxxxxxxxxxxxxxxxxxx',
127
+ },
128
+ })
129
+ ```
130
+
131
+ Це мінімум. Усе решта має розумні дефолти.
132
+
133
+ ### Немає ключів? Не проблема
134
+
135
+ Якщо `endpoint` чи `publicKey` порожні (або `enabled: false`), плагін не
136
+ реєструється, і застосунок працює недоторканим. Тому модуль можна лишати
137
+ встановленим, підключаючи ключі по-середовищах — dev-збірки без ключа просто не
138
+ трекають. Ні падіння, ні шуму в консолі, ні мережевого виклику.
139
+
140
+ ---
141
+
142
+ ## Конфігурація через змінні середовища
143
+
144
+ Кожну опцію можна задати зі змінних середовища замість хардкоду, за конвенцією
145
+ публічних env у Nuxt `runtimeConfig`:
146
+
147
+ ```ini
148
+ NUXT_PUBLIC_ERROR_TRACKER_ENDPOINT=https://<ваш-observer-хост>/v1/ingest
149
+ NUXT_PUBLIC_ERROR_TRACKER_PUBLIC_KEY=pk_xxxxxxxxxxxxxxxxxxxxxxxx
150
+ NUXT_PUBLIC_ERROR_TRACKER_APP_VERSION=1.4.2
151
+ ```
152
+
153
+ Підключіть їх у `nuxt.config.ts`:
154
+
155
+ ```ts
156
+ export default defineNuxtConfig({
157
+ modules: ['nuxt-error-tracker'],
158
+ runtimeConfig: {
159
+ public: {
160
+ errorTracker: {
161
+ endpoint: process.env.NUXT_PUBLIC_ERROR_TRACKER_ENDPOINT,
162
+ publicKey: process.env.NUXT_PUBLIC_ERROR_TRACKER_PUBLIC_KEY,
163
+ appVersion: process.env.NUXT_PUBLIC_ERROR_TRACKER_APP_VERSION,
164
+ },
165
+ },
166
+ },
167
+ })
168
+ ```
169
+
170
+ Це рекомендований спосіб націлити ту саму збірку на **різний endpoint** за
171
+ середовищем (staging vs. production) без правки коду.
172
+
173
+ ---
174
+
175
+ ## Опції
176
+
177
+ | Опція | Тип | Дефолт | Опис |
178
+ | --------------------- | --------- | ----------- | ------------------------------------------------------------------ |
179
+ | `endpoint` | `string` | `''` | Ingest URL (`.../v1/ingest`). **Обовʼязково** — ваш хост. |
180
+ | `publicKey` | `string` | `''` | Публічний ключ вашого проєкту (`pk_…`). **Обовʼязково**. |
181
+ | `appVersion` | `string` | `'unknown'` | Тегує кожен батч релізом/версією для фільтрації. |
182
+ | `enabled` | `boolean` | `true` | Головний вимикач. `false` = повністю вимкнено, плагін не реєструється. |
183
+ | `captureFetch` | `boolean` | `true` | Обгортає `fetch`, щоб breadcrumbʼити запити і звітувати про збої / 5xx. |
184
+ | `captureFetchDetails` | `boolean` | `true` | Додає повну деталь запиту/відповіді (метод, статус, тайминг, заголовки, тіло запиту, тіло відповіді) до кожного fetch-breadcrumb. Чутливі заголовки редагуються, тіла обрізаються до 2 КБ. `false` — лишити fetch-крихти лише-повідомленням (не захоплювати payload / PII). |
185
+ | `captureConsole` | `boolean` | `true` | Патчить `console.error`, щоб ловити залоговані-але-не-кинуті помилки. |
186
+ | `captureInputs` | `boolean` | `false` | Записує значення полів форм як breadcrumbs (на change/blur). Чутливі поля маскуються. **Opt-in** — захоплює введені користувачем дані. |
187
+ | `flushDelay` | `number` | `2000` | Debounce (мс) перед відправкою черги-батча. |
188
+ | `minFlushInterval` | `number` | `0` | Мін. пауза (мс) між флашами — throttle частоти запитів під час шторму помилок. `0` = вимкнено. |
189
+ | `sampleRate` | `number` | `1` | Частка (0..1) захоплених помилок, що реально шлються; решта дропається при capture. `0.3` ≈ 30%. Ріже обʼєм до серверного rate limit. |
190
+ | `maxPerMinute` | `number` | `0` | Клієнтський ліміт помилок у чергу за rolling-хвилину (fixed window); понад — дроп. Дзеркалить серверний per-project rate limit. `0` = без ліміту. |
191
+ | `maxPerSession` | `number` | `5` | Макс. кількість occurrences *тієї самої* помилки за сесію. |
192
+ | `maxBatch` | `number` | `20` | Макс. помилок на запит (обрізається серверним лімітом 20). |
193
+ | `maxBreadcrumbs` | `number` | `30` | Breadcrumbs у ring buffer / на помилку (обрізається до 50). |
194
+
195
+ ---
196
+
197
+ ## Runtime-хелпери
198
+
199
+ Плагін інʼєктить два хелпери для ручного використання:
200
+
201
+ ```ts
202
+ const { $trackError, $addBreadcrumb } = useNuxtApp()
203
+
204
+ // Звітувати про опрацьовану помилку самому — опційний рівень серйозності
205
+ // ('fatal' | 'error' | 'warning' | 'info' | 'debug', дефолт 'error')
206
+ try {
207
+ await mayFail()
208
+ } catch (e) {
209
+ $trackError(e, 'warning')
210
+ }
211
+
212
+ // Додати власний breadcrumb у ланцюжок
213
+ $addBreadcrumb({
214
+ type: 'custom',
215
+ category: 'checkout',
216
+ message: 'user clicked pay',
217
+ level: 'info',
218
+ })
219
+ ```
220
+
221
+ Обидва хелпери завжди викликаються. Реальне перехоплення — **лише на клієнті**; на
222
+ сервері (SSR — напр. верхньорівневий `<script setup>`) це безпечні no-opʼи, тож
223
+ виклик ніколи не кидає помилку.
224
+
225
+ `$trackError` і `$addBreadcrumb` **повністю типізовані** — модуль постачає
226
+ augmentation для `NuxtApp` і Vue `ComponentCustomProperties`, тож і
227
+ `useNuxtApp()`, і використання в шаблоні бачать їх як функції (без `unknown` /
228
+ TS18046). Ручний `.d.ts` не потрібен.
229
+
230
+ ---
231
+
232
+ ## Що відправляється на API
233
+
234
+ Нічого не відправляється, доки не станеться помилка. Пакет тримає в памʼяті
235
+ ring buffer breadcrumbs, поки користувач взаємодіє; при перехопленні помилки він
236
+ знімає снапшот цього ланцюжка, ставить помилку в чергу і (після debounce) POSTить
237
+ **батч**.
238
+
239
+ Батч має дві частини: **`meta`** (раз на батч, застосовується до всіх помилок у
240
+ ньому) і **`errors`** (масив перехоплених помилок).
241
+
242
+ ### `meta` (раз на батч)
243
+
244
+ | Поле | Тип | Джерело | Нотатки |
245
+ | ------------ | -------- | ------- | ------- |
246
+ | `device` | `object` | `navigator` / `window` | `userAgent`, `language`, `platform`, `screen` (`ШxВ`), `viewport` (`ШxВ`), `online` (bool), `memory` (`deviceMemory`, ГБ або `null`), `connection` (`effectiveType` або `null`). Сервер обрізає JSON до 2 КБ. |
247
+ | `userId` | `string` \| `null` | cookie `user_id` | Читається з cookie `user_id`, якщо є, інакше `null`. Пакет ніколи не вигадує id. Макс. 100 символів. |
248
+ | `appVersion` | `string` | ваша опція `appVersion` | Дефолт `'unknown'`. Макс. 50 символів. |
249
+
250
+ ### `errors[]` (на кожну перехоплену помилку)
251
+
252
+ | Поле | Тип | Опис | Ліміт |
253
+ | ------------- | --------- | ---- | ----- |
254
+ | `message` | `string` | Повідомлення помилки. | ≤ 1000 симв. |
255
+ | `stack` | `string` | Стек-трейс. | ≤ 8000 симв. |
256
+ | `source` | `string` | Звідки: `vue`, `window`, `promise`, `fetch` чи `console`. | ≤ 20 симв. |
257
+ | `level` | `string` | Серйозність: `fatal` \| `error` \| `warning` \| `info` \| `debug`. | одне зі списку |
258
+ | `url` | `string` | `window.location.href` на момент перехоплення. | ≤ 2000 симв. |
259
+ | `route` | `string` | Nuxt-route `fullPath`. | ≤ 500 симв. |
260
+ | `occurredAt` | `string` | ISO-8601 timestamp. | ISO-8601 |
261
+ | `breadcrumbs` | `Breadcrumb[]` | Ланцюжок, що привів до помилки (нижче). | ≤ 50 елементів |
262
+
263
+ Кожен **`Breadcrumb`**:
264
+
265
+ | Поле | Тип | Опис | Ліміт |
266
+ | ----------- | -------- | ---- | ----- |
267
+ | `type` | `string` | `click`, `input`, `navigation`, `fetch`, `console` чи ваш кастомний тип. | ≤ 30 симв. |
268
+ | `category` | `string` | напр. `ui.click`, `ui.input`, `navigation`, `http`, `console`. | ≤ 60 симв. |
269
+ | `message` | `string` | Людиночитний рядок, напр. `POST /api/pay → 500`. | ≤ 500 симв. |
270
+ | `level` | `string` | `info` \| `error` \| … | ≤ 20 симв. |
271
+ | `timestamp` | `string` | ISO-8601. | ISO-8601 |
272
+ | `data` | `object` | Опційний структурований payload (деталі fetch: метод, статус, тайминг, заголовки, тіла запиту/відповіді). | JSON |
273
+
274
+ **Ліміти батча:** щонайбільше **20 помилок на запит**; клієнт також повертає в
275
+ чергу невдалі батчі (обмежено 200 у памʼяті) і дедуплікує *ту саму* помилку до
276
+ `maxPerSession` (5) occurrences за сесію, тож одна повторювана помилка не
277
+ затопить вашу квоту.
278
+
279
+ ---
280
+
281
+ ## Формат передачі та типи даних
282
+
283
+ Payloadʼи кодуються у **protobuf** (proto3) і POSTяться як
284
+ `application/x-protobuf`. Енкодер саморобний і **без залежностей** — жодного
285
+ `protobufjs`, тож споживачам не потрібні ні Vite `optimizeDeps`, ні
286
+ CJS-interop-конфіг.
287
+
288
+ Схема (номери полів визначальні — вони мусять збігатися з декодером API):
289
+
290
+ ```proto
291
+ message Breadcrumb {
292
+ string type = 1;
293
+ string category = 2;
294
+ string message = 3;
295
+ string level = 4;
296
+ string timestamp = 5;
297
+ string data = 6; // обʼєкт у вигляді JSON-рядка
298
+ }
299
+
300
+ message Error {
301
+ string message = 1;
302
+ string stack = 2;
303
+ string source = 3;
304
+ string url = 4;
305
+ string route = 5;
306
+ string userId = 6; // per-error override (цим клієнтом не використовується)
307
+ string appVersion = 7; // per-error override (цим клієнтом не використовується)
308
+ string device = 8; // per-error override (цим клієнтом не використовується)
309
+ string occurredAt = 9;
310
+ repeated Breadcrumb breadcrumbs = 10;
311
+ string level = 11;
312
+ }
313
+
314
+ message Meta { string device = 1; string userId = 2; string appVersion = 3; }
315
+ message Batch { Meta meta = 1; repeated Error errors = 2; }
316
+ ```
317
+
318
+ `device` і breadcrumb `data` — обʼєкти, тож їдуть як **JSON-рядки** всередині
319
+ protobuf. Порожні рядки опускаються (дефолт proto3). Бінарне обрамлення лише
320
+ тримає мережеву панель охайною — це **не** шифрування; схема публічна, тож
321
+ будь-хто з нею може декодувати payload.
322
+
323
+ ### Як передається
324
+
325
+ - Звичайний шлях: `fetch(endpoint, { method: 'POST', headers: { 'Content-Type':
326
+ 'application/x-protobuf', 'X-Public-Key': <ключ> }, body: <bytes>, keepalive:
327
+ true })`.
328
+ - Шлях page-hide: черга примусово скидається через `navigator.sendBeacon(endpoint
329
+ + '?key=<ключ>', Blob)` — оскільки beacon не може ставити заголовки, ключ їде в
330
+ query-параметрі `?key=`. Відкат до `fetch(..., { keepalive: true })`, якщо
331
+ `sendBeacon` недоступний чи відмовив.
332
+
333
+ API також приймає звичайний JSON на тому самому endpoint (та сама валідація), що
334
+ зручно для скриптових тестових POSTів, але SDK завжди шле protobuf.
335
+
336
+ ---
337
+
338
+ ## Перегляд і контроль ваших логів
339
+
340
+ Сенс власного endpoint: після відправки пакетом **ви** володієте даними і керуєте
341
+ ними повністю зі свого дашборда — без сторонньої консолі.
342
+
343
+ - **Перегляд** — відкрийте **Logs** на проєкті, щоб побачити групи помилок, кожну
344
+ з повідомленням, лічильником, first/last seen, зачепленими routes і
345
+ стек-трейсом + breadcrumb-ланцюжком на кожну occurrence (включно з захопленими
346
+ fetch payload/response, коли `captureFetchDetails` увімкнено).
347
+ - **Дедуплікація** — API групує occurrences за **fingerprint** (хеш нормалізованого
348
+ повідомлення + верхні стек-фрейми), тож повторювана помилка — це один рядок із
349
+ лічильником, а не тисячі дублікатів.
350
+ - **Rate-ліміт і origins** — кожен проєкт має власний per-minute rate-ліміт і
351
+ allowlist origins. Звузьте origins, щоб відкидати сторонній трафік; самого
352
+ ключа недостатньо, щоб дані зайшли з неперелічeного origin.
353
+ - **Retention** — occurrences видаляються після вікна retention (за замовчуванням
354
+ 7 днів на сервері), тож сховище лишається обмеженим без жодних дій з вашого
355
+ боку.
356
+
357
+ Оскільки ключ — лише на запис, а читання під логіном, публікація ключа у
358
+ клієнтському бандлі не відкриває жодних збережених даних.
359
+
360
+ ---
361
+
362
+ ## Приватність
363
+
364
+ Click-breadcrumbs записують елемент (tag / id / class / короткий текст / `data-*`
365
+ атрибути) — **ніколи значення полів**. Нічого не захоплюється, доки не станеться
366
+ помилка; breadcrumbs живуть лише в памʼяті і їдуть прикріпленими до помилки.
367
+
368
+ Значення полів захоплюються **лише** коли ви явно вмикаєте `captureInputs: true`.
369
+ Навіть тоді чутливі поля маскуються (зберігаються як `[masked, N chars]`, ніколи
370
+ у відкритому вигляді): будь-який `type="password"`, а також поля, чиї `name` /
371
+ `id` / `autocomplete` збігаються з `pass`, `secret`, `token`, `otp`, `cvv`,
372
+ `card`, `iban`, `ssn`, `pin`, `auth` тощо. Нечутливі значення обрізаються до 100
373
+ символів.
374
+
375
+ Захоплення деталей fetch (`captureFetchDetails`, увімкнено за замовчуванням)
376
+ редагує чутливі заголовки запиту/відповіді (`authorization`, `cookie`,
377
+ `x-api-key`, CSRF-токени, …) і обрізає тіла до 2 КБ. Поставте `false`, якщо не
378
+ хочете, щоб payloadʼи запитів чи тіла відповідей взагалі покидали браузер.
379
+
380
+ ---
381
+
382
+ ## Ліцензія
383
+
384
+ [PolyForm Shield 1.0.0](./LICENSE) — source-available. Безкоштовно ставити й
385
+ використовувати для будь-якої мети, **окрім** створення продукту, що конкурує з
386
+ цим ПЗ або з тим, що ліцензіар надає з його допомогою. Не можна форкнути у
387
+ конкурента. Повні умови у [`LICENSE`](./LICENSE); авторські права лишаються за
388
+ автором.
package/dist/module.d.mts CHANGED
@@ -6,12 +6,11 @@ interface ModuleOptions {
6
6
  /** Wrap window.fetch to capture failed requests (network errors + 5xx). Default true. */
7
7
  captureFetch?: boolean;
8
8
  /**
9
- * On a failed request (HTTP >= 400 or a network error), attach full request/
10
- * response detail (method, url, status, timing, headers, bodies) to the fetch
11
- * breadcrumb's `data` for precise debugging. Sensitive headers (authorization/
12
- * cookie/…) are redacted; bodies are truncated. Only failed requests carry
13
- * detail successful ones stay lightweight. Default true; set false to keep
14
- * fetch breadcrumbs message-only (e.g. to avoid capturing request payloads).
9
+ * Attach full request/response detail (method, url, status, timing, headers,
10
+ * request payload, response body) to every fetch breadcrumb's `data` for
11
+ * precise debugging. Sensitive headers (authorization/cookie/…) are redacted;
12
+ * bodies are truncated. Default true; set false to keep fetch breadcrumbs
13
+ * message-only (e.g. to avoid capturing request payloads / PII).
15
14
  */
16
15
  captureFetchDetails?: boolean;
17
16
  /** Patch console.error to capture logged-but-not-thrown errors. Default true. */
@@ -22,8 +21,34 @@ interface ModuleOptions {
22
21
  * masked. Default false — opt in, since it can capture user-entered data.
23
22
  */
24
23
  captureInputs?: boolean;
25
- /** Debounce before a batch is flushed, in ms. Default 2000. */
24
+ /**
25
+ * How often batches leave the browser (debounce before a flush), in ms.
26
+ * A flush is scheduled `flushDelay` after the first queued error; more errors
27
+ * arriving in that window ride the same batch. Default 2000.
28
+ */
26
29
  flushDelay?: number;
30
+ /**
31
+ * Floor on the spacing between consecutive flushes, in ms. Unlike `flushDelay`
32
+ * (a per-batch debounce), this throttles the *rate* of network requests during
33
+ * a sustained error storm: a flush never fires sooner than `minFlushInterval`
34
+ * after the previous one. Default 0 (off — `flushDelay` alone governs cadence).
35
+ */
36
+ minFlushInterval?: number;
37
+ /**
38
+ * Client-side sampling: fraction (0..1) of captured errors to actually send;
39
+ * the rest are dropped before they ever queue. 1 = send everything (default),
40
+ * 0.3 = keep ~30%. Thins volume at the source so a high-traffic app doesn't
41
+ * flood the per-project server rate limit. Applied per occurrence, before the
42
+ * per-session dedup — so with a low rate even a rare error may be dropped.
43
+ */
44
+ sampleRate?: number;
45
+ /**
46
+ * Client-side hard cap on errors *queued* per rolling minute (fixed window);
47
+ * anything over is dropped. Mirrors the server's per-project rate limit so the
48
+ * browser stops emitting instead of shipping batches that get rejected.
49
+ * Default 0 (off — unlimited).
50
+ */
51
+ maxPerMinute?: number;
27
52
  /** Max occurrences of the same error captured per session. Default 5. */
28
53
  maxPerSession?: number;
29
54
  /** Max errors per flushed batch. Default 20 (server also caps at 20). */
package/dist/module.d.ts CHANGED
@@ -6,12 +6,11 @@ interface ModuleOptions {
6
6
  /** Wrap window.fetch to capture failed requests (network errors + 5xx). Default true. */
7
7
  captureFetch?: boolean;
8
8
  /**
9
- * On a failed request (HTTP >= 400 or a network error), attach full request/
10
- * response detail (method, url, status, timing, headers, bodies) to the fetch
11
- * breadcrumb's `data` for precise debugging. Sensitive headers (authorization/
12
- * cookie/…) are redacted; bodies are truncated. Only failed requests carry
13
- * detail successful ones stay lightweight. Default true; set false to keep
14
- * fetch breadcrumbs message-only (e.g. to avoid capturing request payloads).
9
+ * Attach full request/response detail (method, url, status, timing, headers,
10
+ * request payload, response body) to every fetch breadcrumb's `data` for
11
+ * precise debugging. Sensitive headers (authorization/cookie/…) are redacted;
12
+ * bodies are truncated. Default true; set false to keep fetch breadcrumbs
13
+ * message-only (e.g. to avoid capturing request payloads / PII).
15
14
  */
16
15
  captureFetchDetails?: boolean;
17
16
  /** Patch console.error to capture logged-but-not-thrown errors. Default true. */
@@ -22,8 +21,34 @@ interface ModuleOptions {
22
21
  * masked. Default false — opt in, since it can capture user-entered data.
23
22
  */
24
23
  captureInputs?: boolean;
25
- /** Debounce before a batch is flushed, in ms. Default 2000. */
24
+ /**
25
+ * How often batches leave the browser (debounce before a flush), in ms.
26
+ * A flush is scheduled `flushDelay` after the first queued error; more errors
27
+ * arriving in that window ride the same batch. Default 2000.
28
+ */
26
29
  flushDelay?: number;
30
+ /**
31
+ * Floor on the spacing between consecutive flushes, in ms. Unlike `flushDelay`
32
+ * (a per-batch debounce), this throttles the *rate* of network requests during
33
+ * a sustained error storm: a flush never fires sooner than `minFlushInterval`
34
+ * after the previous one. Default 0 (off — `flushDelay` alone governs cadence).
35
+ */
36
+ minFlushInterval?: number;
37
+ /**
38
+ * Client-side sampling: fraction (0..1) of captured errors to actually send;
39
+ * the rest are dropped before they ever queue. 1 = send everything (default),
40
+ * 0.3 = keep ~30%. Thins volume at the source so a high-traffic app doesn't
41
+ * flood the per-project server rate limit. Applied per occurrence, before the
42
+ * per-session dedup — so with a low rate even a rare error may be dropped.
43
+ */
44
+ sampleRate?: number;
45
+ /**
46
+ * Client-side hard cap on errors *queued* per rolling minute (fixed window);
47
+ * anything over is dropped. Mirrors the server's per-project rate limit so the
48
+ * browser stops emitting instead of shipping batches that get rejected.
49
+ * Default 0 (off — unlimited).
50
+ */
51
+ maxPerMinute?: number;
27
52
  /** Max occurrences of the same error captured per session. Default 5. */
28
53
  maxPerSession?: number;
29
54
  /** Max errors per flushed batch. Default 20 (server also caps at 20). */
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.17",
4
+ "version": "0.1.19",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "0.8.4",
7
7
  "unbuild": "2.0.0"
package/dist/module.mjs CHANGED
@@ -4,6 +4,14 @@ function posInt(value, fallback) {
4
4
  const n = Number(value);
5
5
  return Number.isFinite(n) && n > 0 ? Math.floor(n) : fallback;
6
6
  }
7
+ function clamp01(value, fallback) {
8
+ const n = Number(value);
9
+ return Number.isFinite(n) ? Math.min(Math.max(n, 0), 1) : fallback;
10
+ }
11
+ function nonNegInt(value, fallback) {
12
+ const n = Number(value);
13
+ return Number.isFinite(n) && n >= 0 ? Math.floor(n) : fallback;
14
+ }
7
15
  const module = defineNuxtModule({
8
16
  meta: {
9
17
  name: "nuxt-error-tracker",
@@ -19,6 +27,9 @@ const module = defineNuxtModule({
19
27
  captureConsole: true,
20
28
  captureInputs: false,
21
29
  flushDelay: 2e3,
30
+ minFlushInterval: 0,
31
+ sampleRate: 1,
32
+ maxPerMinute: 0,
22
33
  maxPerSession: 5,
23
34
  maxBatch: 20,
24
35
  maxBreadcrumbs: 30
@@ -35,6 +46,9 @@ const module = defineNuxtModule({
35
46
  captureConsole: options.captureConsole !== false,
36
47
  captureInputs: options.captureInputs === true,
37
48
  flushDelay: posInt(options.flushDelay, 2e3),
49
+ minFlushInterval: nonNegInt(options.minFlushInterval, 0),
50
+ sampleRate: clamp01(options.sampleRate, 1),
51
+ maxPerMinute: nonNegInt(options.maxPerMinute, 0),
38
52
  maxPerSession: posInt(options.maxPerSession, 5),
39
53
  // Server hard-caps batches at 20 — never emit larger, even if misconfigured.
40
54
  maxBatch: Math.min(posInt(options.maxBatch, 20), 20),
@@ -11,12 +11,29 @@ export default defineNuxtPlugin((nuxtApp) => {
11
11
  if (!config || !config.endpoint || !config.publicKey) return;
12
12
  const route = useRoute();
13
13
  const FLUSH_DELAY = config.flushDelay ?? 2e3;
14
+ const MIN_FLUSH_INTERVAL = config.minFlushInterval ?? 0;
15
+ const SAMPLE_RATE = config.sampleRate ?? 1;
16
+ const MAX_PER_MINUTE = config.maxPerMinute ?? 0;
14
17
  const MAX_PER_SESSION = config.maxPerSession ?? 5;
15
18
  const MAX_BATCH = config.maxBatch ?? 20;
16
19
  const MAX_CRUMBS = config.maxBreadcrumbs ?? 30;
17
20
  const queue = [];
18
21
  const sessionCounts = /* @__PURE__ */ new Map();
19
22
  let flushTimer = null;
23
+ let lastFlushAt = 0;
24
+ let rateWindowStart = 0;
25
+ let rateWindowCount = 0;
26
+ function rateLimited() {
27
+ if (!MAX_PER_MINUTE) return false;
28
+ const now = Date.now();
29
+ if (now - rateWindowStart >= 6e4) {
30
+ rateWindowStart = now;
31
+ rateWindowCount = 0;
32
+ }
33
+ if (rateWindowCount >= MAX_PER_MINUTE) return true;
34
+ rateWindowCount++;
35
+ return false;
36
+ }
20
37
  const crumbs = [];
21
38
  function addCrumb(c) {
22
39
  crumbs.push({ ...c, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
@@ -43,6 +60,7 @@ export default defineNuxtPlugin((nuxtApp) => {
43
60
  const MAX_QUEUE = 200;
44
61
  function flush(useBeacon = false) {
45
62
  if (!queue.length) return;
63
+ lastFlushAt = Date.now();
46
64
  try {
47
65
  const batch = queue.splice(0, MAX_BATCH);
48
66
  const body = {
@@ -78,18 +96,22 @@ export default defineNuxtPlugin((nuxtApp) => {
78
96
  }
79
97
  function scheduleFlush() {
80
98
  if (flushTimer) return;
99
+ const sinceLast = Date.now() - lastFlushAt;
100
+ const wait = Math.max(FLUSH_DELAY, MIN_FLUSH_INTERVAL - sinceLast);
81
101
  flushTimer = setTimeout(() => {
82
102
  flushTimer = null;
83
103
  flush();
84
- }, FLUSH_DELAY);
104
+ }, wait);
85
105
  }
86
106
  function capture(err, source, level = "error") {
87
107
  try {
88
108
  const error = err instanceof Error ? err : new Error(String(err));
89
109
  if (error.message === "Script error.") return;
110
+ if (SAMPLE_RATE < 1 && Math.random() >= SAMPLE_RATE) return;
90
111
  const key = sessionKey(error.message, error.stack ?? "");
91
112
  const count = sessionCounts.get(key) ?? 0;
92
113
  if (count >= MAX_PER_SESSION) return;
114
+ if (rateLimited()) return;
93
115
  sessionCounts.set(key, count + 1);
94
116
  queue.push({
95
117
  message: error.message.slice(0, 1e3),
@@ -266,7 +288,7 @@ export default defineNuxtPlugin((nuxtApp) => {
266
288
  if (!isOwn) {
267
289
  const failed = res.status >= 400;
268
290
  let data;
269
- if (failed && config.captureFetchDetails) {
291
+ if (config.captureFetchDetails) {
270
292
  data = {
271
293
  method,
272
294
  url: reqUrl,
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "nuxt-error-tracker",
3
- "version": "0.1.17",
3
+ "version": "0.1.19",
4
4
  "type": "module",
5
+ "license": "PolyForm-Shield-1.0.0",
5
6
  "exports": {
6
7
  ".": {
7
8
  "types": "./dist/types.d.ts",