api-tracer-kit 1.0.0 → 1.1.0
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/CHANGELOG.md +17 -0
- package/README.md +77 -342
- package/cli/bin/api-tracer.mjs +93 -2
- package/cli/setup.mjs +182 -0
- package/cli/test.mjs +97 -0
- package/cli/web/app.css +1 -2
- package/dist/axios.cjs +8 -0
- package/dist/axios.cjs.map +1 -1
- package/dist/axios.js +8 -0
- package/dist/axios.js.map +1 -1
- package/dist/index.cjs +8 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -1
- package/dist/react.cjs +8 -0
- package/dist/react.cjs.map +1 -1
- package/dist/react.js +8 -0
- package/dist/react.js.map +1 -1
- package/dist/ui.cjs +8 -0
- package/dist/ui.cjs.map +1 -1
- package/dist/ui.js +8 -0
- package/dist/ui.js.map +1 -1
- package/docs/architecture.md +136 -0
- package/docs/configuration.md +275 -0
- package/docs/console.md +428 -0
- package/docs/frameworks.md +192 -0
- package/docs/getting-started.md +120 -0
- package/docs/security.md +143 -0
- package/docs/tracer.md +330 -0
- package/docs/troubleshooting.md +194 -0
- package/package.json +2 -1
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# Getting started
|
|
2
|
+
|
|
3
|
+
## Install
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install api-tracer-kit
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Nothing runs on install, and importing the package evaluates no browser global.
|
|
10
|
+
Tracing begins only when you call `init()`.
|
|
11
|
+
|
|
12
|
+
## One command
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npx api-tracer setup
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
It detects your framework, writes a module that configures the tracer, adds the
|
|
19
|
+
import to your entry file, and gitignores the console's data directory. Re-running
|
|
20
|
+
it is safe, and it warns if anything else already calls `init()`.
|
|
21
|
+
|
|
22
|
+
| Option | |
|
|
23
|
+
| --- | --- |
|
|
24
|
+
| `--framework cra\|vite\|next\|node` | override the detection |
|
|
25
|
+
| `--entry <file>` | the entry file to import from |
|
|
26
|
+
| `--out <file>` | where to write the module (default `src/apiTracer.js`) |
|
|
27
|
+
| `--axios false` | do not attach axios |
|
|
28
|
+
| `--envelope false` | your API reports failure in the status line, not the body |
|
|
29
|
+
| `--force` | overwrite an existing file |
|
|
30
|
+
|
|
31
|
+
What it works out for you:
|
|
32
|
+
|
|
33
|
+
- **the framework**, from your dependencies
|
|
34
|
+
- **the environment gate** that your bundler will actually fold — `REACT_APP_ENV`
|
|
35
|
+
for CRA, `import.meta.env.DEV` for Vite, `NODE_ENV` for Next and Node
|
|
36
|
+
- **whether to attach axios**, from whether you depend on it
|
|
37
|
+
- **TypeScript or JavaScript**, from whether you have a `tsconfig.json`
|
|
38
|
+
|
|
39
|
+
## Or do it by hand
|
|
40
|
+
|
|
41
|
+
Put this at your application's entry point, before anything makes a request:
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
import { apiTracer } from 'api-tracer-kit';
|
|
45
|
+
|
|
46
|
+
apiTracer.init();
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Every `fetch` and `XMLHttpRequest` afterwards is traced. Read them back with:
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
apiTracer.getTraces();
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### If you use axios
|
|
56
|
+
|
|
57
|
+
axios instances are plain objects with no global to patch, so hand yours over:
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
import axios from 'axios';
|
|
61
|
+
|
|
62
|
+
apiTracer.init();
|
|
63
|
+
apiTracer.useAxios(axios);
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
One call covers the default export **and every instance `axios.create()` makes
|
|
67
|
+
afterwards**. See [axios](./tracer.md#axios) for why this is the one integration
|
|
68
|
+
the package cannot do for you.
|
|
69
|
+
|
|
70
|
+
> **Configure it in exactly one place.** `init()` is idempotent — a second call
|
|
71
|
+
> keeps the first one's configuration and warns about the options it ignored.
|
|
72
|
+
> Two modules each calling `init()` is the most common way to end up with a
|
|
73
|
+
> tracer that runs but does nothing you asked for.
|
|
74
|
+
|
|
75
|
+
## See them
|
|
76
|
+
|
|
77
|
+
Three options, in increasing order of usefulness.
|
|
78
|
+
|
|
79
|
+
**1. The console log.** Fine for a quick check:
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
apiTracer.subscribe((trace) => {
|
|
83
|
+
console.log(trace.request.method, trace.request.url, trace.response?.status, `${trace.timing.duration}ms`);
|
|
84
|
+
});
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
**2. The in-app panel.** A floating list with search, filters and per-call
|
|
88
|
+
detail. React only:
|
|
89
|
+
|
|
90
|
+
```tsx
|
|
91
|
+
import { ApiTracerPanel } from 'api-tracer-kit/ui';
|
|
92
|
+
|
|
93
|
+
<ApiTracerPanel enabled={process.env.NODE_ENV !== 'production'} />;
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
**3. The console.** A separate process that maps every endpoint in your source,
|
|
97
|
+
receives your app's traffic, and lets you re-send any call. This is the one
|
|
98
|
+
worth setting up. See [the console](./console.md).
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
npx api-tracer start
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
then point the tracer at it:
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
apiTracer.init({ reportTo: true }); // true means http://localhost:4400
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Where to go next
|
|
111
|
+
|
|
112
|
+
| | |
|
|
113
|
+
| --- | --- |
|
|
114
|
+
| [Tracer API](./tracer.md) | every option, the `ApiTrace` model, storage, lifecycle |
|
|
115
|
+
| [The console](./console.md) | CLI, dashboard, replay, drift, coverage, import/export |
|
|
116
|
+
| [Configuration](./configuration.md) | the config file, scanner presets, sign-in flows |
|
|
117
|
+
| [Frameworks](./frameworks.md) | React, Next.js, Vite, CRA, Node, plain JavaScript |
|
|
118
|
+
| [Troubleshooting](./troubleshooting.md) | when nothing is being recorded |
|
|
119
|
+
| [Security](./security.md) | redaction, what is stored, what is safe to share |
|
|
120
|
+
| [Architecture](./architecture.md) | how it works inside |
|
package/docs/security.md
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# Security
|
|
2
|
+
|
|
3
|
+
The tracer sees everything your API sees. On some products that means
|
|
4
|
+
credentials, personal data, and — in health, finance or HR software — data whose
|
|
5
|
+
mishandling is a regulatory matter, not just an embarrassment. Read this before
|
|
6
|
+
deploying a console anywhere but your own laptop.
|
|
7
|
+
|
|
8
|
+
## What is redacted, by default
|
|
9
|
+
|
|
10
|
+
**Headers**, matched case-insensitively:
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
authorization, cookie, set-cookie, proxy-authorization, x-api-key, api-key,
|
|
14
|
+
auth_token, access_token, secret_token, x-auth-token, x-csrf-token
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
**Body and query fields**, matched by name:
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
/(token|password|passwd|secret|api[-_]?key|authorization|credential|otp|ssn)/i
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
The **value** is replaced with `<redacted>` and the surrounding shape is kept, so
|
|
24
|
+
the sample still shows what the endpoint expects.
|
|
25
|
+
|
|
26
|
+
Redaction happens **before** a trace is stored, so a real credential is never
|
|
27
|
+
held in the tracer's memory, never reaches a subscriber, and never leaves the
|
|
28
|
+
browser. The console redacts again before writing anything to disk.
|
|
29
|
+
|
|
30
|
+
Extend rather than replace:
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
import { DEFAULT_REDACT_HEADERS } from 'api-tracer-kit';
|
|
34
|
+
|
|
35
|
+
apiTracer.init({
|
|
36
|
+
redactHeaders: [...DEFAULT_REDACT_HEADERS, 'x-internal-key'],
|
|
37
|
+
redactFields: /(token|password|secret|nhs_number|date_of_birth)/i,
|
|
38
|
+
});
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## What is *not* redacted
|
|
42
|
+
|
|
43
|
+
This is the part that matters.
|
|
44
|
+
|
|
45
|
+
Redaction is **name-based**. A field called `notes` containing a patient's
|
|
46
|
+
history, a `body` containing an address, a response listing every user — none of
|
|
47
|
+
those look like credentials, and none are touched. The tracer records response
|
|
48
|
+
bodies in full, up to `maxBodyBytes`.
|
|
49
|
+
|
|
50
|
+
So: **assume every trace and every capture contains real data from the
|
|
51
|
+
environment you are watching.**
|
|
52
|
+
|
|
53
|
+
If that is unacceptable for your product, either narrow what is traced:
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
apiTracer.init({ include: ['/api/reference/'] });
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
or drop response bodies entirely:
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
apiTracer.init({ maxBodyBytes: 0 });
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
or post-process in a subscriber before anything is stored or reported.
|
|
66
|
+
|
|
67
|
+
## Where data ends up
|
|
68
|
+
|
|
69
|
+
### In the browser
|
|
70
|
+
|
|
71
|
+
Traces live in memory, in a capped ring (500 by default), and disappear on
|
|
72
|
+
reload. Nothing is persisted unless you supply storage that does.
|
|
73
|
+
|
|
74
|
+
### In the console's data directory
|
|
75
|
+
|
|
76
|
+
`<project>/.api-tracer/` holds captured request and response bodies, response
|
|
77
|
+
shapes, and a real session token.
|
|
78
|
+
|
|
79
|
+
**Add it to `.gitignore` before you run anything:**
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
.api-tracer/
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
`tokens.json` is written owner-only (`chmod 600`), but it is still a real
|
|
86
|
+
credential on disk. **Sign out** deletes it, and on a shared machine that is
|
|
87
|
+
worth doing.
|
|
88
|
+
|
|
89
|
+
If it has already been committed, unstage it — the files stay on disk:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
git rm -r --cached .api-tracer
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
If it reached a pushed commit, treat the token as compromised and rotate it. The
|
|
96
|
+
bodies are in the history and removing them means rewriting it.
|
|
97
|
+
|
|
98
|
+
## What is safe to share
|
|
99
|
+
|
|
100
|
+
HAR, cURL and Postman exports carry `<paste your token>` in place of a real
|
|
101
|
+
token, so an export is safe to attach to a ticket or hand to a backend
|
|
102
|
+
developer — **as far as credentials go**.
|
|
103
|
+
|
|
104
|
+
The bodies in them are still real. An export of a busy session is a data
|
|
105
|
+
extract. Treat it accordingly.
|
|
106
|
+
|
|
107
|
+
## Deploying a console
|
|
108
|
+
|
|
109
|
+
It is a Node process that writes JSON files and holds a token. It is not a
|
|
110
|
+
static asset.
|
|
111
|
+
|
|
112
|
+
- **Keep it off the public internet.** VPN, SSO, or an allowlisted origin behind
|
|
113
|
+
your CDN.
|
|
114
|
+
- **`CONSOLE_USER` / `CONSOLE_PASS`** turn on basic auth — but **`/api/record`
|
|
115
|
+
stays open**, because the app posts recordings from a browser that cannot send
|
|
116
|
+
those credentials. The gate protects reading captures, replaying calls and the
|
|
117
|
+
stored token; it does not stop someone who can reach the host from posting
|
|
118
|
+
junk recordings. If that matters, put the whole thing behind VPN or SSO.
|
|
119
|
+
- **`ALLOWED_ORIGINS`** is an allowlist for posting recordings. Localhost always
|
|
120
|
+
works; every other origin must be named, so a stray site cannot feed or read
|
|
121
|
+
the console.
|
|
122
|
+
- **`HOST`** stays on `127.0.0.1` unless you change it, so a laptop instance is
|
|
123
|
+
not exposed by accident.
|
|
124
|
+
- **Keep the data volume off shared storage.**
|
|
125
|
+
- **Mixed content**: an HTTPS app cannot post to an `http://` console, and the
|
|
126
|
+
browser blocks it silently. Terminate TLS in front of it, or serve it under a
|
|
127
|
+
path on the app's own origin.
|
|
128
|
+
|
|
129
|
+
## Replaying against production
|
|
130
|
+
|
|
131
|
+
The console refuses to replay writes against an environment named `prod`.
|
|
132
|
+
|
|
133
|
+
That is a guard, not a guarantee — it matches on the environment name you
|
|
134
|
+
configured. Consider commenting out the production base URL in your config
|
|
135
|
+
entirely, so it cannot be selected. `LOCK_ENV` pins a shared console to one
|
|
136
|
+
environment so nobody can switch it.
|
|
137
|
+
|
|
138
|
+
Replay is not a dry run. Every POST creates a real record each time.
|
|
139
|
+
|
|
140
|
+
## Reporting a vulnerability
|
|
141
|
+
|
|
142
|
+
Open an issue for anything non-sensitive. For a genuine vulnerability, contact
|
|
143
|
+
the maintainer directly rather than filing publicly.
|
package/docs/tracer.md
ADDED
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
# The tracer
|
|
2
|
+
|
|
3
|
+
The framework-free half of the package. No React, no axios, no DOM required.
|
|
4
|
+
|
|
5
|
+
## `apiTracer`
|
|
6
|
+
|
|
7
|
+
A single shared instance. It is held in a global registry keyed by
|
|
8
|
+
`Symbol.for('api-tracer-kit.registry')`, so `api-tracer-kit` and
|
|
9
|
+
`api-tracer-kit/axios` — separate bundles with separate module graphs — resolve
|
|
10
|
+
to the same object. Without that, `useAxios()` would attach to an instance
|
|
11
|
+
`getTraces()` never reads from.
|
|
12
|
+
|
|
13
|
+
| Method | Description |
|
|
14
|
+
| --- | --- |
|
|
15
|
+
| `init(options?)` | Installs the interceptors. Idempotent: a second call keeps the first one's configuration and warns about the options it ignored. Returns `this`. |
|
|
16
|
+
| `useAxios(axios)` | Traces an axios object and everything it creates. Calls `init()` first if needed. Returns `this`. |
|
|
17
|
+
| `getTraces()` | Every trace held, oldest first. |
|
|
18
|
+
| `getTrace(id)` | One trace, or `undefined`. |
|
|
19
|
+
| `subscribe(fn)` | Calls `fn(trace)` for each **finished** trace. Returns the unsubscribe function. |
|
|
20
|
+
| `clear()` | Drops every trace. |
|
|
21
|
+
| `remove(id)` | Drops one. |
|
|
22
|
+
| `destroy()` | Restores every patched global and every axios adapter. After this the runtime behaves exactly as it did before `init()`. |
|
|
23
|
+
| `isActive` | Whether tracing is currently installed. |
|
|
24
|
+
|
|
25
|
+
Standalone function forms, for callers who would rather not reach through an
|
|
26
|
+
object: `initApiTracer`, `getApiTraces`, `clearApiTraces`,
|
|
27
|
+
`subscribeToApiTraces`, `destroyApiTracer`. They act on the same shared
|
|
28
|
+
instance.
|
|
29
|
+
|
|
30
|
+
`ApiTracer` is exported as a class too, if you want an isolated instance — most
|
|
31
|
+
useful in tests, where a shared singleton between test cases is a nuisance.
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
import { ApiTracer } from 'api-tracer-kit';
|
|
35
|
+
|
|
36
|
+
const tracer = new ApiTracer().init({ transports: ['fetch'] });
|
|
37
|
+
// ...
|
|
38
|
+
tracer.destroy();
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Options
|
|
42
|
+
|
|
43
|
+
Every one is optional.
|
|
44
|
+
|
|
45
|
+
### `transports`
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
transports?: ('fetch' | 'xhr' | 'axios')[]
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Which globals to patch. Default: all of them that exist in the runtime. Pass
|
|
52
|
+
`[]` to patch nothing globally — useful when you only want axios, which is
|
|
53
|
+
attached separately.
|
|
54
|
+
|
|
55
|
+
`'axios'` in this list does nothing on its own; axios has no global to patch and
|
|
56
|
+
is reached only through `useAxios()`.
|
|
57
|
+
|
|
58
|
+
### `include` / `exclude`
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
include?: (string | RegExp)[]
|
|
62
|
+
exclude?: (string | RegExp)[]
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Strings match by substring, so `'/api/'` is usually enough. `exclude` is applied
|
|
66
|
+
first and wins. With no `include`, everything not excluded is traced.
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
apiTracer.init({ include: ['/api/'], exclude: [/\/health$/, 'analytics'] });
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`reportTo`'s own endpoint is excluded automatically — the tracer never traces
|
|
73
|
+
its own reporting.
|
|
74
|
+
|
|
75
|
+
### `redactHeaders` / `redactFields`
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
redactHeaders?: string[] // default: DEFAULT_REDACT_HEADERS
|
|
79
|
+
redactFields?: RegExp // default: DEFAULT_REDACT_FIELDS
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Header names are matched case-insensitively; field names are matched with the
|
|
83
|
+
regex. Both replace the **value** with `<redacted>` and keep the surrounding
|
|
84
|
+
shape. Redaction happens before a trace is stored, so a real credential is never
|
|
85
|
+
held in memory.
|
|
86
|
+
|
|
87
|
+
Defaults are exported so you can extend rather than replace them:
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
import { DEFAULT_REDACT_HEADERS } from 'api-tracer-kit';
|
|
91
|
+
|
|
92
|
+
apiTracer.init({ redactHeaders: [...DEFAULT_REDACT_HEADERS, 'x-internal-key'] });
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
`DEFAULT_REDACT_HEADERS` covers `authorization`, `cookie`, `set-cookie`,
|
|
96
|
+
`proxy-authorization`, `x-api-key`, `api-key`, `auth_token`, `access_token`,
|
|
97
|
+
`secret_token`, `x-auth-token`, `x-csrf-token`.
|
|
98
|
+
|
|
99
|
+
`DEFAULT_REDACT_FIELDS` is
|
|
100
|
+
`/(token|password|passwd|secret|api[-_]?key|authorization|credential|otp|ssn)/i`.
|
|
101
|
+
|
|
102
|
+
### `maxBodyBytes`
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
maxBodyBytes?: number // default 200_000
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Bodies larger than this are recorded as omitted rather than truncated — a
|
|
109
|
+
partial JSON body is more misleading than none. A response declaring a larger
|
|
110
|
+
`content-length` is never read at all.
|
|
111
|
+
|
|
112
|
+
### `maxTraces`
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
maxTraces?: number // default 500
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
The in-memory store is a capped ring; older traces fall off the end. An
|
|
119
|
+
unbounded store in a long-lived tab is a memory leak.
|
|
120
|
+
|
|
121
|
+
### `envelope`
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
envelope?: {
|
|
125
|
+
codeFields?: string[]; // default ['status', 'code']
|
|
126
|
+
okField?: string; // default 'success'
|
|
127
|
+
failFrom?: number; // default 400
|
|
128
|
+
} | false
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
For APIs that answer HTTP `200` and put the real verdict in the body:
|
|
132
|
+
|
|
133
|
+
```json
|
|
134
|
+
{ "status": 801, "success": false, "message": "Authentication token header missing" }
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
With an envelope configured, that trace gets `envelopeOk: false` and
|
|
138
|
+
`envelopeCode: 801` even though `response.status` is `200`. Pass an empty object
|
|
139
|
+
`{}` to accept the defaults; omit it entirely and no envelope is read.
|
|
140
|
+
|
|
141
|
+
### `reportTo`
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
reportTo?: string | boolean
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
POST each finished trace to a console. `true` means `http://localhost:4400`.
|
|
148
|
+
Failures are swallowed — a console that is not running must never affect the
|
|
149
|
+
app — and after six consecutive failures the reporter stops trying.
|
|
150
|
+
|
|
151
|
+
### `storage`
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
storage?: TraceStorage
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Replaces the in-memory store. See [Storage](#storage).
|
|
158
|
+
|
|
159
|
+
### `onTrace`
|
|
160
|
+
|
|
161
|
+
```ts
|
|
162
|
+
onTrace?: (trace: ApiTrace) => void
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Identical to `subscribe()`, set up front. This is the one option a second
|
|
166
|
+
`init()` still honours, because adding a subscriber from another module is
|
|
167
|
+
legitimate.
|
|
168
|
+
|
|
169
|
+
### `debug`
|
|
170
|
+
|
|
171
|
+
```ts
|
|
172
|
+
debug?: boolean // default false
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Logs a line on init and reports subscribers that throw.
|
|
176
|
+
|
|
177
|
+
## The `ApiTrace` model
|
|
178
|
+
|
|
179
|
+
```ts
|
|
180
|
+
interface ApiTrace {
|
|
181
|
+
id: string;
|
|
182
|
+
transport: 'fetch' | 'xhr' | 'axios';
|
|
183
|
+
status: 'pending' | 'success' | 'error' | 'network-error' | 'cancelled';
|
|
184
|
+
|
|
185
|
+
request: {
|
|
186
|
+
url: string; // absolute
|
|
187
|
+
path: string; // origin + pathname, for grouping
|
|
188
|
+
method: string;
|
|
189
|
+
headers: Record<string, string>; // redacted
|
|
190
|
+
params: Record<string, unknown>; // parsed query string, redacted
|
|
191
|
+
body?: unknown; // parsed and redacted
|
|
192
|
+
bodyType: 'json' | 'formdata' | 'urlencoded' | 'text' | 'binary' | 'none';
|
|
193
|
+
bodyOmitted?: 'too-large' | 'stream' | 'binary' | 'unreadable';
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
response?: {
|
|
197
|
+
status: number;
|
|
198
|
+
statusText?: string;
|
|
199
|
+
headers: Record<string, string>; // redacted
|
|
200
|
+
body?: string; // raw text
|
|
201
|
+
bodyOmitted?: 'too-large' | 'stream' | 'binary' | 'unreadable';
|
|
202
|
+
size?: number;
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
timing: {
|
|
206
|
+
startedAt: number; // performance.now() where available
|
|
207
|
+
completedAt?: number;
|
|
208
|
+
duration?: number; // milliseconds, rounded
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
error?: { message: string; name?: string; stack?: string };
|
|
212
|
+
|
|
213
|
+
envelopeOk?: boolean;
|
|
214
|
+
envelopeCode?: number;
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### What each `status` means
|
|
219
|
+
|
|
220
|
+
| | |
|
|
221
|
+
| --- | --- |
|
|
222
|
+
| `pending` | in flight; `response` and `timing.duration` are not set yet |
|
|
223
|
+
| `success` | HTTP 2xx |
|
|
224
|
+
| `error` | the server answered, but not with a 2xx |
|
|
225
|
+
| `network-error` | no answer: DNS failure, connection refused, timeout |
|
|
226
|
+
| `cancelled` | aborted by the caller — not a broken endpoint |
|
|
227
|
+
|
|
228
|
+
Subscribers are called only when a trace finishes, so a subscriber never sees
|
|
229
|
+
`pending`. `getTraces()` can return one.
|
|
230
|
+
|
|
231
|
+
### Bodies
|
|
232
|
+
|
|
233
|
+
`bodyType` records what actually went on the wire, so a captured call can be
|
|
234
|
+
replayed the same way. A `FormData` is unpacked field by field — `JSON.stringify`
|
|
235
|
+
flattens one to `{}` — and files inside it become
|
|
236
|
+
`<file: scan.pdf, 20418 bytes>`, because a file cannot be replayed.
|
|
237
|
+
|
|
238
|
+
`bodyOmitted` says why a body is missing:
|
|
239
|
+
|
|
240
|
+
| | |
|
|
241
|
+
| --- | --- |
|
|
242
|
+
| `too-large` | over `maxBodyBytes` |
|
|
243
|
+
| `stream` | a `ReadableStream` request body, or a `text/event-stream` response — reading either would break it |
|
|
244
|
+
| `binary` | a Blob, ArrayBuffer, or a non-textual content type |
|
|
245
|
+
| `unreadable` | the body could not be cloned or parsed |
|
|
246
|
+
|
|
247
|
+
## Storage
|
|
248
|
+
|
|
249
|
+
```ts
|
|
250
|
+
interface TraceStorage {
|
|
251
|
+
add(trace: ApiTrace): void;
|
|
252
|
+
update(id: string, patch: Partial<ApiTrace>): ApiTrace | undefined;
|
|
253
|
+
get(id: string): ApiTrace | undefined;
|
|
254
|
+
getAll(): ApiTrace[];
|
|
255
|
+
remove(id: string): void;
|
|
256
|
+
clear(): void;
|
|
257
|
+
}
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
The default is `MemoryTraceStorage`, exported so you can size it yourself:
|
|
261
|
+
|
|
262
|
+
```ts
|
|
263
|
+
import { MemoryTraceStorage } from 'api-tracer-kit';
|
|
264
|
+
|
|
265
|
+
apiTracer.init({ storage: new MemoryTraceStorage(50) });
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
`update()` returning `undefined` for an unknown id is normal, not an error: a
|
|
269
|
+
long-running request can be evicted by the cap while still in flight.
|
|
270
|
+
|
|
271
|
+
Nothing is persisted by default. To keep traces across reloads, supply storage
|
|
272
|
+
backed by `sessionStorage` or IndexedDB — but see
|
|
273
|
+
[Security](./security.md) first, because traces contain request and response
|
|
274
|
+
bodies.
|
|
275
|
+
|
|
276
|
+
## axios
|
|
277
|
+
|
|
278
|
+
axios calls are traced two ways.
|
|
279
|
+
|
|
280
|
+
In a browser axios rides on `XMLHttpRequest`, so `apiTracer.init()` alone sees
|
|
281
|
+
them. `useAxios()` adds what the XHR adapter cannot:
|
|
282
|
+
|
|
283
|
+
- the `params` object as axios received it, rather than a re-parsed query string
|
|
284
|
+
- Node's `http` adapter, which no global patch can reach
|
|
285
|
+
- correct handling of axios's habit of rejecting on non-2xx while still having a
|
|
286
|
+
response
|
|
287
|
+
|
|
288
|
+
A call seen by both adapters is recorded **once**. axios builds and opens its
|
|
289
|
+
XHR synchronously inside its adapter, so the tracer raises a flag around the
|
|
290
|
+
adapter call that is still up when `open` runs and down before anything else can
|
|
291
|
+
start a request. The suppression is exact, not time-based.
|
|
292
|
+
|
|
293
|
+
`useAxios()` wraps the instance's adapter rather than only its interceptors,
|
|
294
|
+
because the adapter is the one place both the request and the raw response are
|
|
295
|
+
available. If your application later assigns `axios.defaults.adapter` — a mock,
|
|
296
|
+
a retry library, a custom transport — tracing survives it: the property is an
|
|
297
|
+
accessor that routes the assignment underneath the wrapper.
|
|
298
|
+
|
|
299
|
+
`destroy()` gives every instance back the adapter it last had, including
|
|
300
|
+
instances created through the patched `create()`.
|
|
301
|
+
|
|
302
|
+
## What the tracer will not do to your app
|
|
303
|
+
|
|
304
|
+
Removing it changes nothing. Specifically:
|
|
305
|
+
|
|
306
|
+
- responses are read through `Response.clone()`, **after** the original has been
|
|
307
|
+
handed back, so your `.json()` still works
|
|
308
|
+
- `text/event-stream` responses are never buffered
|
|
309
|
+
- binary and non-textual responses are described, not read
|
|
310
|
+
- request bodies that are streams are never consumed
|
|
311
|
+
- errors are rethrown exactly as they arrived; aborts stay `AbortError`
|
|
312
|
+
- `AbortSignal`, redirects, credentials and headers pass through untouched
|
|
313
|
+
- nothing is retried, and no request is made twice
|
|
314
|
+
- XHR is observed with `addEventListener`, never by taking `onload` or
|
|
315
|
+
`onreadystatechange` — those belong to the caller
|
|
316
|
+
- a subscriber that throws is caught
|
|
317
|
+
- `destroy()` restores every patched global
|
|
318
|
+
|
|
319
|
+
## Cleanup
|
|
320
|
+
|
|
321
|
+
```ts
|
|
322
|
+
apiTracer.destroy();
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
Restores `fetch`, the `XMLHttpRequest` prototype and every axios adapter, clears
|
|
326
|
+
the subscribers, and stops reporting. `fetch` is only restored if nothing else
|
|
327
|
+
has patched it since — clobbering another tool's interceptor on the way out
|
|
328
|
+
would be worse than leaving ours in place.
|
|
329
|
+
|
|
330
|
+
Calling `destroy()` on a tracer that was never started is safe.
|