api-tracer-kit 1.0.1 → 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 +9 -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/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
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.1.0
|
|
4
|
+
|
|
5
|
+
- `api-tracer setup` wires the tracer into an application in one command. It
|
|
6
|
+
detects the framework, writes the module that configures the tracer with the
|
|
7
|
+
environment gate that bundler will actually fold, adds the import to the entry
|
|
8
|
+
file, gitignores the data directory, and warns if anything else already calls
|
|
9
|
+
`init()`. Safe to re-run.
|
|
10
|
+
- Full documentation set under `docs/`, shipped in the package.
|
|
11
|
+
|
|
3
12
|
## 1.0.1
|
|
4
13
|
|
|
5
14
|
- `init()` now warns when it is called a second time with options, naming the
|
package/README.md
CHANGED
|
@@ -3,13 +3,17 @@
|
|
|
3
3
|
Trace every API call an application makes — and map, exercise and watch the
|
|
4
4
|
endpoints it has.
|
|
5
5
|
|
|
6
|
+
[](https://www.npmjs.com/package/api-tracer-kit)
|
|
7
|
+
[](./LICENSE)
|
|
8
|
+
|
|
6
9
|
Two halves that work on their own or together:
|
|
7
10
|
|
|
8
|
-
- **the tracer** — a framework-free library that records every `fetch`,
|
|
9
|
-
and axios call, with no change to your API layer and no
|
|
11
|
+
- **the tracer** — a framework-free library that records every `fetch`,
|
|
12
|
+
`XMLHttpRequest` and axios call, with no change to your API layer and no
|
|
13
|
+
bundler configuration
|
|
10
14
|
- **the console** — a CLI and web UI that reads your source into an endpoint
|
|
11
15
|
catalog, receives the tracer's traffic, and lets you re-send any call, watch
|
|
12
|
-
for response-shape drift, and export what it captured
|
|
16
|
+
for response-shape drift, and export what it captured
|
|
13
17
|
|
|
14
18
|
```bash
|
|
15
19
|
npm install api-tracer-kit
|
|
@@ -17,26 +21,45 @@ npm install api-tracer-kit
|
|
|
17
21
|
|
|
18
22
|
## Quick start
|
|
19
23
|
|
|
24
|
+
One command wires it into your app — it detects the framework, writes the module
|
|
25
|
+
that configures the tracer, and adds the import to your entry file:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npx api-tracer setup
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Or do it by hand, which is two lines:
|
|
32
|
+
|
|
20
33
|
```ts
|
|
21
34
|
import { apiTracer } from 'api-tracer-kit';
|
|
22
35
|
|
|
23
36
|
apiTracer.init();
|
|
24
37
|
```
|
|
25
38
|
|
|
26
|
-
|
|
39
|
+
Either way, that is the whole setup. Every call made afterwards is traced:
|
|
27
40
|
|
|
28
41
|
```ts
|
|
29
42
|
await fetch('/api/users');
|
|
30
43
|
|
|
31
44
|
apiTracer.getTraces();
|
|
32
|
-
// [{ id, request: { url, method, headers, params, body },
|
|
45
|
+
// [{ id, request: { url, method, headers, params, body },
|
|
46
|
+
// response: { status, headers, body },
|
|
33
47
|
// timing: { startedAt, completedAt, duration }, status: 'success' }]
|
|
34
48
|
```
|
|
35
49
|
|
|
36
50
|
Nothing happens at import time, so the package is safe to import during SSR, in
|
|
37
51
|
a build step, or in a test. `init()` is the only thing that patches anything.
|
|
38
52
|
|
|
39
|
-
|
|
53
|
+
**Using axios?** Hand yours over — an axios instance has no global to patch:
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
apiTracer.useAxios(axios);
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
One call covers the default export and every instance `axios.create()` makes
|
|
60
|
+
afterwards.
|
|
61
|
+
|
|
62
|
+
**Want the console?**
|
|
40
63
|
|
|
41
64
|
```bash
|
|
42
65
|
npx api-tracer start
|
|
@@ -46,6 +69,22 @@ It reads your source, finds your endpoints, and opens at
|
|
|
46
69
|
`http://localhost:4400`. Point the tracer at it with `reportTo: true` and your
|
|
47
70
|
app's own traffic fills it in as you click around.
|
|
48
71
|
|
|
72
|
+
> Configure the tracer in exactly one place. `init()` is idempotent — a second
|
|
73
|
+
> call keeps the first one's configuration and warns about what it ignored.
|
|
74
|
+
|
|
75
|
+
## Documentation
|
|
76
|
+
|
|
77
|
+
| | |
|
|
78
|
+
| --- | --- |
|
|
79
|
+
| [Getting started](./docs/getting-started.md) | install, first trace, seeing them |
|
|
80
|
+
| [Tracer API](./docs/tracer.md) | every option, the `ApiTrace` model, storage, axios, lifecycle |
|
|
81
|
+
| [The console](./docs/console.md) | CLI, dashboard, replay, drift, coverage, import/export, deployment |
|
|
82
|
+
| [Configuration](./docs/configuration.md) | the config file, scanner presets, custom parsers, sign-in flows |
|
|
83
|
+
| [Frameworks](./docs/frameworks.md) | React, Next.js, Vite, CRA, Node, testing |
|
|
84
|
+
| [Troubleshooting](./docs/troubleshooting.md) | when nothing is being recorded |
|
|
85
|
+
| [Security](./docs/security.md) | redaction, what is stored, what is safe to share |
|
|
86
|
+
| [Architecture](./docs/architecture.md) | how it works inside, and why |
|
|
87
|
+
|
|
49
88
|
## Features
|
|
50
89
|
|
|
51
90
|
**Tracing**
|
|
@@ -60,24 +99,23 @@ app's own traffic fills it in as you click around.
|
|
|
60
99
|
- files in a `FormData` recorded as `<file: scan.pdf, 20418 bytes>`
|
|
61
100
|
- a unique id per call, and no mixing between concurrent requests
|
|
62
101
|
- credential redaction in headers and bodies, on by default
|
|
63
|
-
- envelope reading, for APIs that answer `200` and put the
|
|
64
|
-
- subscribers, a replaceable storage layer,
|
|
102
|
+
- envelope reading, for APIs that answer `200` and put the verdict in the body
|
|
103
|
+
- subscribers, a replaceable storage layer, a capped in-memory store by default
|
|
65
104
|
|
|
66
105
|
**Console**
|
|
67
106
|
|
|
68
|
-
- an endpoint catalog read straight out of your source —
|
|
107
|
+
- an endpoint catalog read straight out of your source — nothing hand-maintained
|
|
69
108
|
- live recording: your app's traffic lands on the endpoint it belongs to, with
|
|
70
109
|
the real params, payload and path ids already filled in
|
|
71
|
-
- send any endpoint, with a Postman-style tabbed editor
|
|
72
|
-
|
|
73
|
-
- bulk runs, and a full replay of every captured call, with a plan shown
|
|
74
|
-
anything is sent
|
|
110
|
+
- send any endpoint, with a Postman-style tabbed editor and a syntax-highlighted
|
|
111
|
+
JSON editor
|
|
112
|
+
- bulk runs, and a full replay of every captured call, with a plan shown first
|
|
75
113
|
- response-shape contracts, so a backend that quietly drops a field is caught
|
|
76
114
|
even though it answered `200`
|
|
77
115
|
- coverage: which endpoints your app has never exercised
|
|
78
116
|
- an insight report — coverage, failures, drift, risks, hygiene, auth surface,
|
|
79
|
-
latency, inventory
|
|
80
|
-
- HAR / cURL / Postman export,
|
|
117
|
+
latency, inventory, trend — as Markdown or JSON
|
|
118
|
+
- HAR / cURL / Postman export, HAR / cURL import
|
|
81
119
|
- `{{variables}}`, captured from responses and chained between calls
|
|
82
120
|
|
|
83
121
|
## Supported environments
|
|
@@ -85,13 +123,13 @@ app's own traffic fills it in as you click around.
|
|
|
85
123
|
| | |
|
|
86
124
|
| --- | --- |
|
|
87
125
|
| Browsers | ✅ `fetch`, `XMLHttpRequest`, axios |
|
|
88
|
-
| Node 18+ | ✅ `fetch` and axios; `XMLHttpRequest`
|
|
126
|
+
| Node 18+ | ✅ `fetch` and axios; `XMLHttpRequest` skipped when absent |
|
|
89
127
|
| React / Next.js / Vite / CRA / webpack | ✅ no plugin, loader, alias or config change |
|
|
90
128
|
| SSR and build-time execution | ✅ importing does nothing; `init()` no-ops without a runtime |
|
|
91
|
-
| TypeScript | ✅ types shipped, `strict`-clean |
|
|
129
|
+
| TypeScript | ✅ types shipped, `strict`-clean, `bundler` and `node16` resolution |
|
|
92
130
|
| Deno / Bun | ⚠️ untested, but the tracer only uses standard globals |
|
|
93
131
|
|
|
94
|
-
Nothing in the core imports React or axios. Both are optional peer dependencies
|
|
132
|
+
Nothing in the core imports React or axios. Both are optional peer dependencies,
|
|
95
133
|
used only by the entry points that need them.
|
|
96
134
|
|
|
97
135
|
## Entry points
|
|
@@ -103,295 +141,19 @@ import { useApiTraces } from 'api-tracer-kit/react'; // hooks
|
|
|
103
141
|
import { ApiTracerPanel } from 'api-tracer-kit/ui'; // in-app panel
|
|
104
142
|
```
|
|
105
143
|
|
|
106
|
-
Each is bundled separately
|
|
107
|
-
|
|
108
|
-
## Configuration
|
|
109
|
-
|
|
110
|
-
All of it is optional.
|
|
111
|
-
|
|
112
|
-
```ts
|
|
113
|
-
apiTracer.init({
|
|
114
|
-
// which transports to patch. Default: all of them that exist.
|
|
115
|
-
transports: ['fetch', 'xhr'],
|
|
116
|
-
|
|
117
|
-
// only trace URLs matching one of these. Strings match by substring.
|
|
118
|
-
include: ['/api/'],
|
|
119
|
-
exclude: [/\/health$/],
|
|
120
|
-
|
|
121
|
-
// header names replaced with "<redacted>"
|
|
122
|
-
redactHeaders: ['authorization', 'cookie', 'set-cookie', 'x-api-key'],
|
|
123
|
-
|
|
124
|
-
// body and query field names replaced with "<redacted>"
|
|
125
|
-
redactFields: /(token|password|secret|api[-_]?key)/i,
|
|
126
|
-
|
|
127
|
-
// bodies bigger than this are recorded as omitted rather than kept
|
|
128
|
-
maxBodyBytes: 200_000,
|
|
129
|
-
|
|
130
|
-
// how many traces to keep; the oldest fall off the end
|
|
131
|
-
maxTraces: 500,
|
|
132
|
-
|
|
133
|
-
// for APIs that answer 200 and put the verdict in the body
|
|
134
|
-
envelope: { codeFields: ['status', 'code'], okField: 'success', failFrom: 400 },
|
|
135
|
-
|
|
136
|
-
// stream finished traces to the console; `true` means http://localhost:4400
|
|
137
|
-
reportTo: true,
|
|
138
|
-
|
|
139
|
-
// replace the in-memory store
|
|
140
|
-
storage: myStorage,
|
|
141
|
-
|
|
142
|
-
onTrace: (trace) => console.log(trace),
|
|
143
|
-
});
|
|
144
|
-
```
|
|
145
|
-
|
|
146
|
-
There is no config file, and the consuming project needs no changes to its
|
|
147
|
-
webpack, Vite, Babel, TypeScript, Next.js, CRA or ESLint setup.
|
|
148
|
-
|
|
149
|
-
## API reference
|
|
150
|
-
|
|
151
|
-
### `apiTracer`
|
|
152
|
-
|
|
153
|
-
| | |
|
|
154
|
-
| --- | --- |
|
|
155
|
-
| `init(options?)` | installs the interceptors. Idempotent — calling it twice does not double-install, and the second call's options are ignored with a warning. Configure it in one place, or `destroy()` first. |
|
|
156
|
-
| `useAxios(axios)` | traces an axios object and everything `axios.create()` makes from it |
|
|
157
|
-
| `getTraces()` | every trace held, oldest first |
|
|
158
|
-
| `getTrace(id)` | one trace |
|
|
159
|
-
| `subscribe(fn)` | called with each finished trace; returns the unsubscribe function |
|
|
160
|
-
| `clear()` | drops every trace |
|
|
161
|
-
| `remove(id)` | drops one |
|
|
162
|
-
| `destroy()` | restores every patched global; the app behaves exactly as it did before `init()` |
|
|
163
|
-
| `isActive` | whether tracing is installed |
|
|
164
|
-
|
|
165
|
-
Standalone equivalents are exported too, for callers who prefer functions:
|
|
166
|
-
`initApiTracer`, `getApiTraces`, `clearApiTraces`, `subscribeToApiTraces`,
|
|
167
|
-
`destroyApiTracer`.
|
|
168
|
-
|
|
169
|
-
### `ApiTrace`
|
|
170
|
-
|
|
171
|
-
```ts
|
|
172
|
-
interface ApiTrace {
|
|
173
|
-
id: string;
|
|
174
|
-
transport: 'fetch' | 'xhr' | 'axios';
|
|
175
|
-
status: 'pending' | 'success' | 'error' | 'network-error' | 'cancelled';
|
|
176
|
-
|
|
177
|
-
request: {
|
|
178
|
-
url: string;
|
|
179
|
-
path: string; // origin + pathname, for grouping
|
|
180
|
-
method: string;
|
|
181
|
-
headers: Record<string, string>;
|
|
182
|
-
params: Record<string, unknown>; // parsed query string
|
|
183
|
-
body?: unknown; // parsed and redacted
|
|
184
|
-
bodyType: 'json' | 'formdata' | 'urlencoded' | 'text' | 'binary' | 'none';
|
|
185
|
-
bodyOmitted?: 'too-large' | 'stream' | 'binary' | 'unreadable';
|
|
186
|
-
};
|
|
187
|
-
|
|
188
|
-
response?: {
|
|
189
|
-
status: number;
|
|
190
|
-
statusText?: string;
|
|
191
|
-
headers: Record<string, string>;
|
|
192
|
-
body?: string;
|
|
193
|
-
bodyOmitted?: 'too-large' | 'stream' | 'binary' | 'unreadable';
|
|
194
|
-
size?: number;
|
|
195
|
-
};
|
|
196
|
-
|
|
197
|
-
timing: { startedAt: number; completedAt?: number; duration?: number };
|
|
198
|
-
error?: { message: string; name?: string; stack?: string };
|
|
199
|
-
|
|
200
|
-
envelopeOk?: boolean; // false when a 2xx carried an error code
|
|
201
|
-
envelopeCode?: number;
|
|
202
|
-
}
|
|
203
|
-
```
|
|
204
|
-
|
|
205
|
-
### Storage
|
|
206
|
-
|
|
207
|
-
```ts
|
|
208
|
-
interface TraceStorage {
|
|
209
|
-
add(trace: ApiTrace): void;
|
|
210
|
-
update(id: string, patch: Partial<ApiTrace>): ApiTrace | undefined;
|
|
211
|
-
get(id: string): ApiTrace | undefined;
|
|
212
|
-
getAll(): ApiTrace[];
|
|
213
|
-
remove(id: string): void;
|
|
214
|
-
clear(): void;
|
|
215
|
-
}
|
|
216
|
-
```
|
|
217
|
-
|
|
218
|
-
The default is `MemoryTraceStorage`, a capped ring — an unbounded store in a
|
|
219
|
-
long-lived tab is a memory leak. Nothing is persisted; supply your own storage
|
|
220
|
-
if you want it to be.
|
|
221
|
-
|
|
222
|
-
## React
|
|
223
|
-
|
|
224
|
-
```tsx
|
|
225
|
-
import { useApiTracer, useApiTraces } from 'api-tracer-kit/react';
|
|
226
|
-
|
|
227
|
-
function App() {
|
|
228
|
-
useApiTracer(); // init once, StrictMode-safe
|
|
229
|
-
const { traces, clear } = useApiTraces();
|
|
230
|
-
return <span>{traces.length} calls</span>;
|
|
231
|
-
}
|
|
232
|
-
```
|
|
233
|
-
|
|
234
|
-
Updates are batched on an animation frame, so a burst of calls on page load
|
|
235
|
-
re-renders once rather than once per request.
|
|
144
|
+
Each is bundled separately with `sideEffects: false`, so importing the tracer
|
|
145
|
+
never pulls in React, and an unused import is dropped entirely.
|
|
236
146
|
|
|
237
|
-
##
|
|
147
|
+
## It will not break your API calls
|
|
238
148
|
|
|
239
|
-
|
|
240
|
-
|
|
149
|
+
Removing the tracer changes nothing. Responses are read through
|
|
150
|
+
`Response.clone()` after the original is handed back; streaming responses are
|
|
151
|
+
never buffered; request-body streams are never consumed; errors are rethrown
|
|
152
|
+
exactly as they arrived; nothing is retried and no request is made twice; XHR is
|
|
153
|
+
observed with `addEventListener` rather than by taking `onload`; and `destroy()`
|
|
154
|
+
restores every patched global.
|
|
241
155
|
|
|
242
|
-
|
|
243
|
-
```
|
|
244
|
-
|
|
245
|
-
A floating panel with the call list, per-call detail (request and response
|
|
246
|
-
headers, params, bodies, status, duration, errors), search, method and status
|
|
247
|
-
filters, sorting, expand/collapse and clear. Styles are inline, so there is no
|
|
248
|
-
stylesheet to import and nothing to collide with your app's CSS.
|
|
249
|
-
|
|
250
|
-
## Axios
|
|
251
|
-
|
|
252
|
-
axios calls are traced two ways.
|
|
253
|
-
|
|
254
|
-
In a browser axios rides on `XMLHttpRequest`, so `apiTracer.init()` alone
|
|
255
|
-
already sees them. For richer data — the params object as axios received it, the
|
|
256
|
-
config, and Node's `http` adapter, which no global patch can reach — attach it
|
|
257
|
-
explicitly:
|
|
258
|
-
|
|
259
|
-
```ts
|
|
260
|
-
import axios from 'axios';
|
|
261
|
-
import { apiTracer } from 'api-tracer-kit';
|
|
262
|
-
|
|
263
|
-
apiTracer.init();
|
|
264
|
-
apiTracer.useAxios(axios);
|
|
265
|
-
```
|
|
266
|
-
|
|
267
|
-
One call covers the default export **and every instance `axios.create()` makes
|
|
268
|
-
afterwards**. That matters in codebases that send some requests through per-page
|
|
269
|
-
instances and the rest through the default object — patching only the default
|
|
270
|
-
misses half the traffic.
|
|
271
|
-
|
|
272
|
-
A call seen by both adapters is recorded once, not twice.
|
|
273
|
-
|
|
274
|
-
Why an explicit call at all: an axios instance is a plain object with no global
|
|
275
|
-
to patch, so there is no way for the tracer to reach one it was never handed.
|
|
276
|
-
This is the smallest integration that works, and it is the only one in the
|
|
277
|
-
package.
|
|
278
|
-
|
|
279
|
-
## SSR
|
|
280
|
-
|
|
281
|
-
Importing the package evaluates no browser global and installs nothing. `init()`
|
|
282
|
-
checks for each runtime capability before patching it, so on a server it simply
|
|
283
|
-
finds nothing to patch and returns.
|
|
284
|
-
|
|
285
|
-
```ts
|
|
286
|
-
// safe anywhere
|
|
287
|
-
import { apiTracer } from 'api-tracer-kit';
|
|
288
|
-
|
|
289
|
-
if (typeof window !== 'undefined') apiTracer.init();
|
|
290
|
-
```
|
|
291
|
-
|
|
292
|
-
The guard is not required — `init()` is already a no-op without a runtime — but
|
|
293
|
-
it makes the intent obvious in a Next.js file that runs on both sides.
|
|
294
|
-
|
|
295
|
-
## Not breaking your API calls
|
|
296
|
-
|
|
297
|
-
The tracer is built so that removing it changes nothing. Specifically:
|
|
298
|
-
|
|
299
|
-
- responses are read through `Response.clone()`, after the original has been
|
|
300
|
-
handed back, so your `.json()` still works
|
|
301
|
-
- streaming responses (`text/event-stream`) are never buffered
|
|
302
|
-
- binary and non-textual responses are described, not read
|
|
303
|
-
- request bodies that are `ReadableStream`s are never consumed
|
|
304
|
-
- bodies over `maxBodyBytes` are recorded as omitted rather than copied
|
|
305
|
-
- errors are rethrown exactly as they arrived; aborts stay `AbortError`
|
|
306
|
-
- `AbortSignal`, redirects, credentials and headers are passed through untouched
|
|
307
|
-
- nothing is retried, and no request is ever made twice
|
|
308
|
-
- XHR is observed with `addEventListener`, never by taking `onload` or
|
|
309
|
-
`onreadystatechange`
|
|
310
|
-
- a subscriber that throws is caught, so a bad listener cannot break a request
|
|
311
|
-
- `destroy()` restores every patched global and every axios adapter
|
|
312
|
-
|
|
313
|
-
## The console
|
|
314
|
-
|
|
315
|
-
```bash
|
|
316
|
-
npx api-tracer scan # read the source, write the endpoint catalog
|
|
317
|
-
npx api-tracer serve # the UI, at http://127.0.0.1:4400
|
|
318
|
-
npx api-tracer start # both
|
|
319
|
-
npx api-tracer report # the insight report, Markdown or JSON
|
|
320
|
-
npx api-tracer init # write a config file, if the guesses need help
|
|
321
|
-
```
|
|
322
|
-
|
|
323
|
-
### How it finds your endpoints
|
|
324
|
-
|
|
325
|
-
It runs every scanner preset over your codebase and keeps whichever actually
|
|
326
|
-
reads it:
|
|
327
|
-
|
|
328
|
-
| preset | what it reads |
|
|
329
|
-
| --- | --- |
|
|
330
|
-
| `service-object` | `const request = { subUrl: '/users' }; return get(request)` |
|
|
331
|
-
| `axios-direct` | `axios.get('/users')`, `api.post(\`/users/${id}\`, body)` |
|
|
332
|
-
| `fetch-direct` | `fetch('/api/users', { method: 'POST' })` |
|
|
333
|
-
|
|
334
|
-
Base URLs and the auth header name are read from your source too, so your
|
|
335
|
-
environments stay in sync with the app rather than being copied into a config.
|
|
336
|
-
|
|
337
|
-
If none of the presets fit, supply your own in `api-tracer.config.mjs`:
|
|
338
|
-
|
|
339
|
-
```js
|
|
340
|
-
export default {
|
|
341
|
-
parse: (src, config) => [
|
|
342
|
-
{ name, method, subUrl, usesParams, usesData, chunk, index },
|
|
343
|
-
],
|
|
344
|
-
};
|
|
345
|
-
```
|
|
346
|
-
|
|
347
|
-
### Config file
|
|
348
|
-
|
|
349
|
-
Optional. `api-tracer init` writes one filled in with what the scan worked out.
|
|
350
|
-
Every field has a default:
|
|
351
|
-
|
|
352
|
-
```js
|
|
353
|
-
export default {
|
|
354
|
-
name: 'My API',
|
|
355
|
-
preset: 'service-object',
|
|
356
|
-
sources: ['src'],
|
|
357
|
-
baseUrls: { dev: 'https://dev.example.com/api' },
|
|
358
|
-
auth: { header: 'Authorization' },
|
|
359
|
-
envelope: { codeFields: ['status', 'code'], okField: 'success', failFrom: 400 },
|
|
360
|
-
login: { steps: [/* an optional multi-step sign-in chain */] },
|
|
361
|
-
};
|
|
362
|
-
```
|
|
363
|
-
|
|
364
|
-
### Environment variables
|
|
365
|
-
|
|
366
|
-
| | |
|
|
367
|
-
| --- | --- |
|
|
368
|
-
| `API_TRACER_DATA` | where captures are kept (default `<project>/.api-tracer`) |
|
|
369
|
-
| `PORT` / `HOST` | default `4400` / `127.0.0.1` |
|
|
370
|
-
| `BASE_PATH` | serve under a path, e.g. `/api-console`, behind a proxy |
|
|
371
|
-
| `ALLOWED_ORIGINS` | comma-separated origins allowed to post recordings |
|
|
372
|
-
| `CONSOLE_USER` / `CONSOLE_PASS` | basic auth on everything except `/api/record` |
|
|
373
|
-
| `LOCK_RECORDING` / `LOCK_ENV` | stop one person's click changing a shared console |
|
|
374
|
-
|
|
375
|
-
## Security
|
|
376
|
-
|
|
377
|
-
The tracer sees whatever your API sees, which on some products means
|
|
378
|
-
credentials and personal data. So:
|
|
379
|
-
|
|
380
|
-
- `Authorization`, `Cookie`, `Set-Cookie`, `X-API-Key` and the usual token
|
|
381
|
-
header names are redacted by default, and the list is configurable
|
|
382
|
-
- any field whose **name** looks like a credential (`token`, `password`,
|
|
383
|
-
`secret`, `api_key`, `otp`, …) is replaced with `<redacted>`, keeping the
|
|
384
|
-
surrounding shape so the sample stays useful
|
|
385
|
-
- redaction happens before a trace is stored, so a redacted value is never held
|
|
386
|
-
in memory either
|
|
387
|
-
- the console redacts again before writing anything to disk
|
|
388
|
-
- HAR, cURL and Postman exports carry `<paste your token>` in place of a real
|
|
389
|
-
token, so an export is safe to attach to a ticket
|
|
390
|
-
|
|
391
|
-
The console's data directory holds captured request and response bodies from a
|
|
392
|
-
real environment. Treat it as sensitive: keep it out of version control (it is
|
|
393
|
-
`.api-tracer/`, add it to `.gitignore`) and keep a deployed console behind VPN
|
|
394
|
-
or SSO.
|
|
156
|
+
The full list is in [Tracer API](./docs/tracer.md#what-the-tracer-will-not-do-to-your-app).
|
|
395
157
|
|
|
396
158
|
## Limitations
|
|
397
159
|
|
|
@@ -399,52 +161,23 @@ Worth knowing before you rely on it.
|
|
|
399
161
|
|
|
400
162
|
- **An axios instance must be handed over.** There is no global to patch. In a
|
|
401
163
|
browser the XHR adapter catches axios anyway; in Node it cannot.
|
|
402
|
-
- **
|
|
403
|
-
empty it.
|
|
164
|
+
- **Request bodies that are streams are never read**, because reading one would
|
|
165
|
+
empty it.
|
|
404
166
|
- **Files in a `FormData` are described, not captured**, so a captured upload
|
|
405
167
|
cannot be replayed with its file.
|
|
406
168
|
- **Response bodies over `maxBodyBytes` are omitted**, not truncated — a partial
|
|
407
169
|
JSON body is more misleading than none.
|
|
408
170
|
- **`Response.clone()` buffers.** For a large non-streaming response the clone
|
|
409
171
|
holds a second copy until it is read. The size cap keeps this bounded.
|
|
410
|
-
- **The scanner is regex-based.** It reads
|
|
172
|
+
- **The scanner is regex-based.** It reads three common shapes well and says
|
|
411
173
|
what it could not read; it does not understand a URL assembled from three
|
|
412
|
-
variables. Live recording covers
|
|
413
|
-
becomes an endpoint of its own.
|
|
174
|
+
variables. Live recording covers the rest.
|
|
414
175
|
- **The "unused endpoint" count is a substring count**, so `import * as services`
|
|
415
|
-
|
|
416
|
-
- **The console is single-user by design**: one token, one
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
```
|
|
422
|
-
api-tracer-kit
|
|
423
|
-
├── src/
|
|
424
|
-
│ ├── core/ tracer, data model, redaction, bodies, URLs, envelopes, runtime detection
|
|
425
|
-
│ ├── adapters/ fetch, xhr, axios, and the suppression that stops double-counting
|
|
426
|
-
│ ├── storage/ the TraceStorage interface and the capped in-memory default
|
|
427
|
-
│ ├── transport/ posting finished traces to a console
|
|
428
|
-
│ ├── ui/ the React panel (optional entry point)
|
|
429
|
-
│ ├── index.ts the public API
|
|
430
|
-
│ ├── axios.ts api-tracer-kit/axios
|
|
431
|
-
│ └── react.ts api-tracer-kit/react
|
|
432
|
-
└── cli/
|
|
433
|
-
├── bin/ the api-tracer command
|
|
434
|
-
├── presets.mjs how to find endpoints in a codebase
|
|
435
|
-
├── config.mjs defaults, and guessing what was not configured
|
|
436
|
-
├── scan.mjs source -> endpoint catalog
|
|
437
|
-
├── server.mjs the console: forwards calls, replays, exports, records
|
|
438
|
-
├── import.mjs HAR and cURL, matched back to the catalog
|
|
439
|
-
├── shape.mjs response shapes and drift diffing
|
|
440
|
-
├── report.mjs the insight report model and its Markdown export
|
|
441
|
-
└── web/ the dashboard (no framework, no build step)
|
|
442
|
-
```
|
|
443
|
-
|
|
444
|
-
The core is framework-free and knows nothing about any particular application.
|
|
445
|
-
The console knows nothing about it either: the catalog supplies the endpoints,
|
|
446
|
-
base URLs, auth header and envelope convention, and the config file supplies the
|
|
447
|
-
sign-in flow if there is one worth automating.
|
|
176
|
+
would hide a real usage. Confirm with a grep before deleting.
|
|
177
|
+
- **The console is single-user by design**: one token, one environment, one set
|
|
178
|
+
of results.
|
|
179
|
+
- **Redaction is name-based.** A field called `notes` containing personal data is
|
|
180
|
+
not a credential and is not touched. See [Security](./docs/security.md).
|
|
448
181
|
|
|
449
182
|
## Development
|
|
450
183
|
|
|
@@ -461,6 +194,8 @@ The example project doubles as an end-to-end check:
|
|
|
461
194
|
cd examples/basic && npm install && npm start
|
|
462
195
|
```
|
|
463
196
|
|
|
197
|
+
See [CONTRIBUTING.md](./CONTRIBUTING.md).
|
|
198
|
+
|
|
464
199
|
## Licence
|
|
465
200
|
|
|
466
201
|
MIT
|
package/cli/bin/api-tracer.mjs
CHANGED
|
@@ -13,8 +13,19 @@
|
|
|
13
13
|
* rather than a blank page.
|
|
14
14
|
*/
|
|
15
15
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
16
|
-
import { join, resolve } from 'node:path';
|
|
17
|
-
import { loadConfig, guessConfig, defaults, CONFIG_NAMES } from '../config.mjs';
|
|
16
|
+
import { join, resolve, relative, dirname } from 'node:path';
|
|
17
|
+
import { loadConfig, guessConfig, defaults, CONFIG_NAMES, collectFiles } from '../config.mjs';
|
|
18
|
+
import {
|
|
19
|
+
FRAMEWORKS,
|
|
20
|
+
addImport,
|
|
21
|
+
detectFramework,
|
|
22
|
+
ensureGitignore,
|
|
23
|
+
findEntry,
|
|
24
|
+
findExistingInits,
|
|
25
|
+
recorderSource,
|
|
26
|
+
usesAxios,
|
|
27
|
+
usesTypeScript,
|
|
28
|
+
} from '../setup.mjs';
|
|
18
29
|
import { scanToFile, scanProject } from '../scan.mjs';
|
|
19
30
|
import { buildReport, toMarkdown } from '../report.mjs';
|
|
20
31
|
|
|
@@ -148,6 +159,76 @@ async function init() {
|
|
|
148
159
|
say('Nothing else is needed: `api-tracer start` works from here.');
|
|
149
160
|
}
|
|
150
161
|
|
|
162
|
+
/* ------------------------------------------------------------------ setup */
|
|
163
|
+
|
|
164
|
+
async function setup() {
|
|
165
|
+
const framework = String(flag('framework', detectFramework(ROOT)));
|
|
166
|
+
if (!FRAMEWORKS[framework]) {
|
|
167
|
+
say(`unknown framework "${framework}"; expected one of: ${Object.keys(FRAMEWORKS).join(', ')}`);
|
|
168
|
+
process.exit(1);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const axios = flag('axios') !== undefined ? flag('axios') !== 'false' : usesAxios(ROOT);
|
|
172
|
+
const ts = usesTypeScript(ROOT);
|
|
173
|
+
const out = String(flag('out', `src/apiTracer.${ts ? 'ts' : 'js'}`));
|
|
174
|
+
const entry = flag('entry') ? String(flag('entry')) : findEntry(ROOT, framework);
|
|
175
|
+
|
|
176
|
+
say(`project ${ROOT}`);
|
|
177
|
+
say(`framework ${FRAMEWORKS[framework].label}${flag('framework') ? ' (given)' : ' (detected)'}`);
|
|
178
|
+
say(`axios ${axios ? 'yes, will be attached' : 'no'}`);
|
|
179
|
+
|
|
180
|
+
/*
|
|
181
|
+
* A second init() elsewhere would keep its own configuration and silently
|
|
182
|
+
* ignore this one's, so say so before writing rather than after.
|
|
183
|
+
*/
|
|
184
|
+
const existing = findExistingInits(ROOT, collectFiles(ROOT, defaults).map((f) => relative(ROOT, f)));
|
|
185
|
+
const notOurs = existing.filter((f) => f !== out);
|
|
186
|
+
if (notOurs.length) {
|
|
187
|
+
say('');
|
|
188
|
+
say(`Something already calls apiTracer.init():`);
|
|
189
|
+
for (const f of notOurs) say(` ${f}`);
|
|
190
|
+
say('The tracer must be configured in one place -- remove those calls, or this');
|
|
191
|
+
say('file will be the one whose options are ignored.');
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const target = join(ROOT, out);
|
|
195
|
+
if (existsSync(target) && !flag('force')) {
|
|
196
|
+
say('');
|
|
197
|
+
say(`${out} already exists; pass --force to overwrite it`);
|
|
198
|
+
} else {
|
|
199
|
+
writeFileSync(target, recorderSource({ framework, axios, envelope: flag('envelope') !== 'false' }));
|
|
200
|
+
say('');
|
|
201
|
+
say(`wrote ${out}`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (!entry) {
|
|
205
|
+
say('');
|
|
206
|
+
say(`Could not find an entry file. Add this line to yours yourself:`);
|
|
207
|
+
say(` import "./${out.replace(/^src\//, '').replace(/\.[jt]sx?$/, '')}";`);
|
|
208
|
+
} else {
|
|
209
|
+
const entryPath = join(ROOT, entry);
|
|
210
|
+
const source = readFileSync(entryPath, 'utf8');
|
|
211
|
+
// a relative specifier from the entry file to the module we just wrote
|
|
212
|
+
let spec = relative(dirname(entryPath), target).replace(/\.[jt]sx?$/, '');
|
|
213
|
+
if (!spec.startsWith('.')) spec = `./${spec}`;
|
|
214
|
+
|
|
215
|
+
const { source: next, added } = addImport(source, spec);
|
|
216
|
+
if (added) {
|
|
217
|
+
writeFileSync(entryPath, next);
|
|
218
|
+
say(`imported ${entry} -> import "${spec}"`);
|
|
219
|
+
} else {
|
|
220
|
+
say(`already imported from ${entry}`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (ensureGitignore(ROOT)) say('gitignored .api-tracer/');
|
|
225
|
+
|
|
226
|
+
say('');
|
|
227
|
+
say('Next:');
|
|
228
|
+
say(' npx api-tracer start # scan your source and open the console');
|
|
229
|
+
say(' <start your app> # its calls stream in as you click');
|
|
230
|
+
}
|
|
231
|
+
|
|
151
232
|
/* ----------------------------------------------------------------- report */
|
|
152
233
|
|
|
153
234
|
async function report() {
|
|
@@ -223,6 +304,7 @@ const HELP = `api-tracer
|
|
|
223
304
|
api-tracer serve run the console (default http://127.0.0.1:4400)
|
|
224
305
|
api-tracer start scan, then serve
|
|
225
306
|
api-tracer report build the insight report
|
|
307
|
+
api-tracer setup wire the tracer into this app, in one command
|
|
226
308
|
api-tracer init write a config file from what the scan guessed
|
|
227
309
|
|
|
228
310
|
Options
|
|
@@ -235,11 +317,20 @@ Options
|
|
|
235
317
|
--out <file> write the report to a file (default: stdout)
|
|
236
318
|
--env <name> which base URL the report describes
|
|
237
319
|
|
|
320
|
+
setup options
|
|
321
|
+
--framework <name> cra | vite | next | node (default: detected)
|
|
322
|
+
--entry <file> the entry file to import from (default: detected)
|
|
323
|
+
--out <file> where to write the module (default: src/apiTracer.js)
|
|
324
|
+
--axios false do not attach axios
|
|
325
|
+
--envelope false the API reports failure in the status line, not the body
|
|
326
|
+
--force overwrite an existing file
|
|
327
|
+
|
|
238
328
|
No configuration file is required.
|
|
239
329
|
`;
|
|
240
330
|
|
|
241
331
|
const commands = {
|
|
242
332
|
scan,
|
|
333
|
+
setup,
|
|
243
334
|
init,
|
|
244
335
|
report,
|
|
245
336
|
serve,
|