@backtrack-js/browser 0.5.0 → 0.5.2
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 +54 -396
- package/dev-server/package.json +6 -0
- package/dist/dev-server/index.d.ts +2 -0
- package/dist/dev-server/index.d.ts.map +1 -0
- package/dist/dev-server/middleware.d.ts +14 -0
- package/dist/dev-server/middleware.d.ts.map +1 -0
- package/dist/dev-server.cjs +7 -0
- package/dist/dev-server.cjs.map +1 -0
- package/dist/dev-server.js +173 -0
- package/dist/dev-server.js.map +1 -0
- package/dist/index.cjs +34 -36
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1542 -1889
- package/dist/index.js.map +1 -1
- package/dist/markdown-Ao5Ah9MF.cjs +9 -0
- package/dist/markdown-Ao5Ah9MF.cjs.map +1 -0
- package/dist/markdown-D3C94OlY.js +494 -0
- package/dist/markdown-D3C94OlY.js.map +1 -0
- package/dist/storage/db.d.ts.map +1 -1
- package/dist/types/incident.d.ts +1 -0
- package/dist/types/incident.d.ts.map +1 -1
- package/dist/utils/markdown.d.ts +11 -3
- package/dist/utils/markdown.d.ts.map +1 -1
- package/dist/viewer/assets/index-092olPra.css +1 -0
- package/dist/viewer/assets/index-D8m3bwRf.js +197 -0
- package/dist/viewer/index.html +2 -2
- package/dist/widget/widget.d.ts.map +1 -1
- package/package.json +9 -1
- package/dist/viewer/assets/index-CRrpVNC0.js +0 -192
- package/dist/viewer/assets/index-cVrM07fr.css +0 -1
package/README.md
CHANGED
|
@@ -1,396 +1,54 @@
|
|
|
1
|
-
# Backtrack
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
##
|
|
39
|
-
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
npm
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
```typescript
|
|
56
|
-
import { Backtrack } from '@backtrack-js/browser';
|
|
57
|
-
|
|
58
|
-
const recorder = new Backtrack({
|
|
59
|
-
bufferMinutes: 5, // keep the last 5 minutes in the rolling buffer
|
|
60
|
-
afterErrorSeconds: 15, // keep recording 15s after an error
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
await recorder.start();
|
|
64
|
-
|
|
65
|
-
// Optional: capture manually, e.g. from a "Report a problem" button
|
|
66
|
-
const incidentId = await recorder.capture('User reported payment bug', 30);
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
Then inspect it:
|
|
70
|
-
|
|
71
|
-
```bash
|
|
72
|
-
npx backtrack
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
Drag an exported incident `.json` into the viewer and hit play.
|
|
76
|
-
|
|
77
|
-
---
|
|
78
|
-
|
|
79
|
-
## Readable production stack traces (`backtrack symbolize`)
|
|
80
|
-
|
|
81
|
-
Production bundles are minified, so a stack frame like `app.3f9a1c.js:1:48213` tells you nothing. `backtrack symbolize` reads your source maps and rewrites the incident's errors with the original file, line, column and the surrounding source code.
|
|
82
|
-
|
|
83
|
-
```bash
|
|
84
|
-
npx backtrack symbolize <incident> --sourcemaps <directory> [options]
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
| Argument / option | Description |
|
|
88
|
-
| :--- | :--- |
|
|
89
|
-
| `<incident>` | Path to an exported incident: `.json`, `.ffr.json`, `.gz` or `.ffr.json.gz`. Gzip is detected automatically. |
|
|
90
|
-
| `--sourcemaps <directory>` | Directory containing your `.js.map`, `.mjs.map` and `.cjs.map` files. Searched recursively (e.g. `./dist`, `./build`). |
|
|
91
|
-
| `--output <file>` | Custom output path. Defaults to `<name>.symbolicated.ffr.json` next to the original. The output can never overwrite the input file. |
|
|
92
|
-
| `--release <id>` | Build or release identifier. Validates against the incident's recorded release (if present), warning on mismatch. |
|
|
93
|
-
| `--context-lines <number>` | Lines of original source shown before and after the failing line. Range 0–10, default 3. |
|
|
94
|
-
| `-h, --help` | Show help. |
|
|
95
|
-
|
|
96
|
-
### Example
|
|
97
|
-
|
|
98
|
-
```bash
|
|
99
|
-
npx backtrack symbolize ./incidents/checkout-bug.ffr.json --sourcemaps ./dist --context-lines 5
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
```text
|
|
103
|
-
[Backtrack] Symbolication concluída.
|
|
104
|
-
|
|
105
|
-
Incidente: inc_1727500000_3f2a
|
|
106
|
-
Erros encontrados: 1
|
|
107
|
-
Frames da exceção resolvidos: 4
|
|
108
|
-
Frames da exceção não resolvidos: 0
|
|
109
|
-
Frames React resolvidos: 2
|
|
110
|
-
Frames React não resolvidos: 0
|
|
111
|
-
Saída: /projects/app/incidents/checkout-bug.symbolicated.ffr.json
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
If some frames have no matching source map, the command reports a partial symbolication with warnings instead of failing.
|
|
115
|
-
|
|
116
|
-
Open the `.symbolicated.ffr.json` file in the viewer (`npx backtrack`) and the error tab shows the resolved stack trace, with the failing line highlighted:
|
|
117
|
-
|
|
118
|
-
```text
|
|
119
|
-
applyCoupon src/cart/coupon.ts:42:18
|
|
120
|
-
|
|
121
|
-
40 function applyCoupon(cart, coupon) {
|
|
122
|
-
41 // coupon discount might be missing from API
|
|
123
|
-
42 const off = coupon.discount.percent; <-- highlighted
|
|
124
|
-
43 return cart.total * (1 - off / 100);
|
|
125
|
-
44 }
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
### What it does (and doesn't do)
|
|
129
|
-
|
|
130
|
-
- **Validates safely:** Reads the incident (up to 50 MB, gzip supported) and validates its schema with a strict validator. The original file is never modified.
|
|
131
|
-
- **Multi-browser stack parsing:** Supports V8 (Chrome/Edge), Firefox, and Safari stack formats, stripping URL hashes/query strings and skipping native frames (`[native code]`).
|
|
132
|
-
- **Resolves both exception location and React tree:** Simboliza tanto o ponto onde a exceção foi lançada (`resolvedStack`) quanto a árvore de componentes React (`resolvedComponentStack`), mantendo-os estritamente separados.
|
|
133
|
-
- **Sourcemap validation:** Checks the sourcemap `file` attribute against the minified bundle to prevent cross-bundle misattribution.
|
|
134
|
-
- **Extracts source context:** Extracts the original code from `sourcesContent`, so your source maps must include it.
|
|
135
|
-
- **Doesn't leak local paths:** File paths are normalized (e.g. `C:\Users\alice\...` or `/home/bob/...` become paths starting at `src/`).
|
|
136
|
-
- **Atomic write:** Writes the result atomically (to a `.tmp` file, then renames) and stores it in `resolvedStack` and `resolvedComponentStack`.
|
|
137
|
-
|
|
138
|
-
### Limitations & Caveats
|
|
139
|
-
|
|
140
|
-
- **Frames without line/column:** Stack frames missing line or column numbers cannot be mapped to source locations.
|
|
141
|
-
- **Matching sourcemap release:** Source maps must correspond to the exact production build that generated the incident. Use `--release <id>` to ensure compatibility.
|
|
142
|
-
- **Privacy notice:** O artefato poderá incluir replay visual, URLs, logs, respostas de rede, anotações e trechos do código-fonte incorporados durante a symbolication.
|
|
143
|
-
|
|
144
|
-
---
|
|
145
|
-
|
|
146
|
-
## Privacy & Data Masking
|
|
147
|
-
|
|
148
|
-
Backtrack is designed with strict security defaults:
|
|
149
|
-
|
|
150
|
-
- **Inputs & Forms:** All `<input>`, `<textarea>`, and `<select>` values are masked by default (`maskAllInputs: true`).
|
|
151
|
-
- **Network Headers:** Authorization, Cookies, API keys, and Bearer tokens are automatically redacted.
|
|
152
|
-
- **Sensitive Selectors:** Use standard selectors or CSS classes to protect proprietary or personal data:
|
|
153
|
-
|
|
154
|
-
```typescript
|
|
155
|
-
const recorder = new Backtrack({
|
|
156
|
-
privacy: {
|
|
157
|
-
maskAllInputs: true,
|
|
158
|
-
blockMedia: true,
|
|
159
|
-
blockSelector: '.backtrack-block, [data-private]', // Completely hides elements in replay
|
|
160
|
-
maskTextSelector: '.backtrack-mask, .customer-pII', // Scrambles text content
|
|
161
|
-
sanitizeUrl: (url) => {
|
|
162
|
-
// Remove sensitive query parameters
|
|
163
|
-
url.searchParams.delete('token');
|
|
164
|
-
return url.toString();
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
});
|
|
168
|
-
```
|
|
169
|
-
|
|
170
|
-
---
|
|
171
|
-
|
|
172
|
-
## Network & Payload Capture
|
|
173
|
-
|
|
174
|
-
Network requests and responses can be configured using `network` options:
|
|
175
|
-
|
|
176
|
-
```typescript
|
|
177
|
-
const recorder = new Backtrack({
|
|
178
|
-
network: {
|
|
179
|
-
capturePayloads: false, // Bodies are disabled by default for privacy
|
|
180
|
-
maxPayloadSize: 64 * 1024
|
|
181
|
-
}
|
|
182
|
-
});
|
|
183
|
-
```
|
|
184
|
-
|
|
185
|
-
- **Bodies are not captured by default (`capturePayloads: false`):** Request and response bodies are disabled by default to prevent unintentional data leakage. HTTP method, URL, status, timing, and sanitized headers continue to be captured.
|
|
186
|
-
- **Enabling bodies increases the risk of PII:** Enabling `capturePayloads: true` should be done mindfully according to your compliance and privacy requirements.
|
|
187
|
-
- **Sanitization remains active:** Even when payload capture is explicitly enabled, Backtrack continues to sanitize and redact known sensitive keys (passwords, tokens, credentials, etc.) and patterns (Bearer tokens, emails, CPFs, cards).
|
|
188
|
-
- **`maxPayloadSize` is a limit per payload, not per incident:** It bounds each individual request or response body (default is `64 * 1024` bytes). Payloads exceeding this limit are truncated safely with a truncation marker.
|
|
189
|
-
|
|
190
|
-
---
|
|
191
|
-
|
|
192
|
-
## Capturing Incidents
|
|
193
|
-
|
|
194
|
-
### Automatic Triggers
|
|
195
|
-
Backtrack automatically triggers and freezes an incident when:
|
|
196
|
-
1. An unhandled window exception occurs (`window.onerror`).
|
|
197
|
-
2. An unhandled promise rejection occurs (`unhandledrejection`).
|
|
198
|
-
3. An HTTP request returns a matching error status (`captureHttpStatus`).
|
|
199
|
-
|
|
200
|
-
### Manual Capture
|
|
201
|
-
You can trigger incident recording manually anywhere in your code (e.g. from an error boundary or user feedback modal):
|
|
202
|
-
|
|
203
|
-
```typescript
|
|
204
|
-
// Capture the last 60 seconds of context
|
|
205
|
-
const incidentId = await recorder.capture('Checkout error', 60);
|
|
206
|
-
|
|
207
|
-
// Export the complete incident artifact as a JSON object
|
|
208
|
-
const artifact = await recorder.exportIncident(incidentId);
|
|
209
|
-
|
|
210
|
-
// Download or send artifact to your support/logging service
|
|
211
|
-
const jsonBlob = new Blob([JSON.stringify(artifact, null, 2)], { type: 'application/json' });
|
|
212
|
-
```
|
|
213
|
-
|
|
214
|
-
### Exception Capture
|
|
215
|
-
|
|
216
|
-
#### React Error Boundary
|
|
217
|
-
Em Error Boundaries (ex: `componentDidCatch` ou `react-error-boundary`), forneça `source: 'react'` e a árvore de componentes `componentStack`:
|
|
218
|
-
|
|
219
|
-
```typescript
|
|
220
|
-
// React Error Boundary (captura renderização/lifecycle com árvore de componentes)
|
|
221
|
-
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
|
222
|
-
recorder.captureException(error, {
|
|
223
|
-
source: 'react',
|
|
224
|
-
componentStack: errorInfo.componentStack
|
|
225
|
-
});
|
|
226
|
-
}
|
|
227
|
-
```
|
|
228
|
-
|
|
229
|
-
#### Captura manual (try / catch)
|
|
230
|
-
Para exceções capturadas imperativamente em handlers de eventos, chamadas assíncronas ou blocos `try/catch`, use `source: 'manual'`:
|
|
231
|
-
|
|
232
|
-
```typescript
|
|
233
|
-
// Captura imperativa sem árvore de componentes React
|
|
234
|
-
try {
|
|
235
|
-
processCheckout();
|
|
236
|
-
} catch (err) {
|
|
237
|
-
recorder.captureException(err, {
|
|
238
|
-
source: 'manual'
|
|
239
|
-
});
|
|
240
|
-
}
|
|
241
|
-
```
|
|
242
|
-
|
|
243
|
-
> **Atenção:** Use `source: 'react'` apenas quando o erro foi capturado por um Error Boundary com a árvore de renderização. O visualizador e o simbolizador tratam `source: 'react'` e `componentStack` como a árvore de componentes da interface, e a stack JavaScript comum como o local exato do disparo da exceção.
|
|
244
|
-
|
|
245
|
-
---
|
|
246
|
-
|
|
247
|
-
## Remote Transport & beforeSend
|
|
248
|
-
|
|
249
|
-
Backtrack allows applications to automatically dispatch finalized incidents to any remote endpoint or telemetry backend using `transport` and `beforeSend`:
|
|
250
|
-
|
|
251
|
-
```typescript
|
|
252
|
-
const recorder = new Backtrack({
|
|
253
|
-
transport: {
|
|
254
|
-
async send(artifact) {
|
|
255
|
-
const response = await fetch('/api/backtrack/incidents', {
|
|
256
|
-
method: 'POST',
|
|
257
|
-
headers: {
|
|
258
|
-
'content-type': 'application/json'
|
|
259
|
-
},
|
|
260
|
-
body: JSON.stringify(artifact)
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
if (!response.ok) {
|
|
264
|
-
throw new Error(`Upload failed: ${response.status}`);
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
},
|
|
268
|
-
|
|
269
|
-
beforeSend(artifact) {
|
|
270
|
-
if (artifact.environment.url.includes('/healthcheck')) {
|
|
271
|
-
return null;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
return artifact;
|
|
275
|
-
}
|
|
276
|
-
});
|
|
277
|
-
```
|
|
278
|
-
|
|
279
|
-
### Dispatch Flow & Semantics
|
|
280
|
-
|
|
281
|
-
- **Sent Only When Finalized:** Incidents are dispatched only after they have been completely finalized and safely written to IndexedDB.
|
|
282
|
-
```text
|
|
283
|
-
incident finalized
|
|
284
|
-
↓
|
|
285
|
-
build artifact
|
|
286
|
-
↓
|
|
287
|
-
beforeSend
|
|
288
|
-
↓
|
|
289
|
-
null? cancel upload
|
|
290
|
-
↓
|
|
291
|
-
transport.send
|
|
292
|
-
```
|
|
293
|
-
- **Role of `beforeSend`:**
|
|
294
|
-
- Affects **only** the automated transmission via `transport.send`.
|
|
295
|
-
- Does **not** mutate the artifact stored in local IndexedDB.
|
|
296
|
-
- Does **not** affect `getArtifact()` or explicit file downloads via `exportIncident()`.
|
|
297
|
-
- Returning `null` cancels the upload.
|
|
298
|
-
- If `beforeSend` throws an error, the upload is aborted cleanly without affecting the application or the recorded incident.
|
|
299
|
-
- **Fault Isolation:** Network or transport errors will never break the host application, reject user captures, delete local incidents, prevent finalization, or trigger unhandled promise rejections.
|
|
300
|
-
- **Concurrency & Delivery Guarantees:**
|
|
301
|
-
- Each finalized incident triggers at most one upload attempt.
|
|
302
|
-
- Multiple incidents can upload concurrently.
|
|
303
|
-
- `stop()` does not wait for pending network uploads.
|
|
304
|
-
- There is currently no delivery guarantee (persistent retry queue, offline backoff, etc.); if transport fails, the incident remains saved in local IndexedDB and can be accessed or resent by the application.
|
|
305
|
-
|
|
306
|
-
---
|
|
307
|
-
|
|
308
|
-
## Release and trace correlation
|
|
309
|
-
|
|
310
|
-
Backtrack artifacts (`formatVersion: 2`) provide an observability `context` to correlate client-side replays with backend traces, application releases, and environments.
|
|
311
|
-
|
|
312
|
-
### Static Context
|
|
313
|
-
|
|
314
|
-
Configure static metadata when initializing Backtrack:
|
|
315
|
-
|
|
316
|
-
```typescript
|
|
317
|
-
const recorder = new Backtrack({
|
|
318
|
-
context: {
|
|
319
|
-
release: 'checkout-web@2.14.0',
|
|
320
|
-
environment: 'production',
|
|
321
|
-
traceId: '4bf92f3577b34da6a3ce929d0e0e4736',
|
|
322
|
-
spanId: '00f067aa0ba902b7'
|
|
323
|
-
}
|
|
324
|
-
});
|
|
325
|
-
```
|
|
326
|
-
|
|
327
|
-
### Dynamic Context (`getContext`)
|
|
328
|
-
|
|
329
|
-
When trace IDs or active environments change dynamically during user navigation (e.g. from OpenTelemetry, Datadog, Sentry, or custom APM SDKs), use `getContext` to evaluate the latest context at incident export time:
|
|
330
|
-
|
|
331
|
-
```typescript
|
|
332
|
-
const recorder = new Backtrack({
|
|
333
|
-
context: {
|
|
334
|
-
release: 'checkout-web@2.14.0',
|
|
335
|
-
environment: 'production'
|
|
336
|
-
},
|
|
337
|
-
getContext: async () => {
|
|
338
|
-
const activeSpan = tracer.getActiveSpan();
|
|
339
|
-
const spanContext = activeSpan?.spanContext();
|
|
340
|
-
|
|
341
|
-
return {
|
|
342
|
-
release: 'checkout-web@2.14.0',
|
|
343
|
-
environment: 'production',
|
|
344
|
-
traceId: spanContext?.traceId,
|
|
345
|
-
spanId: spanContext?.spanId
|
|
346
|
-
};
|
|
347
|
-
}
|
|
348
|
-
});
|
|
349
|
-
```
|
|
350
|
-
|
|
351
|
-
### Correlation Semantics
|
|
352
|
-
|
|
353
|
-
- **Precedence:** `getContext() > context (static) > environment metadata`.
|
|
354
|
-
- **Trace correlation scope:** Backtrack **only correlates** external trace and span identifiers supplied by your application. It does **not** generate backend traces, start trace spans, or perform distributed instrumentation on its own.
|
|
355
|
-
- **Normalization:** `traceId` (32 hex characters) and `spanId` (16 hex characters) are validated and normalized to lowercase. A `spanId` cannot be supplied without an accompanying `traceId`.
|
|
356
|
-
- **Fault Resilience:** If `getContext()` throws an error or rejects, incident export continues safely using available static context without failing the export or losing incident data.
|
|
357
|
-
|
|
358
|
-
---
|
|
359
|
-
|
|
360
|
-
## Inspecting Incidents (Viewer)
|
|
361
|
-
|
|
362
|
-
Backtrack includes an offline visualizer with video replay, console logs, network inspection, and breadcrumbs.
|
|
363
|
-
|
|
364
|
-
### Launch the Viewer CLI
|
|
365
|
-
Run the embedded CLI to launch the local visualizer:
|
|
366
|
-
|
|
367
|
-
```bash
|
|
368
|
-
npx backtrack
|
|
369
|
-
```
|
|
370
|
-
|
|
371
|
-
This starts a local server at `http://localhost:5173/` and opens your default browser. Drag and drop any exported incident `.json` file to replay it!
|
|
372
|
-
|
|
373
|
-
---
|
|
374
|
-
|
|
375
|
-
## API Reference
|
|
376
|
-
|
|
377
|
-
### `Backtrack`
|
|
378
|
-
|
|
379
|
-
| Method | Return Type | Description |
|
|
380
|
-
| --- | --- | --- |
|
|
381
|
-
| `start()` | `Promise<void>` | Initializes storage and begins recording. |
|
|
382
|
-
| `stop()` | `Promise<void>` | Stops recording, flushes pending batches to storage, and detaches listeners. |
|
|
383
|
-
| `capture(reason?, windowSeconds?)` | `Promise<string>` | Manually captures and finalizes an incident. Returns incident ID. |
|
|
384
|
-
| `captureException(error, context?)` | `Promise<string \| undefined> \| void` | Records a caught exception and triggers an incident. |
|
|
385
|
-
| `listIncidents()` | `Promise<IncidentSummary[]>` | Lists all stored incidents. |
|
|
386
|
-
| `getArtifact(incidentId)` | `Promise<FlightRecorderArtifact>` | Retrieves the full artifact for an incident (v1 or v2). |
|
|
387
|
-
| `exportIncident(incidentId)` | `Promise<FlightRecorderArtifact>` | Exports the artifact and marks it finalized. |
|
|
388
|
-
| `deleteIncident(incidentId)` | `Promise<void>` | Deletes an incident from local storage. |
|
|
389
|
-
| `clear()` | `Promise<void>` | Clears all stored chunks, sessions, and incidents. |
|
|
390
|
-
| `getHealth()` | `RecorderHealth` | Returns current buffer status, storage usage, and state. |
|
|
391
|
-
|
|
392
|
-
---
|
|
393
|
-
|
|
394
|
-
## License
|
|
395
|
-
|
|
396
|
-
[Backtrack Free-Use License](LICENSE) — Free for personal and commercial integration. Redistribution, copying, or creating derivative/competing works is strictly prohibited.
|
|
1
|
+
# Backtrack JS
|
|
2
|
+
|
|
3
|
+
Flight recorder para aplicações web: captura uma janela recente da sessão, salva incidentes localmente e ajuda a investigar replay, rede e stack traces.
|
|
4
|
+
|
|
5
|
+
## Instalação
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm install @backtrack-js/browser
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Inicialize uma vez no cliente:
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { Backtrack } from '@backtrack-js/browser';
|
|
15
|
+
|
|
16
|
+
void Backtrack.init({
|
|
17
|
+
bufferMinutes: 5,
|
|
18
|
+
showWidget: true,
|
|
19
|
+
network: { capturePayloads: false }
|
|
20
|
+
}).catch(error => {
|
|
21
|
+
console.warn('Backtrack não pôde iniciar', error);
|
|
22
|
+
});
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
O widget permite salvar uma gravação. Abra o artefato no visualizador para investigar. A biblioteca não envia gravações para um servidor por padrão.
|
|
26
|
+
|
|
27
|
+
## Documentação
|
|
28
|
+
|
|
29
|
+
[English documentation](site/docs/content/en/overview.md) · [Português](site/docs/content/pt/overview.md)
|
|
30
|
+
|
|
31
|
+
- [Primeiros passos](site/docs/content/pt/quickstart.md) e [integrações](site/docs/content/pt/integrations.md).
|
|
32
|
+
- [Configuração](site/docs/content/pt/configuration.md), [privacidade e sanitização](site/docs/content/pt/privacy.md), [rede e disparos automáticos](site/docs/content/pt/network.md).
|
|
33
|
+
- [Visualizador](site/docs/content/pt/viewer.md), [source maps](site/docs/content/pt/source-maps.md), [exportação local](site/docs/content/pt/local-export.md) e [transportes](site/docs/content/pt/transport.md).
|
|
34
|
+
- [Referência da API](site/docs/content/pt/api.md), [solução de problemas](site/docs/content/pt/troubleshooting.md) e [versões e limitações](site/docs/content/pt/releases.md).
|
|
35
|
+
|
|
36
|
+
Os guias descrevem o contrato local da versão 0.5.1. Recursos ainda não implementados são identificados explicitamente. Dados sintéticos não desativam automaticamente a sanitização fixa.
|
|
37
|
+
|
|
38
|
+
## Desenvolvimento
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
npm install
|
|
42
|
+
npm run build
|
|
43
|
+
npm run build:viewer
|
|
44
|
+
npm run build:site
|
|
45
|
+
npm run preview:site
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Documentação local: http://localhost:4174/docs/
|
|
49
|
+
|
|
50
|
+
[Como editar e publicar o site](site/README.md). A saída do site é `dist/site`; a do visualizador permanece separada. A documentação ainda precisa de um destino de publicação.
|
|
51
|
+
|
|
52
|
+
## Licença
|
|
53
|
+
|
|
54
|
+
[Backtrack Free-Use License](LICENSE)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/dev-server/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,yBAAyB,EACzB,KAAK,0BAA0B,EAC/B,KAAK,qBAAqB,EAC3B,MAAM,cAAc,CAAC"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from 'http';
|
|
2
|
+
export interface BacktrackMiddlewareOptions {
|
|
3
|
+
/** Absolute directory; use only one middleware instance per directory. */
|
|
4
|
+
directory?: string;
|
|
5
|
+
/** Alias for directory. */
|
|
6
|
+
outputDir?: string;
|
|
7
|
+
allowedOrigins?: string[];
|
|
8
|
+
maxBodyBytes?: number;
|
|
9
|
+
maxIncidents?: number;
|
|
10
|
+
maxStorageBytes?: number;
|
|
11
|
+
}
|
|
12
|
+
export type ConnectLikeMiddleware = (req: IncomingMessage, res: ServerResponse, next?: (err?: unknown) => void) => void;
|
|
13
|
+
export declare function createBacktrackMiddleware(options: BacktrackMiddlewareOptions): ConnectLikeMiddleware;
|
|
14
|
+
//# sourceMappingURL=middleware.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../../src/dev-server/middleware.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,MAAM,CAAC;AAM5D,MAAM,WAAW,0BAA0B;IACzC,0EAA0E;IAC1E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,2BAA2B;IAC3B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AACD,MAAM,MAAM,qBAAqB,GAAG,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,cAAc,EAAE,IAAI,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,OAAO,KAAK,IAAI,KAAK,IAAI,CAAC;AAmCxH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,0BAA0B,GAAG,qBAAqB,CAqKpG"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("fs"),y=require("path"),I=require("./markdown-Ao5Ah9MF.cjs"),U=/^[a-zA-Z0-9_-]{1,128}$/,W=/^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/i,_=t=>U.test(t)&&!W.test(t),A=t=>["localhost","127.0.0.1","[::1]"].includes(t.toLowerCase()),Q=t=>t==="127.0.0.1"||t==="::1"||t==="::ffff:127.0.0.1";function V(t,o){if(!Q(t.socket.remoteAddress))return!1;try{const S=t.headers.host;if(!S)return!1;const d=new URL(`http://${S}`);if(d.host!==S.toLowerCase()||!A(d.hostname))return!1;const f=t.headers.origin;if(!f)return!(o!=null&&o.length);const h=new URL(f);return!["http:","https:"].includes(h.protocol)||!A(h.hostname)||h.origin!==f?!1:o!=null&&o.length?o.includes(f):h.host===d.host}catch{return!1}}const Z=(t,o)=>t.triggeredAt-o.triggeredAt||(t.id<o.id?-1:t.id>o.id?1:0);function m(t){return e.existsSync(t)&&e.lstatSync(t).isFile()}function G(t){var L;const o=(t==null?void 0:t.directory)??(t==null?void 0:t.outputDir);if(!t||typeof o!="string"||!y.isAbsolute(o))throw new Error('[Backtrack dev-server] A opção "directory" (ou "outputDir") absoluta é obrigatória.');const S=y.resolve(o),d=y.join(S,"incidents"),f=y.join(S,"latest.md"),h=t.maxBodyBytes??50*1024*1024,F=t.maxIncidents??20,v=t.maxStorageBytes??200*1024*1024;for(const i of[h,F,v])if(!Number.isSafeInteger(i)||i<=0)throw new Error("Backtrack limits must be positive safe integers.");if((L=t.allowedOrigins)!=null&&L.some(i=>{try{const r=new URL(i);return!A(r.hostname)||!["http:","https:"].includes(r.protocol)||r.origin!==i}catch{return!0}}))throw new Error("Backtrack origins must be local HTTP(S) origins.");let P=Promise.resolve();function x(){const i=[];for(const r of e.readdirSync(d)){if(!r.endsWith(".json"))continue;const s=r.slice(0,-5);if(!_(s))continue;const n=y.join(d,r),c=y.join(d,`${s}.md`);try{if(!m(n)||!m(c)||e.statSync(n).size>h)continue;const a=I.validateFlightRecorderArtifact(JSON.parse(e.readFileSync(n,"utf8")));if(!a.success||a.data.incident.id!==s||!e.readFileSync(c,"utf8").startsWith(`# Incidente Backtrack
|
|
2
|
+
`))continue;i.push({id:s,jsonPath:n,mdPath:c,triggeredAt:a.data.incident.triggeredAt,size:e.statSync(n).size+e.statSync(c).size})}catch{}}return i.sort(Z)}function D(){const i=x(),r=i[i.length-1];if(!r)throw new Error("No complete incident to publish");const s=`> **Artefato completo:** [\`${r.id}.json\`](incidents/${r.id}.json)
|
|
3
|
+
|
|
4
|
+
`+e.readFileSync(r.mdPath,"utf8"),n=`${f}.${Date.now()}.tmp`;try{e.writeFileSync(n,s,{encoding:"utf8",flag:"wx"}),e.renameSync(n,f)}finally{m(n)&&e.unlinkSync(n)}return r.id}function E(i){const r=x();let s=r.length,n=r.reduce((c,a)=>c+a.size,e.statSync(f).size);for(const c of r){if(s<=F&&n<=v)break;c.id!==i&&(e.unlinkSync(c.mdPath),e.unlinkSync(c.jsonPath),s--,n-=c.size)}}return(i,r,s)=>{const n=(l,$)=>{!r.destroyed&&!r.headersSent&&(r.writeHead(l,{"Content-Type":"application/json",...l===405?{Allow:"POST"}:{}}),r.end(JSON.stringify($)))};if((i.url||"").split("?")[0]!=="/__backtrack/incident"){s?s():n(404,{error:"Not found"});return}if(i.method!=="POST"){n(405,{error:"Method Not Allowed"});return}if((i.headers["content-type"]||"").split(";")[0].trim().toLowerCase()!=="application/json"){n(415,{error:"Unsupported Media Type: application/json required"});return}if(!V(i,t.allowedOrigins)){n(403,{error:"Forbidden: invalid origin or non-local access"});return}let c=0,a=!1;const p=[];i.on("error",()=>{a=!0,p.length=0}),i.on("aborted",()=>{a=!0,p.length=0}),i.on("data",l=>{a||(c+=l.length,c>h?(a=!0,p.length=0,r.setHeader("Connection","close"),n(413,{error:"Payload Too Large"})):p.push(l))}),i.on("end",()=>{if(a)return;const l=Buffer.concat(p).toString("utf8");p.length=0;let $;try{$=JSON.parse(l)}catch{n(400,{error:"Invalid JSON"});return}const z=I.validateFlightRecorderArtifact($);if(!z.success){n(400,{error:"Invalid artifact schema"});return}const M=z.data,g=M.incident.id;if(!_(g)){n(400,{error:"Invalid incident ID format"});return}P=P.then(()=>{try{for(const u of[S,d]){if(e.existsSync(u)&&!e.lstatSync(u).isDirectory())throw new Error("Invalid output directory");e.mkdirSync(u,{recursive:!0})}const w=y.join(d,`${g}.json`),k=y.join(d,`${g}.md`),j=e.existsSync(w),O=e.existsSync(k);if(j&&(!m(w)||e.readFileSync(w,"utf8")!==l)||O&&(!m(k)||!j)){n(409,{error:"Conflict: incident ID exists with different content"});return}if(j&&O){const u=D();E(u),n(200,{id:g,status:"already_persisted"});return}const N=I.formatIncidentMarkdown(M),H=`> **Artefato completo:** [\`${g}.json\`](incidents/${g}.json)
|
|
5
|
+
|
|
6
|
+
`;if(Buffer.byteLength(l)+2*Buffer.byteLength(N)+Buffer.byteLength(H)>v){n(413,{error:"Incident size exceeds maxStorageBytes limit"});return}const R=`${Date.now()}-${Math.random().toString(36).slice(2)}`,B=`${w}.${R}.tmp`,b=`${k}.${R}.tmp`;let T=!1,C=!1;try{j||e.writeFileSync(B,l,{encoding:"utf8",flag:"wx"}),e.writeFileSync(b,N,{encoding:"utf8",flag:"wx"}),j||(e.renameSync(B,w),T=!0),e.renameSync(b,k),C=!0,D()}catch(u){throw C&&m(k)&&e.unlinkSync(k),T&&m(w)&&e.unlinkSync(w),u}finally{for(const u of[B,b])m(u)&&e.unlinkSync(u)}const J=x();E(J[J.length-1].id),n(201,{id:g,status:"saved"})}catch{n(500,{error:"Failed to persist incident"})}})})}}exports.createBacktrackMiddleware=G;
|
|
7
|
+
//# sourceMappingURL=dev-server.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dev-server.cjs","sources":["../src/dev-server/middleware.ts"],"sourcesContent":["import type { IncomingMessage, ServerResponse } from 'http';\nimport fs from 'fs';\nimport path from 'path';\nimport { validateFlightRecorderArtifact } from '../validation/validate';\nimport { formatIncidentMarkdown } from '../utils/markdown';\n\nexport interface BacktrackMiddlewareOptions {\n /** Absolute directory; use only one middleware instance per directory. */\n directory?: string;\n /** Alias for directory. */\n outputDir?: string;\n allowedOrigins?: string[];\n maxBodyBytes?: number;\n maxIncidents?: number;\n maxStorageBytes?: number;\n}\nexport type ConnectLikeMiddleware = (req: IncomingMessage, res: ServerResponse, next?: (err?: unknown) => void) => void;\nconst SAFE_ID = /^[a-zA-Z0-9_-]{1,128}$/;\nconst RESERVED_ID = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/i;\nconst safeId = (id: string) => SAFE_ID.test(id) && !RESERVED_ID.test(id);\nconst localHostname = (name: string) => ['localhost', '127.0.0.1', '[::1]'].includes(name.toLowerCase());\nconst localAddress = (address?: string) => address === '127.0.0.1' || address === '::1' || address === '::ffff:127.0.0.1';\nfunction allowedRequest(req: IncomingMessage, origins?: string[]): boolean {\n if (!localAddress(req.socket.remoteAddress)) return false;\n try {\n const host = req.headers.host;\n if (!host) return false;\n const target = new URL(`http://${host}`);\n if (target.host !== host.toLowerCase() || !localHostname(target.hostname)) return false;\n const origin = req.headers.origin;\n if (!origin) return !origins?.length;\n const source = new URL(origin);\n if (!['http:', 'https:'].includes(source.protocol) || !localHostname(source.hostname) || source.origin !== origin) return false;\n return origins?.length ? origins.includes(origin) : source.host === target.host;\n } catch {\n return false;\n }\n}\ninterface ManagedIncident {\n id: string;\n triggeredAt: number;\n jsonPath: string;\n mdPath: string;\n size: number;\n}\nconst compareIncidents = (a: ManagedIncident, b: ManagedIncident) =>\n a.triggeredAt - b.triggeredAt || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0);\nfunction regularFile(file: string): boolean {\n return fs.existsSync(file) && fs.lstatSync(file).isFile();\n}\n\nexport function createBacktrackMiddleware(options: BacktrackMiddlewareOptions): ConnectLikeMiddleware {\n const dir = options?.directory ?? options?.outputDir;\n if (!options || typeof dir !== 'string' || !path.isAbsolute(dir)) {\n throw new Error('[Backtrack dev-server] A opção \"directory\" (ou \"outputDir\") absoluta é obrigatória.');\n }\n const baseDirectory = path.resolve(dir);\n const incidentsDir = path.join(baseDirectory, 'incidents');\n const latestPath = path.join(baseDirectory, 'latest.md');\n const maxBodyBytes = options.maxBodyBytes ?? 50 * 1024 * 1024;\n const maxIncidents = options.maxIncidents ?? 20;\n const maxStorageBytes = options.maxStorageBytes ?? 200 * 1024 * 1024;\n for (const value of [maxBodyBytes, maxIncidents, maxStorageBytes]) {\n if (!Number.isSafeInteger(value) || value <= 0) throw new Error('Backtrack limits must be positive safe integers.');\n }\n if (options.allowedOrigins?.some(origin => {\n try {\n const url = new URL(origin);\n return !localHostname(url.hostname) || !['http:', 'https:'].includes(url.protocol) || url.origin !== origin;\n } catch { return true; }\n })) throw new Error('Backtrack origins must be local HTTP(S) origins.');\n\n // ponytail: synchronous local writes serialized per instance; use async IO if measured dev-server latency warrants it.\n let persistenceQueue = Promise.resolve();\n function scan(): ManagedIncident[] {\n const list: ManagedIncident[] = [];\n for (const file of fs.readdirSync(incidentsDir)) {\n if (!file.endsWith('.json')) continue;\n const id = file.slice(0, -5);\n if (!safeId(id)) continue;\n const jsonPath = path.join(incidentsDir, file);\n const mdPath = path.join(incidentsDir, `${id}.md`);\n try {\n if (!regularFile(jsonPath) || !regularFile(mdPath) || fs.statSync(jsonPath).size > maxBodyBytes) continue;\n const validated = validateFlightRecorderArtifact(JSON.parse(fs.readFileSync(jsonPath, 'utf8')));\n if (!validated.success || validated.data.incident.id !== id) continue;\n if (!fs.readFileSync(mdPath, 'utf8').startsWith('# Incidente Backtrack\\n')) continue;\n list.push({ id, jsonPath, mdPath, triggeredAt: validated.data.incident.triggeredAt, size: fs.statSync(jsonPath).size + fs.statSync(mdPath).size });\n } catch {\n // Unknown, incomplete or invalid files are never retention candidates.\n }\n }\n return list.sort(compareIncidents);\n }\n function publishLatest(): string {\n const list = scan();\n const latest = list[list.length - 1];\n if (!latest) throw new Error('No complete incident to publish');\n const content = `> **Artefato completo:** [\\`${latest.id}.json\\`](incidents/${latest.id}.json)\\n\\n` + fs.readFileSync(latest.mdPath, 'utf8');\n const temporary = `${latestPath}.${Date.now()}.tmp`;\n try {\n fs.writeFileSync(temporary, content, { encoding: 'utf8', flag: 'wx' });\n fs.renameSync(temporary, latestPath);\n } finally {\n if (regularFile(temporary)) fs.unlinkSync(temporary);\n }\n return latest.id;\n }\n function retain(latestId: string): void {\n const list = scan();\n let count = list.length;\n let size = list.reduce((total, item) => total + item.size, fs.statSync(latestPath).size);\n for (const item of list) {\n if (count <= maxIncidents && size <= maxStorageBytes) break;\n if (item.id === latestId) continue;\n fs.unlinkSync(item.mdPath);\n fs.unlinkSync(item.jsonPath);\n count--;\n size -= item.size;\n }\n }\n return (req, res, next) => {\n const respond = (status: number, body: Record<string, unknown>) => {\n if (!res.destroyed && !res.headersSent) {\n res.writeHead(status, { 'Content-Type': 'application/json', ...(status === 405 ? { Allow: 'POST' } : {}) });\n res.end(JSON.stringify(body));\n }\n };\n if ((req.url || '').split('?')[0] !== '/__backtrack/incident') {\n if (next) next(); else respond(404, { error: 'Not found' });\n return;\n }\n if (req.method !== 'POST') { respond(405, { error: 'Method Not Allowed' }); return; }\n if ((req.headers['content-type'] || '').split(';')[0].trim().toLowerCase() !== 'application/json') {\n respond(415, { error: 'Unsupported Media Type: application/json required' }); return;\n }\n if (!allowedRequest(req, options.allowedOrigins)) { respond(403, { error: 'Forbidden: invalid origin or non-local access' }); return; }\n let bytes = 0;\n let rejected = false;\n const chunks: Buffer[] = [];\n req.on('error', () => { rejected = true; chunks.length = 0; });\n req.on('aborted', () => { rejected = true; chunks.length = 0; });\n req.on('data', (chunk: Buffer) => {\n if (rejected) return;\n bytes += chunk.length;\n if (bytes > maxBodyBytes) {\n rejected = true;\n chunks.length = 0;\n res.setHeader('Connection', 'close');\n respond(413, { error: 'Payload Too Large' });\n // Drain without retaining data so the HTTP response can reach the client.\n } else chunks.push(chunk);\n });\n req.on('end', () => {\n if (rejected) return;\n const rawBody = Buffer.concat(chunks).toString('utf8');\n chunks.length = 0;\n let parsed: unknown;\n try { parsed = JSON.parse(rawBody); }\n catch { respond(400, { error: 'Invalid JSON' }); return; }\n const validated = validateFlightRecorderArtifact(parsed);\n if (!validated.success) { respond(400, { error: 'Invalid artifact schema' }); return; }\n const artifact = validated.data;\n const id = artifact.incident.id;\n if (!safeId(id)) { respond(400, { error: 'Invalid incident ID format' }); return; }\n persistenceQueue = persistenceQueue.then(() => {\n try {\n for (const dir of [baseDirectory, incidentsDir]) {\n if (fs.existsSync(dir) && !fs.lstatSync(dir).isDirectory()) throw new Error('Invalid output directory');\n fs.mkdirSync(dir, { recursive: true });\n }\n const jsonPath = path.join(incidentsDir, `${id}.json`);\n const mdPath = path.join(incidentsDir, `${id}.md`);\n const jsonExists = fs.existsSync(jsonPath);\n const mdExists = fs.existsSync(mdPath);\n if ((jsonExists && (!regularFile(jsonPath) || fs.readFileSync(jsonPath, 'utf8') !== rawBody)) ||\n (mdExists && (!regularFile(mdPath) || !jsonExists))) {\n respond(409, { error: 'Conflict: incident ID exists with different content' }); return;\n }\n if (jsonExists && mdExists) {\n const latestId = publishLatest();\n retain(latestId);\n respond(200, { id, status: 'already_persisted' }); return;\n }\n const markdown = formatIncidentMarkdown(artifact);\n const header = `> **Artefato completo:** [\\`${id}.json\\`](incidents/${id}.json)\\n\\n`;\n if (Buffer.byteLength(rawBody) + 2 * Buffer.byteLength(markdown) + Buffer.byteLength(header) > maxStorageBytes) {\n respond(413, { error: 'Incident size exceeds maxStorageBytes limit' }); return;\n }\n const nonce = `${Date.now()}-${Math.random().toString(36).slice(2)}`;\n const tmpJson = `${jsonPath}.${nonce}.tmp`;\n const tmpMd = `${mdPath}.${nonce}.tmp`;\n let createdJson = false;\n let createdMd = false;\n try {\n if (!jsonExists) fs.writeFileSync(tmpJson, rawBody, { encoding: 'utf8', flag: 'wx' });\n fs.writeFileSync(tmpMd, markdown, { encoding: 'utf8', flag: 'wx' });\n if (!jsonExists) { fs.renameSync(tmpJson, jsonPath); createdJson = true; }\n fs.renameSync(tmpMd, mdPath); createdMd = true;\n publishLatest();\n } catch (error) {\n if (createdMd && regularFile(mdPath)) fs.unlinkSync(mdPath);\n if (createdJson && regularFile(jsonPath)) fs.unlinkSync(jsonPath);\n throw error;\n } finally {\n for (const file of [tmpJson, tmpMd]) if (regularFile(file)) fs.unlinkSync(file);\n }\n const list = scan();\n retain(list[list.length - 1].id);\n respond(201, { id, status: 'saved' });\n } catch {\n respond(500, { error: 'Failed to persist incident' });\n }\n });\n });\n };\n}\r\n"],"names":["SAFE_ID","RESERVED_ID","safeId","id","localHostname","name","localAddress","address","allowedRequest","req","origins","host","target","origin","source","compareIncidents","a","b","regularFile","file","fs","createBacktrackMiddleware","options","dir","path","baseDirectory","incidentsDir","latestPath","maxBodyBytes","maxIncidents","maxStorageBytes","value","_a","url","persistenceQueue","scan","list","jsonPath","mdPath","validated","validateFlightRecorderArtifact","publishLatest","latest","content","temporary","retain","latestId","count","size","total","item","res","next","respond","status","body","bytes","rejected","chunks","chunk","rawBody","parsed","artifact","jsonExists","mdExists","markdown","formatIncidentMarkdown","header","nonce","tmpJson","tmpMd","createdJson","createdMd","error"],"mappings":"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"}
|