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
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
# Configuration
|
|
2
|
+
|
|
3
|
+
There is no required configuration. `api-tracer scan` works in a project it has
|
|
4
|
+
never seen: it runs every scanner preset over your codebase and keeps whichever
|
|
5
|
+
actually reads it, then reads your base URLs and auth header out of the source.
|
|
6
|
+
|
|
7
|
+
A config file exists for what guessing cannot cover — a bespoke service shape, a
|
|
8
|
+
base URL that is not written down, a sign-in flow worth automating.
|
|
9
|
+
|
|
10
|
+
## The file
|
|
11
|
+
|
|
12
|
+
One of these in your project root, checked in order:
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
api-tracer.config.mjs
|
|
16
|
+
api-tracer.config.js
|
|
17
|
+
api-tracer.config.json
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Use `.mjs` unless your project is ESM already — a `.js` file with
|
|
21
|
+
`export default` will not load inside a CommonJS package.
|
|
22
|
+
|
|
23
|
+
Generate one filled in with what the scan worked out:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npx api-tracer init
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
It prints what each preset found before writing, so you can see the evidence:
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
1,284 source files scanned
|
|
33
|
+
service-object 325
|
|
34
|
+
fetch-direct 1
|
|
35
|
+
axios-direct 0
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Every field
|
|
39
|
+
|
|
40
|
+
```js
|
|
41
|
+
export default {
|
|
42
|
+
// shown in the console header and in Postman exports
|
|
43
|
+
name: 'My API',
|
|
44
|
+
|
|
45
|
+
// which preset reads this codebase; null or absent means guess
|
|
46
|
+
preset: 'service-object',
|
|
47
|
+
|
|
48
|
+
// for the service-object preset: the object key holding the path
|
|
49
|
+
pathKey: 'subUrl',
|
|
50
|
+
|
|
51
|
+
// where to look for API calls, relative to the project root
|
|
52
|
+
sources: ['src', 'app', 'lib', 'services', 'api'],
|
|
53
|
+
|
|
54
|
+
// never walked
|
|
55
|
+
ignore: ['node_modules', 'dist', 'build', 'coverage', '.git', '.next', 'out', 'vendor'],
|
|
56
|
+
|
|
57
|
+
extensions: ['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs'],
|
|
58
|
+
|
|
59
|
+
// environment -> base URL; guessed from the source when absent
|
|
60
|
+
baseUrls: {
|
|
61
|
+
dev: 'https://dev-api.example.com',
|
|
62
|
+
stage: 'https://stage-api.example.com',
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
// the header the API authenticates with
|
|
66
|
+
auth: { header: 'Authorization' },
|
|
67
|
+
|
|
68
|
+
// how to read pass/fail out of a 2xx body; false turns it off
|
|
69
|
+
envelope: { codeFields: ['status', 'code'], okField: 'success', failFrom: 400 },
|
|
70
|
+
|
|
71
|
+
// the module an endpoint belongs to; the filename by default
|
|
72
|
+
moduleOf: (file, root) => file.split('/').at(-2),
|
|
73
|
+
|
|
74
|
+
// your own scanner, when no preset fits
|
|
75
|
+
parse: (src, config) => [],
|
|
76
|
+
|
|
77
|
+
// an optional multi-step sign-in chain
|
|
78
|
+
login: null,
|
|
79
|
+
};
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Scanner presets
|
|
83
|
+
|
|
84
|
+
### `service-object`
|
|
85
|
+
|
|
86
|
+
One exported function per endpoint, building a request object:
|
|
87
|
+
|
|
88
|
+
```js
|
|
89
|
+
export const getPatients = (params) => {
|
|
90
|
+
const request = { subUrl: `/v1/patients.json`, params };
|
|
91
|
+
return get(request);
|
|
92
|
+
};
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Recognised verbs: `get`, `get2`, `getRequest`, `post`, `postRequest`, `put`,
|
|
96
|
+
`patch`, `delete`, `deletee`, `del`, `destroy`. The request argument may be
|
|
97
|
+
named `request`, `config`, `options` or `req`. Change the path key with
|
|
98
|
+
`pathKey` if yours is not `subUrl`.
|
|
99
|
+
|
|
100
|
+
### `axios-direct`
|
|
101
|
+
|
|
102
|
+
```js
|
|
103
|
+
export const listUsers = () => axios.get('/api/users', { params: { page: 1 } });
|
|
104
|
+
export const addUser = (body) => api.post(`/api/users/${body.id}`, body);
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Any identifier works, not just `axios` — `api`, `http`, `client`.
|
|
108
|
+
|
|
109
|
+
### `fetch-direct`
|
|
110
|
+
|
|
111
|
+
```js
|
|
112
|
+
export const load = () => fetch('/api/things');
|
|
113
|
+
export const save = (b) => fetch('/api/things', { method: 'POST', body: JSON.stringify(b) });
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Path placeholders
|
|
117
|
+
|
|
118
|
+
All three read `${expr}`, `:name` and `{name}` as holes, so Express- and
|
|
119
|
+
OpenAPI-style paths work as well as template literals.
|
|
120
|
+
|
|
121
|
+
### Why regexes and not a parser
|
|
122
|
+
|
|
123
|
+
A parser would need a dependency per language and per flavour of syntax, and
|
|
124
|
+
would still not understand a template literal assembled from three variables. A
|
|
125
|
+
scanner that reads 90% of a codebase and says what it could not read is more
|
|
126
|
+
useful than a parser that refuses to start. Live recording covers the rest: a
|
|
127
|
+
call the scan cannot explain is adopted as an endpoint of its own.
|
|
128
|
+
|
|
129
|
+
Commented-out lines are blanked before scanning, with line numbers preserved.
|
|
130
|
+
Services routinely keep the previous URL directly above the live one, and
|
|
131
|
+
reading the first match would silently take the dead path.
|
|
132
|
+
|
|
133
|
+
### Your own scanner
|
|
134
|
+
|
|
135
|
+
When no preset fits, supply `parse` rather than forking the tool. It is called
|
|
136
|
+
once per file with the source (comments already stripped) and the config, and
|
|
137
|
+
returns one object per endpoint:
|
|
138
|
+
|
|
139
|
+
```js
|
|
140
|
+
export default {
|
|
141
|
+
parse(src, config) {
|
|
142
|
+
return [...src.matchAll(/route\("(\w+)",\s*"([^"]+)"\)/g)].map((m) => ({
|
|
143
|
+
name: m[2].replace(/\W+/g, '_'), // the endpoint's function name
|
|
144
|
+
method: m[1].toUpperCase(), // GET, POST, ...
|
|
145
|
+
subUrl: m[2], // the path template
|
|
146
|
+
usesParams: m[1] === 'get', // sends a query string
|
|
147
|
+
usesData: m[1] !== 'get', // sends a body
|
|
148
|
+
absolute: /^https?:\/\//.test(m[2]),
|
|
149
|
+
chunk: src.slice(m.index, m.index + 300), // scanned for auth quirks
|
|
150
|
+
index: m.index, // for the source line number
|
|
151
|
+
}));
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
`parse` takes precedence over `preset`. Throwing on one file skips that file
|
|
157
|
+
rather than failing the scan.
|
|
158
|
+
|
|
159
|
+
## Base URLs and the auth header
|
|
160
|
+
|
|
161
|
+
Both are read out of your source by default, so your environments stay in sync
|
|
162
|
+
with the app rather than being copied into a config that drifts.
|
|
163
|
+
|
|
164
|
+
Base URLs are found by looking for an object mapping environment names to URLs:
|
|
165
|
+
|
|
166
|
+
```js
|
|
167
|
+
const apiBaseUrls = {
|
|
168
|
+
dev: 'https://dev-api.example.com',
|
|
169
|
+
prod: 'https://api.example.com',
|
|
170
|
+
};
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Recognised environment names: `dev`, `develop`, `development`, `stage`,
|
|
174
|
+
`staging`, `uat`, `qa`, `test`, `prod`, `production`, `local`. Failing that, the
|
|
175
|
+
first API-looking URL becomes `dev`.
|
|
176
|
+
|
|
177
|
+
The auth header is found by looking for a named constant — the strongest signal,
|
|
178
|
+
because a codebase that uses the header in more than one place tends to write it
|
|
179
|
+
that way:
|
|
180
|
+
|
|
181
|
+
```js
|
|
182
|
+
const authTokenKey = 'AUTH_TOKEN';
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
then for header names used as object keys or header lookups.
|
|
186
|
+
|
|
187
|
+
Set either explicitly in the config to override. Consider commenting out a
|
|
188
|
+
production base URL in a generated config, so nobody replays writes against live
|
|
189
|
+
data by selecting the wrong environment.
|
|
190
|
+
|
|
191
|
+
## Sign-in flows
|
|
192
|
+
|
|
193
|
+
Plenty of APIs need more than one call to hand out a usable token. Rather than
|
|
194
|
+
make everyone paste one, describe the chain and the console will drive it.
|
|
195
|
+
|
|
196
|
+
Each step declares the fields to ask for, the call to make, and what to do with
|
|
197
|
+
the answer:
|
|
198
|
+
|
|
199
|
+
```js
|
|
200
|
+
export default {
|
|
201
|
+
login: {
|
|
202
|
+
// optional: a short-lived signed JWT sent with every step
|
|
203
|
+
jwt: {
|
|
204
|
+
secret: process.env.API_SIGNING_KEY,
|
|
205
|
+
secretProd: process.env.API_SIGNING_KEY_PROD,
|
|
206
|
+
headerName: 'ACCESS_TOKEN',
|
|
207
|
+
timestampPrefix: 'MyApp-',
|
|
208
|
+
ttlSeconds: 600,
|
|
209
|
+
// or build the payload yourself
|
|
210
|
+
payload: ({ now, secretCode, env }) => ({ iat: now, exp: now + 600 }),
|
|
211
|
+
},
|
|
212
|
+
|
|
213
|
+
steps: [
|
|
214
|
+
{
|
|
215
|
+
name: 'credentials',
|
|
216
|
+
title: 'Sign in with your email and password.',
|
|
217
|
+
fields: [
|
|
218
|
+
{ name: 'email', label: 'Email', type: 'email' },
|
|
219
|
+
{ name: 'password', label: 'Password', type: 'password' },
|
|
220
|
+
],
|
|
221
|
+
// the scanned endpoint id, so a path change in the app is picked up;
|
|
222
|
+
// `path` is the fallback when there is no catalog entry
|
|
223
|
+
endpointId: 'session.loginApi',
|
|
224
|
+
path: '/users/login.json',
|
|
225
|
+
body: (input) => ({ user: input }),
|
|
226
|
+
then: (data) => ({
|
|
227
|
+
state: { apiToken: data.api_token, phone: data.phone },
|
|
228
|
+
next: 'code',
|
|
229
|
+
}),
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
name: 'code',
|
|
233
|
+
title: 'Enter the code we sent you.',
|
|
234
|
+
fields: [{ name: 'otp', label: 'Code', inputmode: 'numeric' }],
|
|
235
|
+
path: '/users/verify_otp.json',
|
|
236
|
+
headers: (state) => ({ SECRET_TOKEN: state.apiToken }),
|
|
237
|
+
body: (input, state) => ({ otp: input.otp, phone: state.phone }),
|
|
238
|
+
then: (data) => ({
|
|
239
|
+
token: data.auth_token,
|
|
240
|
+
owner: { name: data.name, email: data.email },
|
|
241
|
+
}),
|
|
242
|
+
},
|
|
243
|
+
],
|
|
244
|
+
},
|
|
245
|
+
};
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
### The step contract
|
|
249
|
+
|
|
250
|
+
| Field | |
|
|
251
|
+
| --- | --- |
|
|
252
|
+
| `name` | the step's id, used as `next` from the previous step |
|
|
253
|
+
| `title` | shown above the fields |
|
|
254
|
+
| `fields` | `{ name, label?, type?, inputmode?, placeholder?, options? }`. `options` renders a select. |
|
|
255
|
+
| `endpointId` | a scanned endpoint whose path to use |
|
|
256
|
+
| `path` | the path to POST to, when `endpointId` is absent or unmatched |
|
|
257
|
+
| `headers(state)` | extra headers for this step |
|
|
258
|
+
| `body(input, state)` | the request body from the submitted fields and the carried state |
|
|
259
|
+
| `then(data, state, input)` | what to do with the response |
|
|
260
|
+
|
|
261
|
+
`then` returns any of:
|
|
262
|
+
|
|
263
|
+
| | |
|
|
264
|
+
| --- | --- |
|
|
265
|
+
| `state` | merged into the carried state for later steps |
|
|
266
|
+
| `next` | the name of the step to show next |
|
|
267
|
+
| `choices` | options for a select on the next step |
|
|
268
|
+
| `token` | the auth token — ends the flow and stores it |
|
|
269
|
+
| `owner` | `{ name, email }`, shown on a shared console so people know whose session they are replaying as |
|
|
270
|
+
|
|
271
|
+
The password is forwarded once and is never stored, logged, or written to disk.
|
|
272
|
+
Only the resulting token is kept.
|
|
273
|
+
|
|
274
|
+
A project with no `login` config gets the paste-a-token form, which is all most
|
|
275
|
+
APIs need.
|
package/docs/console.md
ADDED
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
# The console
|
|
2
|
+
|
|
3
|
+
A separate process that maps every endpoint in your source, receives your app's
|
|
4
|
+
live traffic, and lets you fire any endpoint and read the response. It needs no
|
|
5
|
+
framework, no build step and — in most projects — no configuration.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx api-tracer start
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Reads your source, writes a catalog, and opens at `http://127.0.0.1:4400`.
|
|
12
|
+
|
|
13
|
+
## Commands
|
|
14
|
+
|
|
15
|
+
| | |
|
|
16
|
+
| --- | --- |
|
|
17
|
+
| `api-tracer scan` | read the source, write the endpoint catalog |
|
|
18
|
+
| `api-tracer serve` | run the console |
|
|
19
|
+
| `api-tracer start` | scan, then serve |
|
|
20
|
+
| `api-tracer report` | build the insight report |
|
|
21
|
+
| `api-tracer init` | write a config file from what the scan guessed |
|
|
22
|
+
|
|
23
|
+
### Options
|
|
24
|
+
|
|
25
|
+
| | |
|
|
26
|
+
| --- | --- |
|
|
27
|
+
| `--root <dir>` | the project to read (default: cwd) |
|
|
28
|
+
| `--data <dir>` | where captures are kept (default: `<root>/.api-tracer`) |
|
|
29
|
+
| `--port <n>` | default `4400` |
|
|
30
|
+
| `--host <addr>` | default `127.0.0.1` |
|
|
31
|
+
| `--preset <name>` | force a scanner preset |
|
|
32
|
+
| `--format md\|json` | report format (default `md`) |
|
|
33
|
+
| `--out <file>` | write the report to a file (default: stdout) |
|
|
34
|
+
| `--env <name>` | which base URL the report describes |
|
|
35
|
+
| `--force` | let `init` overwrite an existing config |
|
|
36
|
+
|
|
37
|
+
## The catalog
|
|
38
|
+
|
|
39
|
+
`scan` reads your source into `<data>/endpoints.json`: one entry per endpoint,
|
|
40
|
+
with its module, HTTP verb, path template, the `${...}` holes in it, whether it
|
|
41
|
+
sends query params or a body, its auth quirk, and the file and line it came
|
|
42
|
+
from.
|
|
43
|
+
|
|
44
|
+
It is a plain JSON file, so a deployed console needs no source and no build —
|
|
45
|
+
just the file that shipped with it. Re-running `scan` where there is no source
|
|
46
|
+
keeps the catalog that is already there.
|
|
47
|
+
|
|
48
|
+
The scan also counts how many files outside its own module reference each
|
|
49
|
+
endpoint. Anything at zero is flagged **unused in app** and gets its own filter.
|
|
50
|
+
|
|
51
|
+
> This is a substring count, so `import * as services` or a re-export would hide
|
|
52
|
+
> a real usage. Confirm with a grep before deleting anything.
|
|
53
|
+
|
|
54
|
+
See [Configuration](./configuration.md) for how endpoints are found and what to
|
|
55
|
+
do when the presets do not fit your codebase.
|
|
56
|
+
|
|
57
|
+
## Live recording
|
|
58
|
+
|
|
59
|
+
The main way to use it. Run your app normally with `reportTo` pointed at the
|
|
60
|
+
console, and every call it makes streams in as you click — no export, no import,
|
|
61
|
+
no typing payloads.
|
|
62
|
+
|
|
63
|
+
Each request lands on its endpoint in the tree with the real query params, the
|
|
64
|
+
real payload and the real path ids already filled in. The response is scored
|
|
65
|
+
too, so an endpoint your app just used successfully goes green without you
|
|
66
|
+
running anything.
|
|
67
|
+
|
|
68
|
+
Click **Live** in the header to pause or resume. The counter shows what has
|
|
69
|
+
arrived.
|
|
70
|
+
|
|
71
|
+
A call that no endpoint in the catalog explains is **not** dropped — it is
|
|
72
|
+
adopted as an endpoint of its own, marked *not found in source*, and behaves
|
|
73
|
+
like the rest. Recording only what could be matched would quietly hide real
|
|
74
|
+
traffic. **Clear uncatalogued** removes them.
|
|
75
|
+
|
|
76
|
+
## Reading pass and fail
|
|
77
|
+
|
|
78
|
+
Some APIs return HTTP 200 for failures and put the real verdict in the envelope:
|
|
79
|
+
|
|
80
|
+
```json
|
|
81
|
+
{ "status": 801, "success": false, "message": "Authentication token header missing" }
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
So an endpoint counts as **passing** only when the HTTP status is 2xx **and**
|
|
85
|
+
the body's status code is not at or above the failure threshold **and** the
|
|
86
|
+
success flag is not `false`. The response viewer shows the HTTP code and the
|
|
87
|
+
body code side by side.
|
|
88
|
+
|
|
89
|
+
Configure the convention with `envelope` in the config file, or set it to
|
|
90
|
+
`false` if your API uses the status line honestly.
|
|
91
|
+
|
|
92
|
+
## Using one endpoint
|
|
93
|
+
|
|
94
|
+
Pick it in the tree. The request pane is tabbed like Postman:
|
|
95
|
+
|
|
96
|
+
- **Params** — query params, as JSON, stringified into one param, or as a raw
|
|
97
|
+
query string
|
|
98
|
+
- **Headers** — extra headers, merged on top of the token and `Content-Type` the
|
|
99
|
+
server adds
|
|
100
|
+
- **Body** — the type selector: `none` / `JSON` / `multipart/form-data` /
|
|
101
|
+
`x-www-form-urlencoded`. JSON gives a validated editor; the form types give
|
|
102
|
+
key/value rows. Switching type keeps your values.
|
|
103
|
+
- **Capture** — rules that store response values as `{{variables}}`
|
|
104
|
+
|
|
105
|
+
The body type is preselected from what your app actually sent, so a captured
|
|
106
|
+
`FormData` call opens on **multipart/form-data** with its fields already in the
|
|
107
|
+
table, and **Send** replays it that way. Replaying a form post as JSON would
|
|
108
|
+
fail for reasons that have nothing to do with the endpoint.
|
|
109
|
+
|
|
110
|
+
Also on the pane: the source location (`src/services/appointments.js:91`), the
|
|
111
|
+
resolved URL updating live as you type, a field per path placeholder, and after
|
|
112
|
+
**Send** (or Cmd/Ctrl+Enter) the status, body code, latency, size, response body
|
|
113
|
+
and headers. **Copy as cURL** leaves the token as a placeholder.
|
|
114
|
+
|
|
115
|
+
JSON editors are syntax highlighted with line numbers and Tab indents rather
|
|
116
|
+
than leaving the field — a coloured layer behind a transparent textarea, so
|
|
117
|
+
there is no editor library and no dependency.
|
|
118
|
+
|
|
119
|
+
### Query params: three modes
|
|
120
|
+
|
|
121
|
+
Switching carries what you typed across.
|
|
122
|
+
|
|
123
|
+
- **JSON** — `{"page": 1, "ids": [3, 4], "filter": {"status": "new"}}`, serialized
|
|
124
|
+
the way axios does by default: `?page=1&ids[]=3&ids[]=4&filter[status]=new`
|
|
125
|
+
- **stringified** — the whole object in one encoded param,
|
|
126
|
+
`?params=%7B%22page%22%3A1%7D`. The param name is editable.
|
|
127
|
+
- **query string** — pasted straight from DevTools and sent byte-for-byte, so an
|
|
128
|
+
already-encoded `%2B` stays `%2B` instead of becoming `%252B`
|
|
129
|
+
|
|
130
|
+
The URL bar shows the fully resolved URL in every mode, built by the same rules
|
|
131
|
+
the server uses, so the preview never disagrees with what goes out.
|
|
132
|
+
|
|
133
|
+
## Bulk runs and replay
|
|
134
|
+
|
|
135
|
+
**Run every GET endpoint** and **Run all GETs in this module** check many at
|
|
136
|
+
once, six in parallel.
|
|
137
|
+
|
|
138
|
+
Two deliberate limits on bulk GET runs:
|
|
139
|
+
|
|
140
|
+
- **Only GETs are ever run in bulk.** A bulk run that fired nineteen DELETEs
|
|
141
|
+
would be a very bad afternoon.
|
|
142
|
+
- **Path placeholders are left blank**, so `/appointments/${data.id}.json` is
|
|
143
|
+
called as `/appointments/.json`. That proves the endpoint answers; it does not
|
|
144
|
+
prove it behaves correctly with a real id. Treat bulk results as a smoke test.
|
|
145
|
+
|
|
146
|
+
For each placeholder a run takes the first of: the value captured from real
|
|
147
|
+
traffic, a variable sharing the placeholder's name, then blank. The run reports
|
|
148
|
+
how many endpoints went out blank, so a weak result never looks like a strong
|
|
149
|
+
one, and every saved result records the URL that was actually called.
|
|
150
|
+
|
|
151
|
+
### Replay every captured call
|
|
152
|
+
|
|
153
|
+
Re-sends every endpoint that has a sample — GET and writes alike — with the
|
|
154
|
+
payload, query params, path ids and body type your app used.
|
|
155
|
+
|
|
156
|
+
It shows exactly what it will do before it does anything: a count per method,
|
|
157
|
+
the environment and base URL, and what it is leaving out. Defaults, all
|
|
158
|
+
changeable in that dialog:
|
|
159
|
+
|
|
160
|
+
- **DELETEs excluded.** Opt in with a checkbox; the endpoints are named.
|
|
161
|
+
- **Logout excluded**, since it can invalidate the token the rest depends on.
|
|
162
|
+
- **Endpoints needing a signed token excluded.**
|
|
163
|
+
- **Payloads containing a stripped secret are skipped**, because sending the
|
|
164
|
+
literal `<redacted>` would fail for a reason unrelated to the endpoint.
|
|
165
|
+
- **Runs one at a time**, in order. Captured writes often depend on each other,
|
|
166
|
+
and a failure order you cannot reproduce is worse than a slow run.
|
|
167
|
+
- **Refused entirely on a `prod` environment.**
|
|
168
|
+
|
|
169
|
+
This is not a dry run. Every POST creates a new record each time.
|
|
170
|
+
|
|
171
|
+
While it runs the dialog becomes a progress view: a pass/fail bar, a running
|
|
172
|
+
count, the call in flight, and the last few results. **Stop** halts after the
|
|
173
|
+
call in progress. Results survive navigating away and reloading, because the run
|
|
174
|
+
is held on the server.
|
|
175
|
+
|
|
176
|
+
A call that fails during a replay keeps its response — open that endpoint and
|
|
177
|
+
the response pane shows what came back, labelled *from the last replay*, until
|
|
178
|
+
you send it again yourself. Those endpoints are marked **failed when replayed**.
|
|
179
|
+
|
|
180
|
+
Results are saved and survive a restart, so you can see what changed since
|
|
181
|
+
yesterday. An endpoint that was passing and now fails is marked **broke since
|
|
182
|
+
last run**. Switching environment clears them, since dev results would be
|
|
183
|
+
misleading on stage.
|
|
184
|
+
|
|
185
|
+
## Contract drift
|
|
186
|
+
|
|
187
|
+
A status check cannot see the failure that matters most on an API that answers
|
|
188
|
+
`200` for everything: a backend that quietly drops a field, or changes an id
|
|
189
|
+
from a number to a string, still looks green.
|
|
190
|
+
|
|
191
|
+
So every passing response is reduced to its **shape** — field paths and types,
|
|
192
|
+
values ignored — and compared with the endpoint's baseline. The first good
|
|
193
|
+
response becomes the baseline; after that a change is reported:
|
|
194
|
+
|
|
195
|
+
```
|
|
196
|
+
-data[].pinned, +data[].author, data[].id: number->string
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Drifted endpoints get an amber dot in the tree, a panel on the endpoint, and
|
|
200
|
+
their own filter. When the change is intended, **This change is expected, make
|
|
201
|
+
it the baseline** promotes the new shape and clears the flag.
|
|
202
|
+
|
|
203
|
+
Live traffic feeds this for free: your app's own calls set and check the
|
|
204
|
+
baseline as you use it.
|
|
205
|
+
|
|
206
|
+
## Coverage
|
|
207
|
+
|
|
208
|
+
The home screen shows how much of the API your app has actually exercised — how
|
|
209
|
+
many endpoints have ever been seen, which modules remain untouched, and how many
|
|
210
|
+
have drifted. Module rows show `seen/total`.
|
|
211
|
+
|
|
212
|
+
An endpoint nobody has triggered has not been proven either way, which is
|
|
213
|
+
different from failing. The **never captured** filter is the to-do list.
|
|
214
|
+
|
|
215
|
+
## Variables and chaining
|
|
216
|
+
|
|
217
|
+
Use `{{name}}` in any path value, query param or payload. Set them by hand in
|
|
218
|
+
**Variables**, or capture them from a response: a rule like
|
|
219
|
+
`patient_id = data.0.id` stores that value after a passing call, for every later
|
|
220
|
+
call. Dotted paths walk arrays. A path that finds nothing reports an error
|
|
221
|
+
rather than silently storing a blank.
|
|
222
|
+
|
|
223
|
+
Variables are not a place for tokens — those stay in the console's own token
|
|
224
|
+
store.
|
|
225
|
+
|
|
226
|
+
## Import
|
|
227
|
+
|
|
228
|
+
For traffic captured earlier, or from someone else's session.
|
|
229
|
+
|
|
230
|
+
1. Open your app in Chrome, DevTools → **Network**, tick **Preserve log**.
|
|
231
|
+
2. Click through the screens you care about.
|
|
232
|
+
3. Right-click the request list → **Save all as HAR with content**.
|
|
233
|
+
4. Drop the file anywhere on the console page.
|
|
234
|
+
|
|
235
|
+
Each request is matched back to the endpoint the scanner found, by method and
|
|
236
|
+
path shape, so a real call to `POST /appointments/4821/accept` lands on the
|
|
237
|
+
template `/appointments/${data.id}/accept`. From it the console keeps the path
|
|
238
|
+
id, the query params and the request body, which become the endpoint's starting
|
|
239
|
+
values.
|
|
240
|
+
|
|
241
|
+
For a single request, **Copy as cURL** in DevTools and paste it into the import
|
|
242
|
+
dialog.
|
|
243
|
+
|
|
244
|
+
The import reports what it could not place — usually an endpoint the backend has
|
|
245
|
+
but the frontend does not call yet.
|
|
246
|
+
|
|
247
|
+
## Export
|
|
248
|
+
|
|
249
|
+
Every module page has **Export HAR** and **Export cURL**; the home screen has
|
|
250
|
+
**Export all captured**.
|
|
251
|
+
|
|
252
|
+
| | |
|
|
253
|
+
| --- | --- |
|
|
254
|
+
| **HAR** | a valid HAR 1.2 file with request, query string, payload and recorded response body. Opens in Chrome DevTools, imports into Postman, and re-imports here. |
|
|
255
|
+
| **cURL** | one runnable command per endpoint, labelled with its source location |
|
|
256
|
+
| **Postman** | a Collection v2.1. Modules become folders; each request carries the real query params and body in the right mode, so a captured form post imports as form-data, not broken JSON. Host and token are left as `{{baseUrl}}` and `{{token}}`. |
|
|
257
|
+
|
|
258
|
+
One entry per endpoint, not per call — the most recent capture wins, which keeps
|
|
259
|
+
an export of a busy session readable.
|
|
260
|
+
|
|
261
|
+
All exports carry `<paste your token>` in place of a real token, so an export is
|
|
262
|
+
safe to attach to a ticket.
|
|
263
|
+
|
|
264
|
+
## The report
|
|
265
|
+
|
|
266
|
+
**Report** in the header builds an insight report from everything the console
|
|
267
|
+
already holds. No extra capture, no new run.
|
|
268
|
+
|
|
269
|
+
| Section | |
|
|
270
|
+
| --- | --- |
|
|
271
|
+
| **Coverage** | exercised vs total, per module, and which modules nobody has walked |
|
|
272
|
+
| **Failing** | status, body code and the message pulled out of the envelope, with regressions and replay failures marked |
|
|
273
|
+
| **Contract drift** | response shapes that changed, with the field-level diff |
|
|
274
|
+
| **Risks** | writes never exercised, captured DELETEs, 2xx responses carrying an error code, payloads whose secrets were stripped |
|
|
275
|
+
| **Hygiene** | endpoints referenced nowhere, duplicate routes, calls not found in the source |
|
|
276
|
+
| **Auth surface** | which endpoints need which token, and which third-party ones deliberately get none |
|
|
277
|
+
| **Latency** | median, p95 and the slowest endpoints |
|
|
278
|
+
| **Inventory** | request params, body fields and response fields per captured endpoint, from real traffic rather than hand-written docs |
|
|
279
|
+
| **Trend** | pass rate and coverage over time, one line appended per run |
|
|
280
|
+
|
|
281
|
+
**Export Markdown** for a ticket, **Export JSON** for CI. Or from the command
|
|
282
|
+
line:
|
|
283
|
+
|
|
284
|
+
```bash
|
|
285
|
+
npx api-tracer report --format json --out api-report.json
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
The latency numbers are one measurement per endpoint, from its most recent call
|
|
289
|
+
— indicative, not a benchmark.
|
|
290
|
+
|
|
291
|
+
## Signing in
|
|
292
|
+
|
|
293
|
+
Click **Set token** and paste one. The token is held by the server, never sent
|
|
294
|
+
back to the page, and written owner-only (`chmod 600`) to the data directory so
|
|
295
|
+
a restart does not sign you out. **Sign out** deletes it.
|
|
296
|
+
|
|
297
|
+
If your API has a multi-step sign-in worth automating — password, then a
|
|
298
|
+
one-time code, then an organisation — describe it in the config file and the
|
|
299
|
+
console will drive it, so nobody has to paste anything. See
|
|
300
|
+
[sign-in flows](./configuration.md#sign-in-flows).
|
|
301
|
+
|
|
302
|
+
Third-party endpoints (anything flagged absolute) never receive your token.
|
|
303
|
+
|
|
304
|
+
## Why calls go through the server
|
|
305
|
+
|
|
306
|
+
The page posts the composed request to the console and the server forwards it.
|
|
307
|
+
Calling the API from the page directly would be blocked by CORS, and the token
|
|
308
|
+
would have to sit in client-side JavaScript.
|
|
309
|
+
|
|
310
|
+
## Getting around
|
|
311
|
+
|
|
312
|
+
- **The logo** goes back to the overview from anywhere
|
|
313
|
+
- **Breadcrumbs** at the top of every page, each part clickable
|
|
314
|
+
- **Escape** backs out one level — endpoint, then module, then overview. Ignored
|
|
315
|
+
while you are typing or a dialog is open.
|
|
316
|
+
- Filters: method, and pass/fail/not-run. Search matches function name, path and
|
|
317
|
+
module.
|
|
318
|
+
|
|
319
|
+
Typed path values and payloads are kept in the browser's local storage, so a
|
|
320
|
+
reload does not lose your work.
|
|
321
|
+
|
|
322
|
+
## Environment variables
|
|
323
|
+
|
|
324
|
+
| | |
|
|
325
|
+
| --- | --- |
|
|
326
|
+
| `API_TRACER_DATA` | where captures are kept (default `<project>/.api-tracer`) |
|
|
327
|
+
| `PORT` / `HOST` | default `4400` / `127.0.0.1` |
|
|
328
|
+
| `BASE_PATH` | serve under a path, e.g. `/api-console`, behind a proxy |
|
|
329
|
+
| `ALLOWED_ORIGINS` | comma-separated origins allowed to post recordings; localhost always works |
|
|
330
|
+
| `CONSOLE_USER` / `CONSOLE_PASS` | basic auth on everything except `/api/record` |
|
|
331
|
+
| `LOCK_RECORDING=1` | recording cannot be paused from the UI |
|
|
332
|
+
| `LOCK_ENV=<name>` | pins the environment |
|
|
333
|
+
|
|
334
|
+
`HOST` stays on `127.0.0.1` by default, so a laptop instance is not exposed by
|
|
335
|
+
accident.
|
|
336
|
+
|
|
337
|
+
## Running one for a team
|
|
338
|
+
|
|
339
|
+
Every developer whose app has the tracer posts to the console whether or not
|
|
340
|
+
they ever open it. The captures accumulate: one person exercises appointments,
|
|
341
|
+
another exercises rota, and the console ends up holding both.
|
|
342
|
+
|
|
343
|
+
Two variables stop one person's click changing things for everyone:
|
|
344
|
+
|
|
345
|
+
```bash
|
|
346
|
+
LOCK_RECORDING=1 LOCK_ENV=dev npx api-tracer serve
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
- Without `LOCK_RECORDING`, one developer clicking **Live** silently stops
|
|
350
|
+
recording for the whole team.
|
|
351
|
+
- Without `LOCK_ENV`, someone switching to stage redirects everyone's next
|
|
352
|
+
replay and clears the shared results.
|
|
353
|
+
|
|
354
|
+
The console also shows **whose** session the shared token belongs to, since
|
|
355
|
+
everyone replays as that user.
|
|
356
|
+
|
|
357
|
+
`CONSOLE_USER` / `CONSOLE_PASS` turn on basic auth. **`/api/record` stays
|
|
358
|
+
open**, because the app posts recordings from a browser that cannot send those
|
|
359
|
+
credentials. The gate protects reading captures, replaying calls and the stored
|
|
360
|
+
token; it does not stop someone who can reach the host from posting junk
|
|
361
|
+
recordings. If that matters, put the whole thing behind VPN or SSO.
|
|
362
|
+
|
|
363
|
+
It is single-user by design: one token, one environment, one set of results. Two
|
|
364
|
+
people on a shared instance will overwrite each other's environment and results.
|
|
365
|
+
|
|
366
|
+
### Behind a proxy
|
|
367
|
+
|
|
368
|
+
The mount path is forwarded as-is, so tell the console where it lives:
|
|
369
|
+
|
|
370
|
+
```bash
|
|
371
|
+
BASE_PATH=/api-console HOST=0.0.0.0 ALLOWED_ORIGINS=https://app.example.com npx api-tracer serve
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
Then point the tracer at the same path:
|
|
375
|
+
|
|
376
|
+
```ts
|
|
377
|
+
apiTracer.init({ reportTo: '/api-console' });
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
Same origin means no CORS and no mixed content.
|
|
381
|
+
|
|
382
|
+
> **Mixed content.** An HTTPS app cannot post to an `http://` console; the
|
|
383
|
+
> browser blocks it silently. Terminate TLS in front of the console, or serve it
|
|
384
|
+
> under a path on the app's own origin.
|
|
385
|
+
|
|
386
|
+
## HTTP API
|
|
387
|
+
|
|
388
|
+
Everything the UI does is available directly.
|
|
389
|
+
|
|
390
|
+
| Route | |
|
|
391
|
+
| --- | --- |
|
|
392
|
+
| `GET /api/endpoints` | catalog, base URLs, auth info, results, samples, live state |
|
|
393
|
+
| `POST /api/token` | store or clear a token |
|
|
394
|
+
| `GET/POST /api/login` | the configured sign-in flow: the next step, and submitting it |
|
|
395
|
+
| `POST /api/env` | switch environment |
|
|
396
|
+
| `POST /api/call` | send one endpoint |
|
|
397
|
+
| `POST /api/run` | bulk run; GETs only unless explicitly allowed |
|
|
398
|
+
| `POST /api/run/preview` | what a run would do |
|
|
399
|
+
| `GET /api/run/status`, `POST /api/run/cancel` | progress and cancellation |
|
|
400
|
+
| `POST /api/record` | one live call from the running app |
|
|
401
|
+
| `GET/POST/DELETE /api/live` | recorder state: counts, pause, reset |
|
|
402
|
+
| `GET/POST/DELETE /api/contracts` | response-shape baselines; POST accepts a drift |
|
|
403
|
+
| `GET /api/export` | `?module=&format=har\|curl\|postman` |
|
|
404
|
+
| `POST /api/import` | import a HAR or a cURL command |
|
|
405
|
+
| `DELETE /api/samples` | drop all captured samples |
|
|
406
|
+
| `DELETE /api/uncatalogued` | drop adopted endpoints |
|
|
407
|
+
| `GET/POST /api/variables` | read or set `{{variables}}` |
|
|
408
|
+
| `DELETE /api/results` | clear saved results |
|
|
409
|
+
| `GET /api/report`, `GET /api/report/export` | the report model, and `?format=md\|json` |
|
|
410
|
+
|
|
411
|
+
## What lives in the data directory
|
|
412
|
+
|
|
413
|
+
`<project>/.api-tracer/`, or wherever `--data` points.
|
|
414
|
+
|
|
415
|
+
| | |
|
|
416
|
+
| --- | --- |
|
|
417
|
+
| `endpoints.json` | the catalog, written by `scan` |
|
|
418
|
+
| `samples.json` | requests captured from real traffic |
|
|
419
|
+
| `results.json` | the last result per endpoint |
|
|
420
|
+
| `contracts.json` | response-shape baselines |
|
|
421
|
+
| `uncatalogued.json` | endpoints adopted from traffic |
|
|
422
|
+
| `variables.json` | `{{variables}}` |
|
|
423
|
+
| `lastrun.json`, `runs.jsonl` | the last run, and one line per run for the trend |
|
|
424
|
+
| `tokens.json` | the session token, written `chmod 600` |
|
|
425
|
+
|
|
426
|
+
**Add `.api-tracer/` to your `.gitignore`.** It contains real request and
|
|
427
|
+
response bodies from the environment you are watching, and a real token. See
|
|
428
|
+
[Security](./security.md).
|