@hakam-aldeen-kh/blix 0.11.0 → 0.13.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/README.md +33 -1537
- package/dist/{DevToolsPanel-M5NWMB3M.js → DevToolsPanel-CGZVZ6HP.js} +1998 -1413
- package/dist/capture/index.js +3 -3
- package/dist/{chunk-P5272747.js → chunk-IWBXQGRD.js} +4 -482
- package/dist/{chunk-IWW5LH64.js → chunk-PP53X2ZA.js} +653 -171
- package/dist/index.js +4 -4
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -1,1567 +1,63 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
<h1>
|
|
2
|
+
<picture>
|
|
3
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/Hakam-aldeen-Kh/blix/main/docs/brand/blix-lockup-dark-bg.png">
|
|
4
|
+
<img alt="blix" src="https://raw.githubusercontent.com/Hakam-aldeen-Kh/blix/main/docs/brand/blix-lockup-light-bg.png" height="64">
|
|
5
|
+
</picture>
|
|
6
|
+
</h1>
|
|
3
7
|
|
|
4
|
-
An in-app dev-tools panel for React
|
|
5
|
-
actions, TanStack Query cache events and realtime traffic, and renders them in
|
|
6
|
-
a dockable panel with cross-source links, diffing, replay and HAR/cURL
|
|
7
|
-
export.
|
|
8
|
+
An in-app dev-tools panel for React developers: every HTTP request, Redux action, TanStack Query cache event and realtime frame your app produces, in one place, while you build.
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
[Production elimination](#production-elimination). To keep it on a staging
|
|
11
|
-
server, see [Enabling Blix in a production build](#enabling-blix-in-a-production-build-staging).
|
|
10
|
+
**[blixjs.dev](https://blixjs.dev)**
|
|
12
11
|
|
|
13
|
-
|
|
12
|
+
[](https://www.npmjs.com/package/@hakam-aldeen-kh/blix) [](https://github.com/Hakam-aldeen-Kh/blix/actions/workflows/ci.yml) [](https://github.com/Hakam-aldeen-Kh/blix/blob/main/LICENSE)
|
|
14
13
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
## Install
|
|
14
|
+
## Why Blix
|
|
18
15
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
16
|
+
- **Four sources, one panel.** HTTP through `fetch` and axios, Redux actions with their state diffs, TanStack Query cache events, and realtime frames. The panel floats or docks to any edge of the page.
|
|
17
|
+
- **Linked across sources.** When a query caused a request, or one entry is a replay of another, the row links to the other side. One click instead of matching timestamps by hand.
|
|
18
|
+
- **Plaintext on both legs.** Attached after your own axios interceptors, Blix records the request body before you encrypt it and the response after you decrypt it.
|
|
19
|
+
- **Replay and export.** Re-send an axios request or re-dispatch a Redux action. Copy any request as cURL or `fetch`, or export the log as HAR, JSON, NDJSON, Markdown, CSV or a cURL script. `Authorization`, `Cookie`, `Set-Cookie` and `X-API-Key` values are masked in every one of them.
|
|
20
|
+
- **Gone from production builds.** The panel sits behind a literal `process.env.NODE_ENV === "development"` check, so it never loads in production. Capture calls become no-ops, and a one-line guard at the call site removes them from the bundle too. In Next.js, also set `NEXT_PUBLIC_BLIX=false` in your production env, or the panel's unused chunk is still emitted.
|
|
22
21
|
|
|
23
|
-
|
|
22
|
+
## Quick start
|
|
24
23
|
|
|
25
24
|
```bash
|
|
26
25
|
npm install -D @hakam-aldeen-kh/blix
|
|
27
26
|
```
|
|
28
27
|
|
|
29
|
-
|
|
30
|
-
`<Blix />` imports it from your application code — so any build that drops
|
|
31
|
-
devDependencies *before* the build step (`npm ci --omit=dev`,
|
|
32
|
-
`pnpm install --prod`, the usual shape of a multi-stage Docker image) fails
|
|
33
|
-
while resolving that import. devDependencies have to be present at build time.
|
|
34
|
-
Dropping them from the final runtime image is fine: nothing from Blix reaches
|
|
35
|
-
the production output anyway.
|
|
36
|
-
|
|
37
|
-
The package ships **ESM only**. Both entry points declare `"types"` and
|
|
38
|
-
`"import"` in `exports`, with no `"require"` and no `"default"` fallback, so
|
|
39
|
-
`require("@hakam-aldeen-kh/blix")` and CJS-only tooling — an older Jest config
|
|
40
|
-
is the usual one — fail with `ERR_PACKAGE_PATH_NOT_EXPORTED`. That is by
|
|
41
|
-
design, not a packaging bug: a dev tool that is eliminated from production has
|
|
42
|
-
no reason to carry a second build output.
|
|
43
|
-
|
|
44
|
-
### Peer dependencies
|
|
45
|
-
|
|
46
|
-
`react` and `react-dom` (v18 or v19) are required. `axios`, `@reduxjs/toolkit` and
|
|
47
|
-
`@tanstack/react-query` are **optional** peers — you only need the ones whose
|
|
48
|
-
capture you actually use. The capture layer is structurally typed against each
|
|
49
|
-
of them and imports none of them — not at runtime, and not in its published
|
|
50
|
-
type declarations — so installing Blix does not pull a data-fetching or state
|
|
51
|
-
library into your tree, and your type-check never goes looking for one.
|
|
52
|
-
|
|
53
|
-
**axios is optional, and fetch-only apps are fully supported.** An app that
|
|
54
|
-
calls `fetch` directly — the default in a Next.js App Router project — captures
|
|
55
|
-
its HTTP traffic with
|
|
56
|
-
[`attachFetchMonitor`](#http-fetch--attachfetchmonitoroptions) and never needs
|
|
57
|
-
axios installed, not even for types.
|
|
58
|
-
|
|
59
|
-
---
|
|
60
|
-
|
|
61
|
-
## Upgrading to 0.11
|
|
62
|
-
|
|
63
|
-
Four things behave differently, and none needs a code change unless you read
|
|
64
|
-
entries yourself:
|
|
65
|
-
|
|
66
|
-
- **`entry.error` is an object.** A failed request stores a
|
|
67
|
-
[`CapturedError`](#entryerror--capturederror) — `kind`, `message`, `status`,
|
|
68
|
-
… — instead of the response body or a string. The body is on
|
|
69
|
-
`error.response`. Anything that read `entry.error` from an export has to
|
|
70
|
-
follow it there.
|
|
71
|
-
- **Saved logs are cleared once.** Logs written by earlier versions are
|
|
72
|
-
discarded the first time 0.11 opens the database. Panel preferences are
|
|
73
|
-
kept.
|
|
74
|
-
- **Docked panels push the page.** A panel docked to an edge reserves that side
|
|
75
|
-
with inline padding on `<html>` rather than covering the app, and no longer
|
|
76
|
-
closes on an outside click. `position: fixed` and `sticky` elements are not
|
|
77
|
-
moved by padding — opt them in with `--blix-dock-top`, `-right`, `-bottom`
|
|
78
|
-
and `-left`, e.g. `right: var(--blix-dock-right, 0px)`. See
|
|
79
|
-
[Finding your way around](#finding-your-way-around).
|
|
80
|
-
- **Shortcuts only act inside the panel.** Single-key shortcuts no longer fire
|
|
81
|
-
while focus is in your app; `Ctrl/⌘` + backtick still toggles the panel from
|
|
82
|
-
anywhere.
|
|
83
|
-
|
|
84
|
-
---
|
|
85
|
-
|
|
86
|
-
## The module-init call-order contract
|
|
87
|
-
|
|
88
|
-
**This is the part that is easy to get wrong.** Capture functions must be
|
|
89
|
-
called where the thing they wrap is *constructed*, at module scope — not from
|
|
90
|
-
inside a component body or a `useEffect`.
|
|
91
|
-
|
|
92
|
-
If you call them from a component, capture only starts once React mounts, and
|
|
93
|
-
every request fired before that point — auth bootstrap, session restore,
|
|
94
|
-
prefetches, anything at module-eval time — is silently missed. The panel then
|
|
95
|
-
shows a log with a hole at the beginning, which is exactly when you most need
|
|
96
|
-
it.
|
|
97
|
-
|
|
98
|
-
> **Import capture from `@hakam-aldeen-kh/blix/capture`, not from the package
|
|
99
|
-
> root.** The root entry carries a `"use client"` directive so that `<Blix />`
|
|
100
|
-
> can be rendered from a server component, which means every module that
|
|
101
|
-
> imports from it lands inside a client boundary. Your axios module, your store
|
|
102
|
-
> and your realtime adapter generally also evaluate on the server — in the
|
|
103
|
-
> Next.js App Router they always do — where the root entry is at best dead
|
|
104
|
-
> weight and at worst a boundary violation. Keep the root import in the single
|
|
105
|
-
> client component that mounts the panel. See [Entry points](#entry-points).
|
|
106
|
-
|
|
107
|
-
| Function | Where to call it | Timing |
|
|
108
|
-
| --- | --- | --- |
|
|
109
|
-
| `attachHttpMonitor` | after your own interceptors are registered on the instance | module scope |
|
|
110
|
-
| `attachFetchMonitor` | before the first `fetch` you want captured — client-side only | module scope |
|
|
111
|
-
| `createReduxMonitorMiddleware` | in `configureStore`'s `middleware` callback | module scope |
|
|
112
|
-
| `tapRealtimeAdapter` | where the adapter singleton is constructed | module scope |
|
|
113
|
-
| `tapQueryClient` | a `useEffect` in your query provider | see below |
|
|
114
|
-
| `captureEncrypted` | inside your own encrypt/decrypt functions, on success paths only | per request — optional, see below |
|
|
115
|
-
|
|
116
|
-
Every one of these is a no-op in production, but the call sites still cost you
|
|
117
|
-
bytes unless you guard them. See
|
|
118
|
-
[Guard your call sites](#guard-your-call-sites) — it is one line per site and
|
|
119
|
-
it is the difference between shipping the capture runtime and not.
|
|
120
|
-
|
|
121
|
-
### HTTP — `attachHttpMonitor(instance, options?)`
|
|
122
|
-
|
|
123
|
-
Call it **after your own interceptors are registered**. That one placement is
|
|
124
|
-
what puts Blix on the plaintext side of *both* legs of the request, for two
|
|
125
|
-
different reasons:
|
|
126
|
-
|
|
127
|
-
- **On the way out**, axios runs request interceptors LIFO, so registering last
|
|
128
|
-
makes Blix's interceptor run **first** — before any encryption or
|
|
129
|
-
serialization step. It sees the plaintext body.
|
|
130
|
-
- **On the way back**, axios runs response interceptors FIFO, so registering
|
|
131
|
-
last makes Blix's interceptor run **last** — after your decrypt interceptor.
|
|
132
|
-
It sees the decrypted body.
|
|
133
|
-
|
|
134
|
-
The two orders are opposite, and they happen to agree on the same answer:
|
|
135
|
-
register last.
|
|
136
|
-
|
|
137
|
-
> **Registering earlier is a silent wrong reading, not an error.** If
|
|
138
|
-
> `attachHttpMonitor` runs before your decrypt interceptor, Blix's response
|
|
139
|
-
> interceptor runs before it too, and the **Response** tab fills with
|
|
140
|
-
> ciphertext presented as an ordinary response body. Nothing throws and nothing
|
|
141
|
-
> warns — the panel just shows you base64 where it should show you an object.
|
|
142
|
-
> On the request side the mirror-image mistake gives you a **Payload** tab full
|
|
143
|
-
> of ciphertext.
|
|
144
|
-
|
|
145
|
-
> **axios 1.19+ can invert the request-side half.** The request-interceptor
|
|
146
|
-
> LIFO order is now governed by the transitional flag
|
|
147
|
-
> `legacyInterceptorReqResOrdering`, which still defaults to `true`. If you set
|
|
148
|
-
> `transitional: { legacyInterceptorReqResOrdering: false }`, request
|
|
149
|
-
> interceptors become FIFO and the request-side rule flips to "register
|
|
150
|
-
> `attachHttpMonitor` **first**" — while the response-side rule still says
|
|
151
|
-
> last, so the two orders no longer agree and you must pick which leg matters
|
|
152
|
-
> more. Response-interceptor order is unaffected by the flag. Blix warns once,
|
|
153
|
-
> in development, when a request carries the flag set to `false`.
|
|
154
|
-
|
|
155
|
-
```ts
|
|
156
|
-
// src/network/axios.ts
|
|
157
|
-
import axios from "axios";
|
|
158
|
-
import { attachHttpMonitor, withInitiatorCapture } from "@hakam-aldeen-kh/blix/capture";
|
|
159
|
-
|
|
160
|
-
export const apiClient = axios.create({ baseURL: "/api" });
|
|
161
|
-
|
|
162
|
-
apiClient.interceptors.request.use(addAuthHeader);
|
|
163
|
-
apiClient.interceptors.request.use(encryptBody);
|
|
164
|
-
|
|
165
|
-
// After your interceptors, at module scope — NOT in a hook, NOT in a component.
|
|
166
|
-
if (process.env.NODE_ENV === "development" && typeof window !== "undefined") {
|
|
167
|
-
attachHttpMonitor(apiClient);
|
|
168
|
-
}
|
|
169
|
-
```
|
|
170
|
-
|
|
171
|
-
`attachHttpMonitor` is idempotent per instance — a second call on the same
|
|
172
|
-
instance, or on its `withInitiatorCapture` wrapper, does nothing — and it
|
|
173
|
-
returns a disposer that ejects both interceptors, for HMR and tests. A request
|
|
174
|
-
cancelled through an `AbortController` or a `CancelToken` settles as
|
|
175
|
-
**aborted**, not as an error; a timeout is still an error.
|
|
176
|
-
|
|
177
|
-
#### Factory and lazy-singleton clients
|
|
178
|
-
|
|
179
|
-
The rule is **causal, not positional**. "Bottom of the module" is shorthand
|
|
180
|
-
that only holds when your interceptors are registered by statements physically
|
|
181
|
-
above the call. If your instance comes from a factory or a lazy singleton, the
|
|
182
|
-
interceptors are registered inside that factory, on first call — so what
|
|
183
|
-
matters is that *something has already triggered construction*:
|
|
184
|
-
|
|
185
|
-
```ts
|
|
186
|
-
// ApiClientFactory.getInstance() registers the interceptors on its first call.
|
|
187
|
-
export const apiClient = withInitiatorCapture(ApiClientFactory.getInstance());
|
|
188
|
-
|
|
189
|
-
// Safe: getInstance() ran on the line above, so the interceptors exist by now.
|
|
190
|
-
if (process.env.NODE_ENV === "development" && typeof window !== "undefined") {
|
|
191
|
-
attachHttpMonitor(apiClient);
|
|
192
|
-
}
|
|
193
|
-
```
|
|
194
|
-
|
|
195
|
-
If nothing above the call has constructed the instance, `attachHttpMonitor`
|
|
196
|
-
registers **first** rather than last, and you get the silent wrong reading
|
|
197
|
-
described above. There is no need for `queueMicrotask`, `setTimeout` or any
|
|
198
|
-
other deferral: Blix does not require one, and deferring only hides whether the
|
|
199
|
-
ordering is actually correct.
|
|
200
|
-
|
|
201
|
-
#### Failed requests and the shape of your rejection
|
|
202
|
-
|
|
203
|
-
Blix records how a request ended **at the wire**: before the first of your
|
|
204
|
-
response interceptors runs, while the value is still the `AxiosError` — or the
|
|
205
|
-
response — that axios produced. So a response interceptor that normalises
|
|
206
|
-
errors into your own domain type no longer costs you the entry:
|
|
207
|
-
|
|
208
|
-
```ts
|
|
209
|
-
// Fine since 0.11: the failure was recorded before this ran
|
|
210
|
-
instance.interceptors.response.use(undefined, (error) =>
|
|
211
|
-
Promise.reject(error.response?.data ?? fallbackError),
|
|
212
|
-
);
|
|
213
|
-
```
|
|
214
|
-
|
|
215
|
-
(Through 0.10 this left **every non-2xx request `pending`** for the rest of the
|
|
216
|
-
session, because the value Blix received had no `config` to correlate by.)
|
|
217
|
-
|
|
218
|
-
The entry carries the real failure — status, response body, the client's code.
|
|
219
|
-
What your interceptor handed the caller *instead* is recorded beside it as
|
|
220
|
-
`error.handledAs`, whenever Blix can tie the two together: your error keeps the
|
|
221
|
-
original on `cause`, `response`, `originalError` or `error`, or still carries
|
|
222
|
-
`config`. An error with no link back to the original is simply not shown; the
|
|
223
|
-
entry is still settled correctly.
|
|
224
|
-
|
|
225
|
-
Three limits:
|
|
226
|
-
|
|
227
|
-
- **An error thrown by a request interceptor** never reaches the wire. It is
|
|
228
|
-
attributed to its request when that is the only one in the air; with several
|
|
229
|
-
it is left to the timeout watchdog below rather than guessed at.
|
|
230
|
-
- **A request with a `timeout`** that is still pending 3 s past it is settled
|
|
231
|
-
as a timeout that says Blix saw no answer — and corrected if one arrives
|
|
232
|
-
after all. **A request with no `timeout`** that never answers stays
|
|
233
|
-
`pending`: nothing times it out.
|
|
234
|
-
- The wire is observed by wrapping the first response interceptor already on
|
|
235
|
-
the instance. On an axios whose internals do not have the shape Blix expects
|
|
236
|
-
it is skipped, and the behaviour above falls back to correlating by
|
|
237
|
-
`error.config` alone.
|
|
238
|
-
|
|
239
|
-
#### Response interceptors that unwrap
|
|
240
|
-
|
|
241
|
-
The same holds on the success path. A response interceptor registered before
|
|
242
|
-
Blix that returns something other than the response —
|
|
243
|
-
`(response) => response.data` is the usual one — used to leave the entry
|
|
244
|
-
`pending`. The response is now recorded at the wire, status and headers
|
|
245
|
-
included, before the interceptor unwraps it:
|
|
246
|
-
|
|
247
|
-
- your request is unaffected: your code receives exactly what your interceptor
|
|
248
|
-
returned. (Before 0.7.0 Blix threw at this point, and every request on the
|
|
249
|
-
instance failed.)
|
|
250
|
-
- the Response tab shows the body your code received — the unwrapped,
|
|
251
|
-
decrypted one — when Blix can recognise it as the same object, and the body
|
|
252
|
-
as it came off the wire otherwise.
|
|
253
|
-
|
|
254
|
-
#### Which request headers the panel shows
|
|
255
|
-
|
|
256
|
-
Blix's request interceptor runs before yours, so when it runs, the headers your
|
|
257
|
-
interceptors add — auth, tracing, signing, locale — do not exist yet, and
|
|
258
|
-
neither does the `Content-Type` axios sets itself. So the panel shows the
|
|
259
|
-
headers the request **settled** with, re-read from the config axios hands back
|
|
260
|
-
with the response or the error, and masked the same way. Until then — and for
|
|
261
|
-
good, for a request that never settles or whose rejection carries no `config` —
|
|
262
|
-
it shows the snapshot taken when the request started. JWT claims follow the same
|
|
263
|
-
rule.
|
|
264
|
-
|
|
265
|
-
Some headers never appear, because axios's adapter adds them to its own copy of
|
|
266
|
-
the config, past the last point Blix can read:
|
|
267
|
-
|
|
268
|
-
- **`Authorization: Basic …` from the `auth` option.** When `auth` is set, the
|
|
269
|
-
panel keeps the `Authorization` it saw when the request started: the settled
|
|
270
|
-
config still shows whatever your interceptors set, which is not what was
|
|
271
|
-
sent. Every other header is still taken from the settled config.
|
|
272
|
-
- **The XSRF header** (`X-XSRF-TOKEN` by default).
|
|
273
|
-
- **Headers the browser manages itself** — `Cookie`, `Origin`, `User-Agent` and
|
|
274
|
-
the like. Chrome's Network tab shows them; JavaScript never sees them.
|
|
275
|
-
|
|
276
|
-
#### Encrypted payloads — `captureEncrypted(config, payload)`
|
|
277
|
-
|
|
278
|
-
*Since 0.3.0.*
|
|
279
|
-
|
|
280
|
-
> **axios only.** Correlation works through the config object your own
|
|
281
|
-
> interceptor is holding, and `fetch` has no interceptor stage to hold one in,
|
|
282
|
-
> so `captureEncrypted` has no effect on requests captured by
|
|
283
|
-
> [`attachFetchMonitor`](#http-fetch--attachfetchmonitoroptions).
|
|
284
|
-
|
|
285
|
-
**Entirely optional.** An app that never calls it behaves exactly as it did
|
|
286
|
-
before this API existed, and its panel shows no Encrypted tab at all — the tab
|
|
287
|
-
appears only on entries that actually carry ciphertext.
|
|
288
|
-
|
|
289
|
-
Blix cannot capture the encrypted forms by itself. It has no knowledge of your
|
|
290
|
-
encryption scheme, and — by the design above — its interceptor deliberately
|
|
291
|
-
sits on the *plaintext* side, so at the moment Blix captures, the ciphertext
|
|
292
|
-
does not exist yet. `captureEncrypted` is the hand-off: you call it from inside
|
|
293
|
-
your own interceptors, where the ciphertext does exist, and pass back the same
|
|
294
|
-
config object Blix already saw.
|
|
295
|
-
|
|
296
|
-
Two calls, one on the way out and one on the way back:
|
|
297
|
-
|
|
298
|
-
```ts
|
|
299
|
-
// src/network/axios.ts
|
|
300
|
-
import { attachHttpMonitor, captureEncrypted } from "@hakam-aldeen-kh/blix/capture";
|
|
301
|
-
|
|
302
|
-
apiClient.interceptors.request.use((config) => {
|
|
303
|
-
const encrypted = encryptBody(config.data);
|
|
304
|
-
captureEncrypted(config, { request: encrypted });
|
|
305
|
-
return { ...config, data: encrypted };
|
|
306
|
-
});
|
|
307
|
-
|
|
308
|
-
apiClient.interceptors.response.use((response) => {
|
|
309
|
-
captureEncrypted(response.config, { response: response.data });
|
|
310
|
-
return { ...response, data: decryptBody(response.data) };
|
|
311
|
-
});
|
|
312
|
-
```
|
|
313
|
-
|
|
314
|
-
Both calls work on the *copies* those interceptors return, not on the objects
|
|
315
|
-
axios created — see [How correlation works](#how-correlation-works) for why
|
|
316
|
-
that still resolves. On the response side it is `response.config` that has to
|
|
317
|
-
carry the stamp, and it does: axios threads the object returned by the last
|
|
318
|
-
request interceptor straight through to `response.config`, so the
|
|
319
|
-
`{ ...config, data: encrypted }` above is literally the object you get back.
|
|
320
|
-
|
|
321
|
-
##### Dropping it into an interceptor you already have
|
|
322
|
-
|
|
323
|
-
The example above builds a fresh interceptor that returns a spread copy. Most
|
|
324
|
-
real pipelines have one large multi-step interceptor that mutates `config.data`
|
|
325
|
-
in place. `captureEncrypted` needs no restructuring for that — it is two lines:
|
|
326
|
-
|
|
327
|
-
```ts
|
|
328
|
-
private static async encryptRequest(config) {
|
|
329
|
-
const encrypted = await encryptionService.encryptApiPayload(
|
|
330
|
-
JSON.stringify(config.data),
|
|
331
|
-
key,
|
|
332
|
-
);
|
|
333
|
-
captureEncrypted(config, { request: encrypted }); // ← add
|
|
334
|
-
config.data = encrypted;
|
|
335
|
-
return config;
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
private static async decryptResponse(response) {
|
|
339
|
-
captureEncrypted(response.config, { response: response.data }); // ← add, BEFORE decrypting
|
|
340
|
-
return { ...response, data: await encryptionService.decryptApiResponse(response) };
|
|
341
|
-
}
|
|
342
|
-
```
|
|
343
|
-
|
|
344
|
-
Note the placement on the response side: call it **before** you decrypt, so the
|
|
345
|
-
value you hand over is the wire form. Calling it after decryption puts your
|
|
346
|
-
plaintext response — secrets included — under a tab labelled Encrypted, which
|
|
347
|
-
is both wrong and a disclosure. See the redaction note below.
|
|
348
|
-
|
|
349
|
-
##### Call it only where encryption actually happened
|
|
350
|
-
|
|
351
|
-
The examples above call `captureEncrypted` unconditionally, which is only
|
|
352
|
-
correct because they have no path that skips encryption. Real pipelines do: an
|
|
353
|
-
endpoint on an exclusion list, an explicit `skipEncryption` flag, a missing or
|
|
354
|
-
not-yet-derived session key, a `FormData` body carrying only file parts, an
|
|
355
|
-
encryption failure the interceptor swallows so the request can still go out.
|
|
356
|
-
|
|
357
|
-
On any of those paths the value you would hand over is **plaintext**, and Blix
|
|
358
|
-
has no way to know that — it labels whatever you pass as the encrypted wire
|
|
359
|
-
form and shows it under the Encrypted tab. The result is a reading that looks
|
|
360
|
-
authoritative and is wrong, which is worse than no reading at all.
|
|
361
|
-
|
|
362
|
-
So put the call on the **success path, inside the function that encrypts** —
|
|
363
|
-
next to the line that produced the ciphertext, where it cannot outlive the
|
|
364
|
-
condition that made it true — rather than in the interceptor after the
|
|
365
|
-
function returns:
|
|
366
|
-
|
|
367
|
-
```ts
|
|
368
|
-
// Inside your encryption module, not in the interceptor.
|
|
369
|
-
function encryptBody(config) {
|
|
370
|
-
if (shouldSkip(config)) return config; // no call — nothing encrypted
|
|
371
|
-
const key = sessionKey();
|
|
372
|
-
if (!key) return config; // no call — nothing encrypted
|
|
373
|
-
|
|
374
|
-
try {
|
|
375
|
-
const encrypted = seal(config.data, key);
|
|
376
|
-
captureEncrypted(config, { request: encrypted }); // only here
|
|
377
|
-
config.data = encrypted;
|
|
378
|
-
} catch {
|
|
379
|
-
// Encryption failed and we're sending plaintext — deliberately no call.
|
|
380
|
-
}
|
|
381
|
-
return config;
|
|
382
|
-
}
|
|
383
|
-
```
|
|
384
|
-
|
|
385
|
-
The same rule governs the response side: call it only where you know the body
|
|
386
|
-
you are holding is the pre-decryption wire form.
|
|
387
|
-
|
|
388
|
-
##### How correlation works
|
|
389
|
-
|
|
390
|
-
Requests and responses are correlated by the **identity of the config object**,
|
|
391
|
-
never by URL or timing, so two concurrent calls to the same endpoint stay
|
|
392
|
-
correctly apart. Pass the object axios handed you; a `{ ...config }` copy made
|
|
393
|
-
by your own interceptor resolves too.
|
|
394
|
-
|
|
395
|
-
The copy resolves because there are two stamps, not one:
|
|
396
|
-
|
|
397
|
-
| Stamp | Visibility | Survives `{ ...config }` |
|
|
398
|
-
| --- | --- | --- |
|
|
399
|
-
| a registry-global `Symbol`, non-enumerable | invisible to `Object.keys`, `JSON.stringify`, `Object.entries` | **no** — spread copies only enumerable own properties |
|
|
400
|
-
| `__monitorId`, a plain enumerable string property | shows up in `Object.keys` and a `JSON.stringify` of the *config* | **yes** |
|
|
401
|
-
|
|
402
|
-
The symbol is the primary; `__monitorId` is what the resolver falls back to,
|
|
403
|
-
and it is the same property `attachHttpMonitor` has set since 0.2.1 for its own
|
|
404
|
-
response interceptor. A third fallback, a `WeakMap`, covers only a frozen or
|
|
405
|
-
sealed config that rejects `defineProperty`.
|
|
406
|
-
|
|
407
|
-
Two consequences worth being explicit about. **Neither stamp reaches the
|
|
408
|
-
wire** — they live on the axios config, and axios serializes only `data`. But
|
|
409
|
-
`__monitorId` *is* enumerable, so it will appear if you log or stringify the
|
|
410
|
-
config object itself; only the symbol is fully invisible. And a copy that
|
|
411
|
-
enumerates fields **explicitly** — `{ url, method, data: encrypted }` rather
|
|
412
|
-
than a spread — carries neither stamp and will not resolve, making the call a
|
|
413
|
-
silent no-op. Spread, or mutate in place.
|
|
414
|
-
|
|
415
|
-
The two calls are independent and order-free: the request-side ciphertext is
|
|
416
|
-
produced early and the response-side arrives late, possibly after Blix has
|
|
417
|
-
already finalized the entry. Either way it merges into the existing entry —
|
|
418
|
-
never creating one of its own — and the panel updates.
|
|
419
|
-
|
|
420
|
-
It is a **silent no-op** — never a throw, never a console warning — in every
|
|
421
|
-
one of these:
|
|
422
|
-
|
|
423
|
-
- outside development (`process.env.NODE_ENV !== "development"`), unless the
|
|
424
|
-
build opted in with `NEXT_PUBLIC_BLIX=true` (see
|
|
425
|
-
[staging](#enabling-blix-in-a-production-build-staging));
|
|
426
|
-
- **outside the browser** — the gate is also `typeof window !== "undefined"`,
|
|
427
|
-
so every call made while rendering on the server does nothing, by design (see
|
|
428
|
-
[Production elimination](#production-elimination));
|
|
429
|
-
- when `attachHttpMonitor` was never called on the instance;
|
|
430
|
-
- when the config carries no stamp — a retry that built a fresh config, a
|
|
431
|
-
config assembled field-by-field rather than spread, or a request that started
|
|
432
|
-
while capture was **paused** from the panel's toolbar;
|
|
433
|
-
- when `payload` is missing, or both `request` and `response` on it are
|
|
434
|
-
`null`/`undefined` (a bare `{ request }` will not blank a `response` captured
|
|
435
|
-
by an earlier call);
|
|
436
|
-
- when the entry has already been evicted from the buffer.
|
|
437
|
-
|
|
438
|
-
In production it is eliminated entirely, along with the rest of capture.
|
|
439
|
-
|
|
440
|
-
Values may be a string, a plain object, or an `ArrayBuffer`/typed array (kept
|
|
441
|
-
as a bounded hex preview plus byte length). They go through the same
|
|
442
|
-
serialization and truncation rules as the plaintext bodies, both in the panel
|
|
443
|
-
and in IndexedDB, so a multi-megabyte ciphertext cannot blow out the log.
|
|
444
|
-
|
|
445
|
-
> **Redaction.** Blix masks sensitive *headers* — and the list is closed at
|
|
446
|
-
> exactly four, matched exactly: `authorization`, `cookie`, `set-cookie`,
|
|
447
|
-
> `x-api-key`. It does **not**, and cannot, redact anything inside the values
|
|
448
|
-
> you pass here — they are bodies, and Blix has no way to tell ciphertext from
|
|
449
|
-
> plaintext. If you pass an already-decrypted body as `response`, whatever
|
|
450
|
-
> secrets it contains are shown in the panel verbatim and written to IndexedDB
|
|
451
|
-
> when preserve-log is on. Pass the wire form, not the decrypted one. See
|
|
452
|
-
> [Security](#security) for the full picture.
|
|
453
|
-
|
|
454
|
-
#### `withInitiatorCapture(instance)`
|
|
455
|
-
|
|
456
|
-
Optional. Wraps the instance in a `Proxy` so each request records the stack of
|
|
457
|
-
its own call site, which the panel shows as the "Initiator" of a row. Wrap
|
|
458
|
-
once, export the wrapped instance:
|
|
459
|
-
|
|
460
|
-
```ts
|
|
461
|
-
export const apiClient = withInitiatorCapture(axios.create({ baseURL: "/api" }));
|
|
462
|
-
```
|
|
463
|
-
|
|
464
|
-
**It does not matter which of the two you hand to `attachHttpMonitor`.** The
|
|
465
|
-
proxy forwards property reads to the underlying instance, and `interceptors` is
|
|
466
|
-
not a function, so it comes back untouched — `wrapped.interceptors` and
|
|
467
|
-
`original.interceptors` are the same object. Registering on either registers on
|
|
468
|
-
both. There is no wrong choice here and no silent failure.
|
|
469
|
-
|
|
470
|
-
What *does* matter is which one your app calls through. Only the proxy's traps
|
|
471
|
-
record a stack, so every request made against the unwrapped instance is still
|
|
472
|
-
captured but arrives with no initiator. Export the wrapped one and
|
|
473
|
-
keep the original private:
|
|
474
|
-
|
|
475
|
-
```ts
|
|
476
|
-
const client = axios.create({ baseURL: "/api" });
|
|
477
|
-
client.interceptors.request.use(encryptBody);
|
|
478
|
-
attachHttpMonitor(client); // either one works
|
|
479
|
-
|
|
480
|
-
export const apiClient = withInitiatorCapture(client); // this is what callers use
|
|
481
|
-
```
|
|
482
|
-
|
|
483
|
-
The traps cover the callable form (`apiClient(config)`) plus `request`, `get`,
|
|
484
|
-
`post`, `put`, `patch`, `delete` and `head`. Other entry points — `options`,
|
|
485
|
-
the `*Form` helpers — pass through unwrapped: still captured, just with no
|
|
486
|
-
initiator stack.
|
|
487
|
-
|
|
488
|
-
> **Known limitation.** The initiator stack is produced by filtering your own
|
|
489
|
-
> HTTP wrapper's frames out of the captured stack, and that filter currently
|
|
490
|
-
> matches a fixed set of module paths rather than deriving them from where
|
|
491
|
-
> `withInitiatorCapture` was called. If your axios module does not sit at one
|
|
492
|
-
> of those paths, the top frame reported will be your own wrapper rather than
|
|
493
|
-
> the true call site. There is no option to extend the filter yet.
|
|
494
|
-
|
|
495
|
-
### HTTP (fetch) — `attachFetchMonitor(options?)`
|
|
496
|
-
|
|
497
|
-
For requests made with `fetch` directly rather than through axios. It wraps
|
|
498
|
-
`globalThis.fetch`, so it captures **every** `fetch` the page makes — your own
|
|
499
|
-
and any library's or third-party script's — into the same Network section, in
|
|
500
|
-
the same shape, with the same header masking and the same absence of body
|
|
501
|
-
redaction (see [Security](#security)). Its rows show **Client: fetch** in the
|
|
502
|
-
Headers tab, and `client:fetch` filters to them. Each row records the call
|
|
503
|
-
stack of the `fetch` call that made it; no `withInitiatorCapture` is needed.
|
|
504
|
-
|
|
505
|
-
```ts
|
|
506
|
-
// instrumentation-client.ts (Next.js 15.3+), or the first module your client bundle evaluates
|
|
507
|
-
import { attachFetchMonitor } from "@hakam-aldeen-kh/blix/capture";
|
|
508
|
-
|
|
509
|
-
if (process.env.NODE_ENV === "development" && typeof window !== "undefined") {
|
|
510
|
-
attachFetchMonitor();
|
|
511
|
-
}
|
|
512
|
-
```
|
|
513
|
-
|
|
514
|
-
**Call it before the first `fetch` you want captured.** There are no
|
|
515
|
-
interceptors to order against, but anything fetched before the wrapper is
|
|
516
|
-
installed is simply not seen. In a Next.js App Router project,
|
|
517
|
-
`instrumentation-client.ts` runs on the client before your application code and
|
|
518
|
-
is the natural place; otherwise, the top of the first module your client bundle
|
|
519
|
-
evaluates.
|
|
520
|
-
|
|
521
|
-
**Client-side only.** It never patches `fetch` on the server, so Next.js's
|
|
522
|
-
server-side `fetch` and its data cache are untouched — and like every capture
|
|
523
|
-
function, it is a no-op in production.
|
|
524
|
-
|
|
525
|
-
It returns a **disposer** that restores the original `fetch`, and calling
|
|
526
|
-
`attachFetchMonitor` again while it is installed does nothing. The disposer
|
|
527
|
-
only restores `fetch` while Blix's wrapper is still the current one: if another
|
|
528
|
-
library has wrapped `fetch` since, restoring would silently remove that wrapper
|
|
529
|
-
too, so it leaves `fetch` alone and warns in development instead.
|
|
530
|
-
|
|
531
|
-
#### Bodies, streams and the size cap
|
|
532
|
-
|
|
533
|
-
Blix reads bodies from clones, so your code always receives an untouched
|
|
534
|
-
`Request` and `Response`. It never buffers without a limit:
|
|
535
|
-
|
|
536
|
-
| Situation | What is recorded |
|
|
537
|
-
| --- | --- |
|
|
538
|
-
| `Content-Type: text/event-stream` | status, headers, timing — body not captured, because the stream does not end |
|
|
539
|
-
| `Content-Length` over the cap | status, headers, timing — body not captured, reading never starts |
|
|
540
|
-
| body passes the cap while downloading | status, headers, timing — body not captured, reading abandoned |
|
|
541
|
-
| `no-cors` (opaque) response | status `0` — headers and body not captured, because the browser hides them |
|
|
542
|
-
| request body is a `ReadableStream` | request body not captured — reading it would consume the upload |
|
|
543
|
-
|
|
544
|
-
A skipped body is recorded as `«body not captured: …»` with the reason, so an
|
|
545
|
-
empty Response tab always means an empty response. The cap applies to request
|
|
546
|
-
and response bodies alike, defaults to **5 MB**, and is set with
|
|
547
|
-
`maxBodyBytes`:
|
|
548
|
-
|
|
549
|
-
```ts
|
|
550
|
-
attachFetchMonitor({ maxBodyBytes: 1024 * 1024 }); // 1 MB — 0 captures no bodies
|
|
551
|
-
```
|
|
552
|
-
|
|
553
|
-
The cap is enforced by **counting bytes as they arrive**, not by trusting
|
|
554
|
-
`Content-Length`. That header is the compressed size, and a small gzipped
|
|
555
|
-
response can decode to many times it; a declared length over the cap only lets
|
|
556
|
-
Blix skip without starting. A row's duration runs to the end of the body when
|
|
557
|
-
Blix read it, and to the response headers when it did not.
|
|
558
|
-
|
|
559
|
-
A non-2xx response is recorded as a failed request with its body under
|
|
560
|
-
**Error**, the same way an axios rejection is. An aborted request settles as
|
|
561
|
-
**aborted**; a network failure or an `AbortSignal.timeout()` settles as an
|
|
562
|
-
error. Either way the original rejection reaches your code untouched.
|
|
563
|
-
|
|
564
|
-
#### What is left out by default
|
|
565
|
-
|
|
566
|
-
A development server makes plenty of `fetch` calls of its own, and they would
|
|
567
|
-
bury your requests. These are **not captured by default**:
|
|
568
|
-
|
|
569
|
-
| Rule | Matches |
|
|
570
|
-
| --- | --- |
|
|
571
|
-
| `"/_next/"` | Next.js assets, HMR updates and Pages Router data requests |
|
|
572
|
-
| `"/__nextjs"` | the Next.js dev overlay — stack frames, source maps, open-in-editor |
|
|
573
|
-
| `/[?&]_rsc=/` | App Router navigations and `<Link>` prefetches |
|
|
574
|
-
| `".hot-update."` | webpack HMR update manifests and chunks |
|
|
575
|
-
| `"/__webpack_hmr"` | webpack-hot-middleware |
|
|
576
|
-
|
|
577
|
-
A string matches anywhere in the URL's path and query, a regular expression is
|
|
578
|
-
tested against the same string, and a function receives the resolved `URL`.
|
|
579
|
-
Pass an array to **replace** the defaults, or a function to **extend** them:
|
|
580
|
-
|
|
581
|
-
```ts
|
|
582
|
-
attachFetchMonitor({ ignore: (defaults) => [...defaults, "/api/health"] }); // extend
|
|
583
|
-
attachFetchMonitor({ ignore: [] }); // capture everything
|
|
584
|
-
```
|
|
585
|
-
|
|
586
|
-
**An ignored request is dropped silently** — no row records that it was
|
|
587
|
-
skipped. If a request you expected is missing, check it against this list
|
|
588
|
-
first.
|
|
589
|
-
|
|
590
|
-
#### Using it alongside axios
|
|
591
|
-
|
|
592
|
-
Installing both `attachFetchMonitor` and `attachHttpMonitor` is fine. axios's
|
|
593
|
-
default browser adapter is XHR, which the fetch wrapper never sees. If you
|
|
594
|
-
configure axios with `adapter: "fetch"`, one request passes through both — and
|
|
595
|
-
**the axios entry wins**. It was captured on the plaintext side of your
|
|
596
|
-
interceptors, keeps `captureEncrypted` and `withInitiatorCapture`, and can be
|
|
597
|
-
replayed; the fetch wrapper would only see the same request after encryption.
|
|
598
|
-
`attachHttpMonitor` marks each request on its way into axios's fetch adapter,
|
|
599
|
-
and the fetch wrapper skips anything carrying the mark, so every request is
|
|
600
|
-
logged exactly once. The order of the two calls does not matter.
|
|
601
|
-
|
|
602
|
-
#### What it does not do
|
|
603
|
-
|
|
604
|
-
- **No replay.** Fetch rows show Replay disabled, with the reason. Blix does
|
|
605
|
-
not record a call's `credentials`, `mode` or `cache`, or a JSON body as the
|
|
606
|
-
exact bytes that were sent, so a replay could not promise to be the same
|
|
607
|
-
request.
|
|
608
|
-
- **No `captureEncrypted`** — see
|
|
609
|
-
[Encrypted payloads](#encrypted-payloads--captureencryptedconfig-payload).
|
|
610
|
-
- **No timeout watchdog.** A `fetch` that never answers stays pending until
|
|
611
|
-
the page aborts it: a `fetch` has no timeout of its own for Blix to measure
|
|
612
|
-
against. (An `AbortSignal.timeout()` that fires is recorded, as a timeout.)
|
|
613
|
-
axios differs: a request with a `timeout` is settled 3 s past it — see
|
|
614
|
-
[Failed requests](#failed-requests-and-the-shape-of-your-rejection).
|
|
615
|
-
|
|
616
|
-
### What a request records — `entry.error` and `entry.wire`
|
|
617
|
-
|
|
618
|
-
Two fields of a captured entry are worth knowing by shape, because they are
|
|
619
|
-
what the JSON and NDJSON exports carry and what the `MonitorEntry` type
|
|
620
|
-
describes. Both are the same whichever client made the request.
|
|
621
|
-
|
|
622
|
-
#### `entry.error` — `CapturedError`
|
|
623
|
-
|
|
624
|
-
A failed request stores a description of the failure, not the thrown object
|
|
625
|
-
and not just the response body:
|
|
626
|
-
|
|
627
|
-
| Field | |
|
|
628
|
-
| --- | --- |
|
|
629
|
-
| `kind` | How it failed — see below. Always present. |
|
|
630
|
-
| `message` | The error's message. Always present. |
|
|
631
|
-
| `name`, `code` | The error's class (`AxiosError`, `TypeError`) and the client's code (`ERR_NETWORK`, `ECONNABORTED`). |
|
|
632
|
-
| `status`, `statusText` | When the server answered. |
|
|
633
|
-
| `response` | **The response body**, when there was one. |
|
|
634
|
-
| `timeoutMs` | The timeout the request ran under, when that is what ended it. |
|
|
635
|
-
| `cause` | What the error says caused it — the same fields, without `kind`. |
|
|
636
|
-
| `stack` | The top frames of the error's stack. |
|
|
637
|
-
| `handledAs` | What your interceptor rejected with instead, when it replaced the client's error — the same fields, without `kind`. |
|
|
638
|
-
|
|
639
|
-
| `kind` | Meaning |
|
|
640
|
-
| --- | --- |
|
|
641
|
-
| `http` | The server answered with a status the client treats as an error. |
|
|
642
|
-
| `timeout` | No answer inside the configured time. |
|
|
643
|
-
| `network` | No answer at all: offline, DNS, refused, or blocked by CORS — the browser does not tell scripts which. |
|
|
644
|
-
| `aborted` | Cancelled by the page: `AbortController`, a cancel token, navigation. |
|
|
645
|
-
| `setup` | Thrown before the request left — by an interceptor, or while building it. |
|
|
646
|
-
| `unknown` | None of the above could be established. |
|
|
647
|
-
|
|
648
|
-
Query and mutation rows, and a Redux action whose payload is an error, store
|
|
649
|
-
the same shape. Only named fields are ever read from an error: never its
|
|
650
|
-
request config or headers.
|
|
651
|
-
|
|
652
|
-
#### `entry.wire` — `WireTiming`
|
|
653
|
-
|
|
654
|
-
Everything else Blix times is two clock reads in JavaScript. `wire` is what the
|
|
655
|
-
*browser* measured for the same request — its `PerformanceResourceTiming`
|
|
656
|
-
entry — attached automatically by every `attach*Monitor`, and shown in the
|
|
657
|
-
Timing tab under **On the wire** and **Server-Timing**:
|
|
658
|
-
|
|
659
|
-
| Field | |
|
|
660
|
-
| --- | --- |
|
|
661
|
-
| `totalMs` | The browser's total for the request. Always present. |
|
|
662
|
-
| `queueMs`, `dnsMs`, `connectMs`, `tlsMs` | Waiting for a connection, DNS, TCP, TLS. |
|
|
663
|
-
| `waitMs`, `downloadMs` | Request sent to first byte (TTFB), and first byte to last. |
|
|
664
|
-
| `redirectMs` | Time spent in redirects, when there were any. |
|
|
665
|
-
| `transferSize`, `encodedBodySize`, `decodedBodySize` | Bytes over the wire, the body as sent, and the body decoded. |
|
|
666
|
-
| `fromCache` | Nothing crossed the wire, yet there was a body. |
|
|
667
|
-
| `protocol` | `h2`, `http/1.1`, … |
|
|
668
|
-
| `serverTiming` | The response's `Server-Timing` metrics: `name`, `durationMs`, `description`. |
|
|
669
|
-
| `restricted` | See below. |
|
|
670
|
-
|
|
671
|
-
For a **cross-origin** request the browser withholds the phases and the sizes
|
|
672
|
-
unless the response carries `Timing-Allow-Origin`. Such an entry has
|
|
673
|
-
`restricted: true` and `totalMs` only — add the header on the API to get the
|
|
674
|
-
rest. An entry has no `wire` at all when the browser reported no timing for
|
|
675
|
-
it, or when Blix could not match one to the request.
|
|
676
|
-
|
|
677
|
-
### Redux — `createReduxMonitorMiddleware(options?)`
|
|
678
|
-
|
|
679
|
-
```ts
|
|
680
|
-
// src/store.ts
|
|
681
|
-
import { configureStore } from "@reduxjs/toolkit";
|
|
682
|
-
import { createReduxMonitorMiddleware } from "@hakam-aldeen-kh/blix/capture";
|
|
683
|
-
|
|
684
|
-
const devMiddleware =
|
|
685
|
-
process.env.NODE_ENV === "development" && typeof window !== "undefined"
|
|
686
|
-
? createReduxMonitorMiddleware({
|
|
687
|
-
ignore: ["analytics/*", "some/noisyAction"],
|
|
688
|
-
})
|
|
689
|
-
: undefined;
|
|
690
|
-
|
|
691
|
-
export const store = configureStore({
|
|
692
|
-
reducer,
|
|
693
|
-
middleware: (getDefault) =>
|
|
694
|
-
devMiddleware ? getDefault().concat(devMiddleware) : getDefault(),
|
|
695
|
-
});
|
|
696
|
-
```
|
|
697
|
-
|
|
698
|
-
Options: `ignore` (exact types or `"prefix/*"` globs), `coalesceMs` (repeat
|
|
699
|
-
dispatches of one type inside this window fold into a single row), and
|
|
700
|
-
`maxActionsPerSecond` (above this rate, capture drops to type + timing and
|
|
701
|
-
skips diffing). Outside development the factory returns a pure pass-through
|
|
702
|
-
middleware, so calling it unguarded is *behaviourally* free — but see
|
|
703
|
-
[Guard your call sites](#guard-your-call-sites) for why the guard above is
|
|
704
|
-
still worth the extra three lines.
|
|
705
|
-
|
|
706
|
-
### Realtime — `tapRealtimeAdapter(adapter, transport)`
|
|
707
|
-
|
|
708
|
-
Returns the adapter wrapped; use the return value. `transport` is a free-form
|
|
709
|
-
label shown in the panel (`"pusher"`, `"socket.io"`, …).
|
|
710
|
-
|
|
711
|
-
```ts
|
|
712
|
-
// src/realtime/adapter.ts
|
|
713
|
-
import { tapRealtimeAdapter } from "@hakam-aldeen-kh/blix/capture";
|
|
714
|
-
|
|
715
|
-
const adapter = new PusherAdapter();
|
|
716
|
-
|
|
717
|
-
export const realtime =
|
|
718
|
-
process.env.NODE_ENV === "development" && typeof window !== "undefined"
|
|
719
|
-
? tapRealtimeAdapter(adapter, "pusher")
|
|
720
|
-
: adapter;
|
|
721
|
-
```
|
|
722
|
-
|
|
723
|
-
Your adapter only needs to structurally satisfy `RealtimeAdapterLike`:
|
|
724
|
-
`connect`, `disconnect`, `subscribe`, `onMessage`, `onPresenceUpdate`. In
|
|
725
|
-
production the adapter is returned untouched.
|
|
726
|
-
|
|
727
|
-
**The tap is transparent, not narrowing.** It returns a `Proxy` typed as your
|
|
728
|
-
adapter's own type, so class-based adapters keep working through it:
|
|
729
|
-
|
|
730
|
-
- methods **outside** `RealtimeAdapterLike` pass straight through — the proxy's
|
|
731
|
-
fallback binds and forwards any property that is not one of the tapped
|
|
732
|
-
methods;
|
|
733
|
-
- `instanceof` still works against your concrete class, because the proxy has
|
|
734
|
-
no `getPrototypeOf` trap and forwards to the target.
|
|
735
|
-
|
|
736
|
-
```ts
|
|
737
|
-
// Both of these still work through the tap.
|
|
738
|
-
if (this.adapter instanceof ActionCableAdapter) {
|
|
739
|
-
this.adapter.setSubscriptionContext({ accountId, userId }); // not in RealtimeAdapterLike
|
|
740
|
-
}
|
|
741
|
-
```
|
|
742
|
-
|
|
743
|
-
You do not need to keep a second reference to the untapped adapter.
|
|
744
|
-
|
|
745
|
-
### TanStack Query — `tapQueryClient(client)`
|
|
746
|
-
|
|
747
|
-
**`tapQueryClient` is the one exception to the module-scope rule.** Call it
|
|
748
|
-
from an effect in your query provider, *not* from the `useState` initializer
|
|
749
|
-
that creates the client:
|
|
750
|
-
|
|
751
|
-
```tsx
|
|
752
|
-
"use client";
|
|
753
|
-
|
|
754
|
-
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
755
|
-
import { tapQueryClient } from "@hakam-aldeen-kh/blix/capture";
|
|
756
|
-
import { useEffect, useState } from "react";
|
|
757
|
-
|
|
758
|
-
export function QueryProvider({ children }: { children: React.ReactNode }) {
|
|
759
|
-
const [client] = useState(() => new QueryClient());
|
|
760
|
-
|
|
761
|
-
useEffect(() => tapQueryClient(client), [client]);
|
|
762
|
-
|
|
763
|
-
return <QueryClientProvider client={client}>{children}</QueryClientProvider>;
|
|
764
|
-
}
|
|
765
|
-
```
|
|
766
|
-
|
|
767
|
-
Why the effect at all: React Strict Mode double-invokes `useState`
|
|
768
|
-
initializers, so tapping there taps a client that is immediately discarded.
|
|
769
|
-
Installing from an effect is correct — because parent effects run after child
|
|
770
|
-
effects, the tap backfills from `getQueryCache().getAll()` on install rather
|
|
771
|
-
than starting blind, so no early events are lost.
|
|
772
|
-
|
|
773
|
-
Returning the disposer is fine: it unsubscribes from both caches and unmarks
|
|
774
|
-
the client, so the Strict Mode cycle — subscribe, dispose, subscribe again —
|
|
775
|
-
reinstalls cleanly and the second install backfills the same way the first did.
|
|
776
|
-
|
|
777
|
-
> **Fixed in 0.3.2.** In 0.3.1 and earlier the disposer unsubscribed but left
|
|
778
|
-
> the client marked as tapped, so the reinstall short-circuited on the tap's
|
|
779
|
-
> internal idempotency guard without resubscribing — leaving the Query tab
|
|
780
|
-
> empty for the whole session, with no error and no warning. On those versions
|
|
781
|
-
> the workaround is to call `tapQueryClient(client)` from the effect without
|
|
782
|
-
> returning its result.
|
|
783
|
-
|
|
784
|
-
If you construct the `QueryClient` at module scope rather than in a component,
|
|
785
|
-
you can tap it at module scope too — the rule is "tap the client that actually
|
|
786
|
-
survives", which in the common React pattern means an effect.
|
|
787
|
-
|
|
788
|
-
---
|
|
789
|
-
|
|
790
|
-
## Mounting the panel
|
|
791
|
-
|
|
792
|
-
Render `<Blix />` **exactly once**. Mounting it more than once gives you
|
|
793
|
-
duplicate panels reading the same log.
|
|
794
|
-
|
|
795
|
-
Mount it from a small client component of its own, and import that component —
|
|
796
|
-
and nothing else Blix-related — from your layout:
|
|
28
|
+
Requires React 18 or 19. The package is ESM only.
|
|
797
29
|
|
|
798
30
|
```tsx
|
|
799
31
|
// app/BlixMount.tsx
|
|
800
32
|
"use client";
|
|
801
33
|
|
|
802
34
|
import { Blix } from "@hakam-aldeen-kh/blix";
|
|
803
|
-
import {
|
|
804
|
-
import { store } from "@/src/store";
|
|
805
|
-
|
|
806
|
-
// This file is the client boundary on purpose. `apiClient` and `store` are
|
|
807
|
-
// module-scope singletons that build themselves during module evaluation —
|
|
808
|
-
// importing them from a Server Component pulls axios, cookie access, your
|
|
809
|
-
// encryption service and any "use client" helpers they touch into the RSC
|
|
810
|
-
// module graph, which fails the production build. Importing them here keeps
|
|
811
|
-
// that evaluation on the client side of the boundary.
|
|
812
|
-
export default function BlixMount() {
|
|
813
|
-
if (
|
|
814
|
-
process.env.NODE_ENV !== "development" &&
|
|
815
|
-
process.env.NEXT_PUBLIC_BLIX !== "true"
|
|
816
|
-
)
|
|
817
|
-
return null;
|
|
818
|
-
return <Blix store={store} apiClient={apiClient} dbName="my-app" />;
|
|
819
|
-
}
|
|
820
|
-
```
|
|
821
|
-
|
|
822
|
-
```tsx
|
|
823
|
-
// app/layout.tsx — stays a Server Component; imports only the mount
|
|
824
|
-
import BlixMount from "./BlixMount";
|
|
825
|
-
|
|
826
|
-
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
827
|
-
return (
|
|
828
|
-
<html lang="en">
|
|
829
|
-
<body>
|
|
830
|
-
{children}
|
|
831
|
-
<BlixMount />
|
|
832
|
-
</body>
|
|
833
|
-
</html>
|
|
834
|
-
);
|
|
835
|
-
}
|
|
836
|
-
```
|
|
837
|
-
|
|
838
|
-
**The client boundary is a leaf.** It does not make your layout or your
|
|
839
|
-
children client components — only this one file and what it imports. There is
|
|
840
|
-
no bundle cost to isolating it this way, and it is what keeps your store and
|
|
841
|
-
HTTP client out of the server graph.
|
|
842
|
-
|
|
843
|
-
`<Blix />` itself carries a `"use client"` directive and can be rendered from a
|
|
844
|
-
server component directly. That is not the reason for the wrapper file: the
|
|
845
|
-
directive governs Blix's own module, not the modules *you* import alongside it.
|
|
846
|
-
Importing `store` and `apiClient` into `app/layout.tsx` is what breaks the
|
|
847
|
-
build, and it breaks it with an opaque module-resolution error from
|
|
848
|
-
`next build`, far from the mount site.
|
|
849
|
-
|
|
850
|
-
The directive on the root entry also means **everything** re-exported from
|
|
851
|
-
`@hakam-aldeen-kh/blix` — capture functions included — is inside that client
|
|
852
|
-
boundary. That is the reason for the split entry point: import capture from
|
|
853
|
-
`@hakam-aldeen-kh/blix/capture` in any module that runs on the server. See
|
|
854
|
-
[Entry points](#entry-points).
|
|
855
|
-
|
|
856
|
-
### Props — all optional
|
|
857
|
-
|
|
858
|
-
| Prop | Effect when omitted |
|
|
859
|
-
| --- | --- |
|
|
860
|
-
| `store` | The **State** tab renders `— Redux store not provided —`, and **Re-dispatch** is disabled with the reason `Redux store not provided`. Everything else works. |
|
|
861
|
-
| `apiClient` | **Replay request** is disabled with the reason `HTTP client not provided`. Everything else works. |
|
|
862
|
-
| `dbName` | Falls back to the shared database `blix:default`, and the panel warns in the console. See [`dbName`](#dbname--when-you-need-it). |
|
|
863
|
-
|
|
864
|
-
`store` and `apiClient` are structurally typed — they need
|
|
865
|
-
`getState`/`subscribe`/`dispatch` and `request` respectively. A redux-toolkit
|
|
866
|
-
store and an axios instance satisfy them as-is. (The interfaces are named
|
|
867
|
-
`StoreLike` and `HttpClientLike` in the source, but they are not exported from
|
|
868
|
-
the package; only `Blix` and `BlixProps` are. You never need to name them —
|
|
869
|
-
structural typing means you just pass your store and client.)
|
|
870
|
-
|
|
871
|
-
Passing neither still gives you a fully working capture log; you only lose the
|
|
872
|
-
two features that need a live handle on the app.
|
|
873
|
-
|
|
874
|
-
### Finding your way around
|
|
875
|
-
|
|
876
|
-
The panel is one header, one rail and two panes.
|
|
877
|
-
|
|
878
|
-
**The rail on the left is the four sources** — Network, Realtime, Redux,
|
|
879
|
-
Query. They are not filters over one table: each has its own columns and its
|
|
880
|
-
own notion of a row, so switching source switches the whole view. `1`–`4` jump
|
|
881
|
-
between them, and each keeps its own selection, so stepping to Redux and back
|
|
882
|
-
returns you to the request you were reading. Each row ends with its own number
|
|
883
|
-
key, because a shortcut is best taught where it is used. Below the sources it
|
|
884
|
-
carries the session totals; collapsed, it keeps the sources and their counts
|
|
885
|
-
and drops the totals, since four numbers without their labels are four
|
|
886
|
-
unattributed numbers.
|
|
887
|
-
|
|
888
|
-
**The header is the session**, not the entry: whether capture is running, what
|
|
889
|
-
is being filtered out, and where the panel lives. While a filter is on, the
|
|
890
|
-
field carries its own count — `8 of 56` — and a clear button, so you never
|
|
891
|
-
have to read the status bar to know one is applied. `.*` widens the search to
|
|
892
|
-
request and response bodies.
|
|
893
|
-
|
|
894
|
-
**The red bin beside Export clears the source you are reading**, and only
|
|
895
|
-
that one: emptying Redux leaves Network alone. Pinned entries survive it, and
|
|
896
|
-
it is disabled when the source has nothing left to clear. To empty every
|
|
897
|
-
source at once, use **Clear log** in the overflow menu, or `⇧C`.
|
|
898
|
-
|
|
899
|
-
**The launcher badge summarizes the log while the panel is closed.** It shows
|
|
900
|
-
one count per source that has captured anything — Network, Realtime, Redux,
|
|
901
|
-
Query, each with its rail icon — and, in red beside a count, how many of that
|
|
902
|
-
source's entries failed. The dot is capture state: green while capturing,
|
|
903
|
-
amber while paused, pulsing while something is in flight.
|
|
904
|
-
|
|
905
|
-
The panel opens **floating** the first time. From the header it can be docked
|
|
906
|
-
to any edge instead; the choice, and each dock's size, is saved per project.
|
|
907
|
-
|
|
908
|
-
| Mode | Layout | A press outside the panel |
|
|
909
|
-
| --- | --- | --- |
|
|
910
|
-
| Float | Overlays the page; drag it by the header, resize from the corner | Closes it |
|
|
911
|
-
| Bottom, right, top, left | Takes that side of the screen — the page is pushed over, not covered | Leaves it open |
|
|
912
|
-
|
|
913
|
-
A docked panel is there so the app can be used while the log is read, so it
|
|
914
|
-
stays put; a floating one is in the way of whatever is under it, so a press
|
|
915
|
-
elsewhere dismisses it. In every mode a press outside still closes an open
|
|
916
|
-
menu or the command palette.
|
|
917
|
-
|
|
918
|
-
**Docked panels reserve their side with inline padding on `<html>`**, removed
|
|
919
|
-
again — and whatever inline value was there before restored — when the panel
|
|
920
|
-
closes, floats, moves to another side or unmounts. The same numbers are
|
|
921
|
-
published as four custom properties on `<html>`:
|
|
922
|
-
|
|
923
|
-
| Property | Value |
|
|
924
|
-
| --- | --- |
|
|
925
|
-
| `--blix-dock-top`, `--blix-dock-right`, `--blix-dock-bottom`, `--blix-dock-left` | The docked panel's size on that side, in px; `0px` on the other three. Set only while a panel is docked — absent while it is closed or floating, so always read them with a fallback |
|
|
926
|
-
|
|
927
|
-
Padding moves what is in the page's flow. An element with `position: fixed`
|
|
928
|
-
or `sticky` is positioned against the viewport, so it is **not** pushed
|
|
929
|
-
automatically — a fixed header still spans the full width, and ends up under a
|
|
930
|
-
side dock. Opt those elements in with the variables:
|
|
931
|
-
|
|
932
|
-
```css
|
|
933
|
-
.app-header { left: var(--blix-dock-left, 0px); right: var(--blix-dock-right, 0px);
|
|
934
|
-
top: var(--blix-dock-top, 0px); }
|
|
935
|
-
.chat-launcher { bottom: calc(16px + var(--blix-dock-bottom, 0px)); }
|
|
936
|
-
```
|
|
937
|
-
|
|
938
|
-
The fallback matters: the properties exist only while `<Blix />` is mounted,
|
|
939
|
-
which in production it is not. The same goes for a layout sized with `100vh`
|
|
940
|
-
or `100vw` — those ignore padding; `100%` of a padded `<html>` does not.
|
|
941
|
-
|
|
942
|
-
**Shortcuts belong to the panel.** Single-key shortcuts — `j`/`k`, `r`,
|
|
943
|
-
`⇧C`, Space and the rest — act only while focus is inside the panel, which it
|
|
944
|
-
is as soon as you open it or click anywhere in it. Typing in your app, in an
|
|
945
|
-
input, a `<select>`, a rich-text editor or through an IME, never triggers
|
|
946
|
-
one. Combinations with Ctrl, ⌘ or Alt are left to the browser and to your app,
|
|
947
|
-
with two exceptions: Ctrl/⌘ + backtick opens and closes the panel from anywhere
|
|
948
|
-
on the page, and `Ctrl/⌘ K` opens the palette from inside it. Shortcuts
|
|
949
|
-
follow the physical key on non-Latin layouts such as Arabic — the toggle and
|
|
950
|
-
`Ctrl/⌘ K` included — so the same key works without switching layout.
|
|
951
|
-
`Esc` closes the innermost open thing — palette, menu, then the panel — and
|
|
952
|
-
only when focus is in the panel; an Escape meant for a dialog in your app is
|
|
953
|
-
not Blix's. `r` re-sends a read at once and asks for a second press before
|
|
954
|
-
re-sending a write, exactly as the Replay button does.
|
|
955
|
-
|
|
956
|
-
**`Ctrl/⌘ K` opens the command palette**, and for several things it is the only
|
|
957
|
-
way in — sort order, row density, dock position, the copy formats, the filter
|
|
958
|
-
syntax. The header spends its width on what you read constantly; everything you
|
|
959
|
-
reach for occasionally lives one keystroke away instead of costing a button
|
|
960
|
-
each. Every row shows its key binding where it has one, so the palette teaches
|
|
961
|
-
its own shortcuts. `?` still opens the full cheatsheet.
|
|
962
|
-
|
|
963
|
-
**The columns are yours.** Right-click the column head — or open **Columns** in
|
|
964
|
-
the overflow menu — and switch off any of Pin, Method, Size, Status, Time,
|
|
965
|
-
Linked and Timing. The choice persists. ROUTE has no switch: it is the column
|
|
966
|
-
that says *which* entry a row is, and a list of rows you cannot identify is not
|
|
967
|
-
a list. A narrow pane drops columns on its own, worst-affordable first, so the
|
|
968
|
-
route keeps the width it needs.
|
|
969
|
-
|
|
970
|
-
**Linked events tie the sources together.** When Blix can see that a query
|
|
971
|
-
caused a request, that a request came from a query, or that one entry is a
|
|
972
|
-
replay of another, the row grows a coloured tick and the foot of the detail
|
|
973
|
-
pane grows a chip you can click to step straight to the other side. The
|
|
974
|
-
relation is observed, never inferred: no tick means *not known*, not
|
|
975
|
-
*unrelated*.
|
|
976
|
-
|
|
977
|
-
### Viewing payloads
|
|
978
|
-
|
|
979
|
-
Every payload pane has a format switch. The choice is remembered, so you pick
|
|
980
|
-
it once rather than per request.
|
|
981
|
-
|
|
982
|
-
| Format | Answers |
|
|
983
|
-
| --- | --- |
|
|
984
|
-
| **Tree** | *What's in here?* Collapsible and searchable; a collapsed subtree costs one row, so it stays fast on multi-megabyte responses. |
|
|
985
|
-
| **Table** | *How do these records compare?* A grid, offered when the payload is a list of records or a keyed map. Sparse and surplus columns are hidden with a count. |
|
|
986
|
-
| **JSON** | *What exactly came back?* Raw and syntax-coloured, with a fold caret on every object and array — plus **Collapse all**, which leaves the top-level keys readable and their contents folded. |
|
|
987
|
-
| **YAML** | *What shape is this?* Indentation instead of punctuation, and multi-line strings — stack traces, SQL — shown as text rather than escapes. |
|
|
988
|
-
| **Text** | *It isn't JSON.* An HTML error page, a CSV body, a proxy's plain-text response. |
|
|
989
|
-
|
|
990
|
-
**Copy gives you what you're looking at**: Table copies CSV, YAML copies YAML.
|
|
991
|
-
|
|
992
|
-
The switch is on every payload pane — request and response bodies, realtime
|
|
993
|
-
frames, the encrypted envelope, Redux actions, and both Redux state views:
|
|
994
|
-
|
|
995
|
-
- **State** shows the live store, scoped by a **slice picker**. Slices the
|
|
996
|
-
selected action wrote to are marked with a dot, so "what does `cart` look
|
|
997
|
-
like now" is one click rather than a hunt through a collapsed root object.
|
|
998
|
-
- **Diff** keeps its `+ / − / ±` rows as the default view and adds the other
|
|
999
|
-
five alongside. **Table** is the natural one — a diff *is* `path | op |
|
|
1000
|
-
before | after` — and Tree lets you open up a `before` that was an object,
|
|
1001
|
-
which the one-line rows could only ever summarise. Copying the rows view
|
|
1002
|
-
gives you a plain-text diff for a ticket, not JSON.
|
|
1003
|
-
|
|
1004
|
-
Right-click any request for **Copy as cURL** or **Copy as fetch** — the latter
|
|
1005
|
-
pastes straight into the browser console, with the body as an editable object
|
|
1006
|
-
literal rather than a pre-serialized string. The cURL snippet is quoted for
|
|
1007
|
-
POSIX shells (bash, zsh, Git Bash); it is **not** safe to paste into Windows
|
|
1008
|
-
`cmd` or PowerShell, which quote differently.
|
|
1009
|
-
|
|
1010
|
-
### Exporting the log
|
|
1011
|
-
|
|
1012
|
-
The **Export** button in the header offers six formats and a scope
|
|
1013
|
-
toggle — **Selected** (the one entry open in the detail pane: a request, a
|
|
1014
|
-
Redux action or a query), **Shown** (what the current section and filters
|
|
1015
|
-
leave visible) or **All**. It defaults to Shown, with every count on the
|
|
1016
|
-
control, so an export says what it will contain before you pick a format.
|
|
1017
|
-
Selected is disabled until you select an entry; if it was your last choice and
|
|
1018
|
-
nothing is selected, the menu opens on Shown instead.
|
|
1019
|
-
|
|
1020
|
-
| | Format | For |
|
|
1021
|
-
| --- | --- | --- |
|
|
1022
|
-
| Tool | **HAR** | Chrome DevTools, Charles, Insomnia, Postman — with the decrypted bodies. HTTP entries only. |
|
|
1023
|
-
| | **JSON** | Everything captured: frames, Redux diffs, timings. |
|
|
1024
|
-
| | **NDJSON** | One entry per line — pipe it into `jq`. |
|
|
1025
|
-
| Person | **Markdown** | A table plus failure bodies, copied to the clipboard for an issue, a PR or Slack. |
|
|
1026
|
-
| | **CSV** | One row per entry, no bodies — sort and count in a spreadsheet. Opens as UTF-8 in Excel. |
|
|
1027
|
-
| Shell | **cURL script** | Every request in order, runnable against another environment. |
|
|
1028
|
-
|
|
1029
|
-
Captured `Authorization` headers are masked, so cURL and fetch output carries a
|
|
1030
|
-
placeholder rather than a working token — use **Replay** for a real re-run.
|
|
1031
|
-
Every export and snippet stays masked even with Authorization shown in full in
|
|
1032
|
-
the panel (see [What is redacted](#what-is-redacted)).
|
|
1033
|
-
|
|
1034
|
-
### Themes
|
|
1035
|
-
|
|
1036
|
-
Twelve themes, under the theme button in the header — it names the one
|
|
1037
|
-
currently applied:
|
|
1038
|
-
|
|
1039
|
-
| | Theme | |
|
|
1040
|
-
| --- | --- | --- |
|
|
1041
|
-
| Dark | **Midnight** | Deep blue-black — the default |
|
|
1042
|
-
| | **Carbon** | True black, high contrast; for OLED displays |
|
|
1043
|
-
| | **Nord** | Muted arctic blues |
|
|
1044
|
-
| | **Tokyo Night** | Deep indigo, soft neon |
|
|
1045
|
-
| | **One Dark** | Atom's classic slate |
|
|
1046
|
-
| | **Mocha** | Catppuccin — gentle pastels |
|
|
1047
|
-
| | **Dracula** | Vivid purples and pinks |
|
|
1048
|
-
| | **Gruvbox** | Warm retro browns and amber |
|
|
1049
|
-
| Light | **Daylight** | Clean white — the light default |
|
|
1050
|
-
| | **GitHub** | The light theme you already read all day |
|
|
1051
|
-
| | **Latte** | Catppuccin — soft pastel light |
|
|
1052
|
-
| | **Solar** | Warm paper, low blue light |
|
|
1053
|
-
|
|
1054
|
-
**Hovering a theme applies it to the panel behind the menu**, so you can see a
|
|
1055
|
-
real payload in it before committing; moving away puts back the one you had.
|
|
1056
|
-
Arrow keys preview the same way.
|
|
1057
|
-
|
|
1058
|
-
The default is **System**: the panel reads the light/dark class off `<html>`
|
|
1059
|
-
and paints Midnight or Daylight to match, re-checking whenever your app's theme
|
|
1060
|
-
changes. Picking a specific theme overrides that. The choice is stored with the
|
|
1061
|
-
panel's other preferences and survives a reload.
|
|
1062
|
-
|
|
1063
|
-
The panel never inherits your app's styling — it portals outside every stacking
|
|
1064
|
-
context and ships its own palettes, so nothing you do to your own theme can
|
|
1065
|
-
distort it. Themes are complete rather than partial: every colour the panel
|
|
1066
|
-
paints, down to the JSON syntax highlighting and the timing bars, comes from
|
|
1067
|
-
the active theme. The two tones that carry text are held to 4.5:1 against every
|
|
1068
|
-
ground they land on, which is why a few of the ported palettes differ by a
|
|
1069
|
-
shade from the originals in the slots used for dense monospace. One tone is
|
|
1070
|
-
deliberately below it and may never hold text: it is for separators, payload
|
|
1071
|
-
punctuation and disabled glyphs.
|
|
1072
|
-
|
|
1073
|
-
There is no API for adding your own; a theme is 33 colour slots in
|
|
1074
|
-
`src/ui/themes/bx.ts` if you are working from source. Every theme fills the
|
|
1075
|
-
same set — a theme that cannot is reported in the console during development,
|
|
1076
|
-
along with any whose text tones fall under 4.5:1 on a ground they actually
|
|
1077
|
-
land on.
|
|
1078
|
-
|
|
1079
|
-
### `dbName` — when you need it
|
|
1080
|
-
|
|
1081
|
-
The panel can persist its log to IndexedDB so it survives a reload, but only
|
|
1082
|
-
when you opt in: **preserve-log is off by default**, and while it is off
|
|
1083
|
-
nothing is written to disk. See [Security](#security) for what the toggle does
|
|
1084
|
-
and what lands there.
|
|
1085
|
-
|
|
1086
|
-
**All of Blix's storage is scoped to the origin, not to your app.** That is how
|
|
1087
|
-
IndexedDB and `localStorage` both work, and it covers the captured log *and*
|
|
1088
|
-
your panel preferences — dock position, theme, density, the preserve-log
|
|
1089
|
-
toggle. Two apps served from the same origin (different ports in dev are
|
|
1090
|
-
different origins, but path-based routing, multi-zone Next.js setups and
|
|
1091
|
-
anything behind one reverse proxy are not) share every one of them: entries
|
|
1092
|
-
from one project appear in the other's panel, and whichever you opened last
|
|
1093
|
-
decides where the panel is docked.
|
|
1094
|
-
|
|
1095
|
-
`dbName` is how projects on one origin are kept apart. Give each app its own:
|
|
1096
|
-
|
|
1097
|
-
```tsx
|
|
1098
|
-
<Blix store={store} apiClient={apiClient} dbName="checkout" />
|
|
1099
|
-
```
|
|
1100
|
-
|
|
1101
|
-
You can also set it from the capture side, which is useful when capture starts
|
|
1102
|
-
before the panel mounts:
|
|
1103
|
-
|
|
1104
|
-
```ts
|
|
1105
|
-
attachHttpMonitor(apiClient, { dbName: "checkout" });
|
|
1106
|
-
```
|
|
1107
|
-
|
|
1108
|
-
Either call must happen before the database is first opened, which the panel
|
|
1109
|
-
does on mount. If both are set, the `<Blix />` prop wins, since render runs
|
|
1110
|
-
after module init. A call that arrives after the database is open is ignored —
|
|
1111
|
-
Blix does not switch databases at runtime — and says so in the console rather
|
|
1112
|
-
than failing quietly.
|
|
1113
|
-
|
|
1114
|
-
The name you pass is prefixed: `dbName="checkout"` gives you the database
|
|
1115
|
-
`blix:checkout` and the preferences key `blix:checkout:prefs`. Passing an
|
|
1116
|
-
already-prefixed name is fine and does not double it. Omitting `dbName`
|
|
1117
|
-
entirely gives you `blix:default`, shared with every other app on the origin
|
|
1118
|
-
that also omits it — and in development the panel warns once per mount when
|
|
1119
|
-
that happens, naming the fix.
|
|
1120
|
-
|
|
1121
|
-
Renaming is not a migration: the old database is left where it is rather than
|
|
1122
|
-
moved or deleted, and the new one starts empty at default preferences.
|
|
1123
|
-
|
|
1124
|
-
#### Databases on this origin
|
|
1125
|
-
|
|
1126
|
-
The status bar always shows which database the panel is on. On the shared
|
|
1127
|
-
default it turns amber and adds a `shared` tag, which is the visible form of
|
|
1128
|
-
the console warning above — click it to open the screen below. The same screen
|
|
1129
|
-
is in **⋯ More actions** and in the command palette.
|
|
1130
|
-
|
|
1131
|
-
Open the command palette (`Ctrl/⌘ K`) → **Databases on this origin** to see
|
|
1132
|
-
every Blix database the origin holds — one per project, plus `nm-devtools`,
|
|
1133
|
-
the single unprefixed database that all projects shared before names were
|
|
1134
|
-
prefixed. Each row shows an approximate size and can be deleted; the one this
|
|
1135
|
-
panel is using is marked and is not deletable from there, since it is open —
|
|
1136
|
-
use **Purge saved log** for that.
|
|
1137
|
-
|
|
1138
|
-
**Peek** on a row lists the 50 newest entries in that database — time, method,
|
|
1139
|
-
URL and status — so you can tell whose log it is before deleting it. It is a
|
|
1140
|
-
read-only snapshot: Blix opens the database, reads, and closes it again, so the
|
|
1141
|
-
list does not update and there is no detail pane, replay or export. The panel
|
|
1142
|
-
itself always stays on its own database; to work with another project's log
|
|
1143
|
-
properly, run that project and open the panel there.
|
|
1144
|
-
|
|
1145
|
-
The screen enumerates with `indexedDB.databases()`, which Firefox does not
|
|
1146
|
-
implement. There it falls back to the databases Blix has itself opened in that
|
|
1147
|
-
browser and labels the list as possibly incomplete.
|
|
1148
|
-
|
|
1149
|
-
---
|
|
1150
|
-
|
|
1151
|
-
## Production elimination
|
|
1152
|
-
|
|
1153
|
-
The panel is gated on a **literal** `process.env.NODE_ENV === "development"`
|
|
1154
|
-
check that survives verbatim into the published `dist/`. Your bundler
|
|
1155
|
-
substitutes it at *your* build time, folds the condition to `false`, and drops
|
|
1156
|
-
the dynamic `import()` of the panel along with the whole branch — so no panel
|
|
1157
|
-
code reaches your production bundle, and no chunk is emitted for it.
|
|
1158
|
-
|
|
1159
|
-
This is why the check is written inline rather than imported as a boolean
|
|
1160
|
-
constant: cross-module constant propagation is not guaranteed by every
|
|
1161
|
-
bundler, but a literal `process.env.NODE_ENV` comparison in the same file is
|
|
1162
|
-
handled by all of them.
|
|
1163
|
-
|
|
1164
|
-
The capture layer is gated on the same condition **plus a
|
|
1165
|
-
`typeof window !== "undefined"` check**, so `attachHttpMonitor` and friends
|
|
1166
|
-
become no-ops in production even though their call sites remain — and also
|
|
1167
|
-
during SSR, in the same dev build where they are live in the browser.
|
|
1168
|
-
|
|
1169
|
-
That second half is not a bundling concern but a correctness one. The capture
|
|
1170
|
-
module is reachable from your HTTP-client module, which typically also runs on
|
|
1171
|
-
the server, and the monitor's buffer is a module-level singleton: on a
|
|
1172
|
-
long-lived Node process it would otherwise accumulate every user's request
|
|
1173
|
-
payloads for the lifetime of the server. Keeping the server-side singleton
|
|
1174
|
-
permanently empty is the point.
|
|
1175
|
-
|
|
1176
|
-
Practically: a `captureEncrypted` call that runs during SSR does nothing, and
|
|
1177
|
-
`<Blix />` renders nothing there. It also renders nothing during hydration, so
|
|
1178
|
-
its first client render matches the server's HTML; the panel mounts on the
|
|
1179
|
-
render right after. (Before 0.9.0 it branched on `typeof window` instead,
|
|
1180
|
-
which Next.js reported as a hydration mismatch on every load.)
|
|
1181
|
-
|
|
1182
|
-
**If the panel disappears in development**, check the console for
|
|
1183
|
-
`[blix] the panel crashed`. Blix renders inside an error boundary of its own,
|
|
1184
|
-
so a failure in the panel removes the panel until the next reload and leaves
|
|
1185
|
-
your app running — the logged error is the thing to report.
|
|
1186
|
-
|
|
1187
|
-
### Enabling Blix in a production build (staging)
|
|
1188
|
-
|
|
1189
|
-
A staging or test server usually runs a production build, where the panel is
|
|
1190
|
-
gone by design. Setting `NODE_ENV=development` for that build does not bring
|
|
1191
|
-
it back on Next.js 16: `next build` runs on Turbopack and inlines `NODE_ENV`
|
|
1192
|
-
as `"production"` whatever the environment says.
|
|
1193
|
-
|
|
1194
|
-
Opt in explicitly instead, in that server's env file:
|
|
1195
|
-
|
|
1196
|
-
```bash
|
|
1197
|
-
NEXT_PUBLIC_BLIX=true
|
|
1198
|
-
```
|
|
1199
|
-
|
|
1200
|
-
The panel and the capture layer both check it next to `NODE_ENV`, as the same
|
|
1201
|
-
kind of literal comparison, so Next.js inlines it at build time.
|
|
1202
|
-
|
|
1203
|
-
> **Set `NEXT_PUBLIC_BLIX=false` in your production env file.** Next.js only
|
|
1204
|
-
> inlines variables that are defined. Left unset, the comparison stays in the
|
|
1205
|
-
> bundle as a runtime check: the panel still never loads — the variable is
|
|
1206
|
-
> undefined in the browser — but its chunk is emitted into your production
|
|
1207
|
-
> build. `false` lets the bundler fold the check and drop the chunk.
|
|
1208
|
-
|
|
1209
|
-
**Never set `NEXT_PUBLIC_BLIX=true` for a build that real users get.** It
|
|
1210
|
-
ships the monitor to them, and with it everything it captures — see
|
|
1211
|
-
[Security](#security).
|
|
1212
|
-
|
|
1213
|
-
**Your own guards need the variable too.** A mount component that returns
|
|
1214
|
-
`null` outside development (as in [Mounting the panel](#mounting-the-panel)),
|
|
1215
|
-
or a call-site guard around `attachHttpMonitor` (below), keeps Blix off on
|
|
1216
|
-
staging regardless of the variable. Add it to each:
|
|
1217
|
-
|
|
1218
|
-
```ts
|
|
1219
|
-
if (
|
|
1220
|
-
(process.env.NODE_ENV === "development" ||
|
|
1221
|
-
process.env.NEXT_PUBLIC_BLIX === "true") &&
|
|
1222
|
-
typeof window !== "undefined"
|
|
1223
|
-
) {
|
|
1224
|
-
attachHttpMonitor(apiClient);
|
|
1225
|
-
}
|
|
1226
|
-
```
|
|
1227
|
-
|
|
1228
|
-
The variable is read under its `NEXT_PUBLIC_` name, which is a Next.js
|
|
1229
|
-
convention. The opt-in has been verified with Next.js 16 (Turbopack) only;
|
|
1230
|
-
other bundlers do not inline that name by default.
|
|
1231
|
-
|
|
1232
|
-
So `NEXT_PUBLIC_BLIX` is honoured only where the bundler inlines it. On Vite,
|
|
1233
|
-
esbuild, Rollup and the like it is ignored, safely: the read is guarded, so it
|
|
1234
|
-
does not depend on a `process` global existing in the browser, and Blix stays
|
|
1235
|
-
off in production.
|
|
1236
|
-
|
|
1237
|
-
### Guard your call sites
|
|
1238
|
-
|
|
1239
|
-
**No-op is not the same as eliminated.** The argument above works for the
|
|
1240
|
-
panel because the literal check lives in the file that makes the dynamic
|
|
1241
|
-
import. It does *not* carry over to the capture layer, and the reason is the
|
|
1242
|
-
same one that motivated writing the check inline in the first place.
|
|
1243
|
-
|
|
1244
|
-
Every capture function tests a single internal constant inside its own body,
|
|
1245
|
-
and that constant lives in Blix's module, not yours. Your bundler folds the
|
|
1246
|
-
constant to `false` — but it keeps the function bodies that reference it,
|
|
1247
|
-
because your call site still imports them. The result is tens of kilobytes of
|
|
1248
|
-
capture runtime in a production bundle where every entry point is dead.
|
|
1249
|
-
|
|
1250
|
-
To drop it, write the same literal check in the file that makes the call,
|
|
1251
|
-
exactly as Blix does internally:
|
|
1252
|
-
|
|
1253
|
-
```ts
|
|
1254
|
-
// ✅ folded away in production — the whole capture chunk is dropped
|
|
1255
|
-
export const apiClient =
|
|
1256
|
-
process.env.NODE_ENV === "development"
|
|
1257
|
-
? withInitiatorCapture(createClient())
|
|
1258
|
-
: createClient();
|
|
35
|
+
import { attachFetchMonitor } from "@hakam-aldeen-kh/blix/capture";
|
|
1259
36
|
|
|
37
|
+
// Module scope, not an effect: requests made before React mounts are captured too.
|
|
1260
38
|
if (process.env.NODE_ENV === "development" && typeof window !== "undefined") {
|
|
1261
|
-
|
|
39
|
+
attachFetchMonitor();
|
|
1262
40
|
}
|
|
1263
|
-
```
|
|
1264
41
|
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
attachHttpMonitor(apiClient);
|
|
42
|
+
export default function BlixMount() {
|
|
43
|
+
return <Blix dbName="my-app" />;
|
|
44
|
+
}
|
|
1269
45
|
```
|
|
1270
46
|
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
- **Write the condition out literally, in the file that makes the call.**
|
|
1274
|
-
Hoisting it into a shared `const IS_DEV` defeats the folding, for the same
|
|
1275
|
-
reason Blix writes it inline in its own source.
|
|
1276
|
-
- **Include `typeof window !== "undefined"`** when the module also evaluates on
|
|
1277
|
-
the server, which in the Next.js App Router it generally does. It is
|
|
1278
|
-
redundant with Blix's internal guard, but it keeps the folded branch
|
|
1279
|
-
unambiguous for the bundler and matches the condition Blix uses internally.
|
|
1280
|
-
|
|
1281
|
-
`createReduxMonitorMiddleware` needs the middleware callback restructured
|
|
1282
|
-
rather than a one-line guard — see the [Redux](#redux--createreduxmonitormiddlewareoptions)
|
|
1283
|
-
example above, which is written in the guarded form.
|
|
1284
|
-
|
|
1285
|
-
---
|
|
1286
|
-
|
|
1287
|
-
## Security
|
|
1288
|
-
|
|
1289
|
-
Blix is a debugger, and it captures what a debugger has to capture: **full
|
|
1290
|
-
request and response bodies, request and response headers, Redux action
|
|
1291
|
-
payloads and state diffs, and realtime frames**. It captures them in
|
|
1292
|
-
plaintext — the request body *before* your encryption interceptor runs, the
|
|
1293
|
-
response body *after* your decryption interceptor. That is the whole point of
|
|
1294
|
-
it, and it means the log holds whatever your traffic holds, credentials
|
|
1295
|
-
included.
|
|
1296
|
-
|
|
1297
|
-
`attachFetchMonitor` widens that to **every `fetch` the page makes**, not only
|
|
1298
|
-
your own: an analytics snippet, a chat widget or a library calling `fetch`
|
|
1299
|
-
under the hood is captured the same way, bodies included. Use its `ignore`
|
|
1300
|
-
option to keep a third party's traffic out of the log.
|
|
1301
|
-
|
|
1302
|
-
By default all of that is **in memory only**. Nothing is written to disk, and
|
|
1303
|
-
a reload starts clean.
|
|
1304
|
-
|
|
1305
|
-
All of it is dev-only unless you opt a build in. Capture is gated on
|
|
1306
|
-
`process.env.NODE_ENV === "development" && typeof window !== "undefined"`, and
|
|
1307
|
-
the database is opened only when the panel mounts — see
|
|
1308
|
-
[Production elimination](#production-elimination). A build made with
|
|
1309
|
-
`NEXT_PUBLIC_BLIX=true` captures exactly as development does, so everything in
|
|
1310
|
-
this section applies to whoever uses that build.
|
|
1311
|
-
|
|
1312
|
-
### What reaches disk, and when
|
|
1313
|
-
|
|
1314
|
-
**Preserve-log** is the sole gate on disk writes. It is off by default, and
|
|
1315
|
-
while it is off nothing Blix captures reaches IndexedDB — the store is
|
|
1316
|
-
actively cleared on every panel mount.
|
|
47
|
+
Render `<BlixMount />` once, in your root layout. Run the dev server and the panel's launcher appears on the page; Ctrl/⌘ + backtick toggles the panel.
|
|
1317
48
|
|
|
1318
|
-
|
|
49
|
+
Using axios? Call `attachHttpMonitor(apiClient)` in the module that creates the instance, after your own interceptors are registered. Redux, TanStack Query and realtime each take one call as well.
|
|
1319
50
|
|
|
1320
|
-
|
|
1321
|
-
| --- | --- |
|
|
1322
|
-
| Header button | Keeps its icon at every width; loses its label when the panel is narrow |
|
|
1323
|
-
| **⋯ More actions** overflow menu | — |
|
|
1324
|
-
| Command palette (`Ctrl/⌘ K`) | Listed as **Preserve log across reloads** / **Stop preserving the log** |
|
|
1325
|
-
| `Shift+L` | — |
|
|
51
|
+
## Documentation
|
|
1326
52
|
|
|
1327
|
-
|
|
53
|
+
**[Full documentation → blixjs.dev/docs](https://blixjs.dev/docs)**
|
|
1328
54
|
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
again before you assume otherwise; it is the one behaviour here that
|
|
1335
|
-
reasonably surprises people.
|
|
1336
|
-
|
|
1337
|
-
With preserve-log on, this is what is kept:
|
|
1338
|
-
|
|
1339
|
-
| Captured | Persisted |
|
|
1340
|
-
| --- | --- |
|
|
1341
|
-
| HTTP entries — bodies, headers, timings | yes |
|
|
1342
|
-
| Realtime frames | yes |
|
|
1343
|
-
| The encrypted envelope, if you call `captureEncrypted` | yes |
|
|
1344
|
-
| Panel preferences and budget totals | yes — preferences are also mirrored to `localStorage` under `blix:<dbName>:prefs` |
|
|
1345
|
-
| Redux actions, payloads and diffs | only if you pin the row |
|
|
1346
|
-
| Query cache rows | only if you pin the row |
|
|
1347
|
-
|
|
1348
|
-
One thing is written regardless of preserve-log: opening the database records
|
|
1349
|
-
its name in the origin-wide `localStorage` key `blix:databases`, which is how
|
|
1350
|
-
[Databases on this origin](#databases-on-this-origin) finds it in browsers
|
|
1351
|
-
without `indexedDB.databases()`. It holds database names and nothing else.
|
|
1352
|
-
|
|
1353
|
-
### What is redacted
|
|
1354
|
-
|
|
1355
|
-
Exactly four header names, and nothing else:
|
|
1356
|
-
|
|
1357
|
-
| Header | Match |
|
|
1358
|
-
| --- | --- |
|
|
1359
|
-
| `authorization` | exact, case-insensitive |
|
|
1360
|
-
| `cookie` | exact, case-insensitive |
|
|
1361
|
-
| `set-cookie` | exact, case-insensitive |
|
|
1362
|
-
| `x-api-key` | exact, case-insensitive |
|
|
1363
|
-
|
|
1364
|
-
The match is **exact on the full header name** — not a prefix, not a
|
|
1365
|
-
substring, not a pattern. Near-miss names are *not* covered and are written in
|
|
1366
|
-
the clear: `x-auth-token` and `api-key` are the two that most often catch
|
|
1367
|
-
people out, and `proxy-authorization`, `x-csrf-token` and
|
|
1368
|
-
`x-amz-security-token` are equally uncovered. If your auth travels in a header
|
|
1369
|
-
that is not one of the four above, it is captured verbatim.
|
|
1370
|
-
|
|
1371
|
-
The same four are masked whichever client made the request, and whatever form
|
|
1372
|
-
the headers were passed in: `AxiosHeaders`, a plain object, a `Headers`
|
|
1373
|
-
instance or `[name, value]` pairs.
|
|
1374
|
-
|
|
1375
|
-
Masking is partial rather than total: for a value longer than 12 characters
|
|
1376
|
-
the first 8 and last 4 survive, so you can still tell which token you sent.
|
|
1377
|
-
Shorter values are replaced outright. The Headers tab tags every masked row
|
|
1378
|
-
`MASKED` rather than leaving you to infer it from an ellipsis, and the stored
|
|
1379
|
-
value keeps a `(masked)` suffix so every export path carries the fact too.
|
|
1380
|
-
|
|
1381
|
-
**Errors are described, never copied.** A failed TanStack query or mutation,
|
|
1382
|
-
and a Redux action whose payload is an error, are stored as a described error
|
|
1383
|
-
— kind, name, code, message, status, the response body, cause and stack —
|
|
1384
|
-
and never as the error object itself. An `AxiosError` carries the request's
|
|
1385
|
-
config and headers as ordinary properties; none of that is read.
|
|
1386
|
-
|
|
1387
|
-
**The four names are also masked as keys, at any depth, in data Blix copies**:
|
|
1388
|
-
Redux payloads and state diffs, query and mutation data, and everything
|
|
1389
|
-
written to IndexedDB. An HTTP body is still shown as captured while it is
|
|
1390
|
-
live in the panel — a key named `authorization` inside one is masked only in
|
|
1391
|
-
the saved copy.
|
|
1392
|
-
|
|
1393
|
-
#### Showing `Authorization` in full
|
|
1394
|
-
|
|
1395
|
-
Blix is development-only, and a developer reading their own token on their own
|
|
1396
|
-
screen is not the risk — the token leaving the machine is. So masking of
|
|
1397
|
-
`authorization`, and only `authorization`, can be switched off: command palette
|
|
1398
|
-
→ **Show Authorization values in full**, or **⋯ More actions** → **Show
|
|
1399
|
-
Authorization in full**. It is a saved preference, off by default, and while it
|
|
1400
|
-
is on the status bar reads **UNMASKED Authorization** and every such row in the
|
|
1401
|
-
Headers tab is tagged `UNMASKED`.
|
|
1402
|
-
|
|
1403
|
-
- It applies to requests captured **after** it is switched on. Earlier entries
|
|
1404
|
-
were masked when they were captured — the full value was never kept — and
|
|
1405
|
-
the panel cannot reveal them; their row says so. An axios request re-reads
|
|
1406
|
-
its headers when it settles (see
|
|
1407
|
-
[Which request headers the panel shows](#which-request-headers-the-panel-shows)),
|
|
1408
|
-
so for one still in flight what counts is the setting at the moment it
|
|
1409
|
-
settles.
|
|
1410
|
-
- The full value is held in memory only. It is **never** written to IndexedDB,
|
|
1411
|
-
and **every** export and copy path — HAR, JSON, NDJSON, CSV, Markdown, the
|
|
1412
|
-
cURL script, **Copy as cURL** and **Copy as fetch** — still carries the
|
|
1413
|
-
masked value. The row's own **Copy** button is the one way to take the full
|
|
1414
|
-
value.
|
|
1415
|
-
- Masking again discards every full value held.
|
|
1416
|
-
- `cookie`, `set-cookie`, `x-api-key` and realtime connect tokens stay masked
|
|
1417
|
-
regardless.
|
|
1418
|
-
|
|
1419
|
-
#### JWT claims
|
|
1420
|
-
|
|
1421
|
-
When an `Authorization` value is a JWT, Blix decodes it — for axios when the
|
|
1422
|
-
request settles, for `fetch` when it is made — and keeps only `alg`, `sub`,
|
|
1423
|
-
`iss`, `iat` and `exp`, never the token or its signature.
|
|
1424
|
-
The **JWT** chip on that row opens them, with the time left until `exp`, in red
|
|
1425
|
-
once it has passed. This works with masking on. The signature is not verified,
|
|
1426
|
-
and a value that is not a well-formed JWT stores no claims at all. The claims
|
|
1427
|
-
say where they were read: when a `fetch` call was made, from the headers an
|
|
1428
|
-
axios request settled with, or — for an axios request that never settled, or
|
|
1429
|
-
used the `auth` option — when it started.
|
|
1430
|
-
|
|
1431
|
-
`alg` is shown but never acted on: a missing, empty or non-string `alg` still
|
|
1432
|
-
yields every other claim, and no value of it withholds anything. The XML-DSig
|
|
1433
|
-
URIs .NET writes into `alg` — `http://www.w3.org/2001/04/xmldsig-more#hmac-sha256`
|
|
1434
|
-
and its HMAC, RSA, RSA-PSS and ECDSA siblings — are shown by name, as
|
|
1435
|
-
`HMAC-SHA256 (HS256)`, with the raw value in the tooltip; the entry and the
|
|
1436
|
-
exports keep the raw value. An `alg` of `none`, in any letter case, is flagged
|
|
1437
|
-
as an unsigned token. The claims
|
|
1438
|
-
are part of the entry, so — unlike the token — they are saved with
|
|
1439
|
-
preserve-log on and included in the JSON and NDJSON exports.
|
|
1440
|
-
|
|
1441
|
-
**Beyond those four key names, nothing inside a body is redacted.** Request
|
|
1442
|
-
bodies, response bodies, error payloads, the encrypted request/response
|
|
1443
|
-
values, Redux payloads and diffs, and realtime frames pass through a walker
|
|
1444
|
-
that truncates large values and looks for those four names only. A
|
|
1445
|
-
`password`, `ssn` or `refreshToken` field is captured, and with preserve-log
|
|
1446
|
-
on written to disk, exactly as it appears. Blix has no mechanism to do otherwise: it has no schema
|
|
1447
|
-
for your payloads and no way to tell a secret from any other string.
|
|
1448
|
-
|
|
1449
|
-
### Retention and clearing
|
|
1450
|
-
|
|
1451
|
-
| Bound | Value |
|
|
1452
|
-
| --- | --- |
|
|
1453
|
-
| Records | 200 |
|
|
1454
|
-
| Total size | 24 MB — HTTP entries only |
|
|
1455
|
-
| Per payload field | 512 KB |
|
|
1456
|
-
| Eviction | oldest first, once either bound is exceeded |
|
|
1457
|
-
| Time-based expiry | none |
|
|
1458
|
-
|
|
1459
|
-
There is **no TTL of any kind**. A record leaves the database when 200 newer
|
|
1460
|
-
records or 24 MB of newer traffic push it out, or when you clear it yourself.
|
|
1461
|
-
On a low-traffic app with preserve-log left on, a captured token stays in the
|
|
1462
|
-
browser profile indefinitely.
|
|
1463
|
-
|
|
1464
|
-
Three ways to purge:
|
|
1465
|
-
|
|
1466
|
-
| Where | Note |
|
|
1467
|
-
| --- | --- |
|
|
1468
|
-
| The persisted-size label in the status bar — the one reading `12 saved · 3.4 MB` | The label *is* the control, and it is the only one that asks twice: click once to arm it, at which point it changes to `Purge saved log?`, and click again. It disarms itself after three seconds. Rendered only while preserve-log is on |
|
|
1469
|
-
| **⋯ More actions** → **Purge saved log** | Deletes on a single press, with no confirmation |
|
|
1470
|
-
| Command palette (`Ctrl/⌘ K`) → **Purge the saved log** | Deletes on a single press, with no confirmation |
|
|
1471
|
-
|
|
1472
|
-
The menu and palette entries are disabled when nothing is on disk, but unlike
|
|
1473
|
-
the status-bar label they do not depend on preserve-log being on — so a log
|
|
1474
|
-
written earlier in the session can still be deleted after you have switched
|
|
1475
|
-
the toggle off.
|
|
1476
|
-
|
|
1477
|
-
Switching preserve-log **off** also clears the stored entries, so turning it
|
|
1478
|
-
off is itself a way to drop everything Blix has written.
|
|
1479
|
-
|
|
1480
|
-
A purge can be **blocked**: IndexedDB will not delete a database that another
|
|
1481
|
-
tab still holds open. The panel reports that, naming the database, instead of
|
|
1482
|
-
reporting the log purged while it is still on disk. Close the other tabs
|
|
1483
|
-
running the app and purge again.
|
|
1484
|
-
|
|
1485
|
-
Other projects' databases on the same origin — and the legacy `nm-devtools`
|
|
1486
|
-
database that 0.5.x and earlier wrote — are deleted from
|
|
1487
|
-
[Databases on this origin](#databases-on-this-origin), not by Purge.
|
|
1488
|
-
|
|
1489
|
-
Every path clears the captured entries; Purge additionally deletes the
|
|
1490
|
-
IndexedDB database itself. **Your panel preferences survive either way** —
|
|
1491
|
-
they are mirrored to `localStorage`, and a fresh database is re-seeded from
|
|
1492
|
-
that mirror on the next boot. Both the database and its mirror key are scoped
|
|
1493
|
-
to this project's `dbName`, so a purge affects only the project that ran it and
|
|
1494
|
-
cannot restore — or destroy — another project's preferences on the same origin.
|
|
1495
|
-
There is no UI or API for clearing them, and no programmatic API for purging
|
|
1496
|
-
either.
|
|
1497
|
-
|
|
1498
|
-
### Threat model
|
|
1499
|
-
|
|
1500
|
-
IndexedDB is scoped **per origin, not per app**, and it is not encrypted at
|
|
1501
|
-
rest. Any script running on that origin can read Blix's database — including
|
|
1502
|
-
browser extension content scripts with access to the origin. Whatever you
|
|
1503
|
-
capture is readable by whatever you have installed.
|
|
1504
|
-
|
|
1505
|
-
A per-project `dbName` does not change that. It keeps projects from mixing
|
|
1506
|
-
their logs; it does not isolate them. The panel itself can open another
|
|
1507
|
-
project's database on the same origin — **Peek** in
|
|
1508
|
-
[Databases on this origin](#databases-on-this-origin) lists its newest URLs,
|
|
1509
|
-
methods and statuses — and so can any other script there.
|
|
1510
|
-
|
|
1511
|
-
Export and copy move captured data out of the browser entirely:
|
|
1512
|
-
|
|
1513
|
-
| Path | Carries |
|
|
1514
|
-
| --- | --- |
|
|
1515
|
-
| HAR export | Decrypted request and response bodies |
|
|
1516
|
-
| JSON / NDJSON export | Everything captured — frames, Redux diffs, timings |
|
|
1517
|
-
| **Copy as cURL** / **Copy as fetch** | Headers and bodies, with the four redacted headers masked — so the output is not a working request |
|
|
1518
|
-
|
|
1519
|
-
HAR is the one to watch. It is a plain JSON file carrying your decrypted
|
|
1520
|
-
bodies, and it is the artifact most likely to end up attached to a ticket.
|
|
1521
|
-
|
|
1522
|
-
### If you handle sensitive data
|
|
1523
|
-
|
|
1524
|
-
- **Leave preserve-log off unless you actively need it.** It is off by
|
|
1525
|
-
default. In-memory capture already gives you the entire panel; the toggle
|
|
1526
|
-
buys you nothing but survival across a reload.
|
|
1527
|
-
- **Clear the log after any session that captured an auth flow** —
|
|
1528
|
-
switching preserve-log off is enough; Purge additionally deletes the
|
|
1529
|
-
database, though neither removes your panel preferences.
|
|
1530
|
-
- **Use the Redux `ignore` option** for action types that carry credentials or
|
|
1531
|
-
personal data, so they are never captured in the first place. See
|
|
1532
|
-
[Redux](#redux--createreduxmonitormiddlewareoptions).
|
|
1533
|
-
- **Treat an exported HAR as a credential-bearing file.** Do not attach one to
|
|
1534
|
-
a public issue, and do not commit one.
|
|
1535
|
-
- **After upgrading from 0.5.x or earlier, delete `nm-devtools`.** Blix no
|
|
1536
|
-
longer reads or writes that database, and it does not delete it for you:
|
|
1537
|
-
anything it holds stays on disk until you remove it from
|
|
1538
|
-
[Databases on this origin](#databases-on-this-origin).
|
|
1539
|
-
- **Scope `attachFetchMonitor` with `ignore`** if third-party scripts on the
|
|
1540
|
-
page send data you do not want in the log.
|
|
1541
|
-
|
|
1542
|
-
---
|
|
1543
|
-
|
|
1544
|
-
## Entry points
|
|
1545
|
-
|
|
1546
|
-
| Import | Contents | `"use client"` |
|
|
1547
|
-
| --- | --- | --- |
|
|
1548
|
-
| `@hakam-aldeen-kh/blix` | `Blix`, `BlixProps` + everything below | **yes** |
|
|
1549
|
-
| `@hakam-aldeen-kh/blix/capture` | capture functions and types only — no React | no |
|
|
1550
|
-
|
|
1551
|
-
Import capture functions from `/capture` in modules that run during SSR or at
|
|
1552
|
-
module-eval time. It pulls in no React code and carries no `"use client"`
|
|
1553
|
-
directive, so it stays usable from a server module — which the root entry, by
|
|
1554
|
-
virtue of the directive that lets `<Blix />` be rendered from a server
|
|
1555
|
-
component, is not.
|
|
1556
|
-
|
|
1557
|
-
The `/capture` entry exports `attachHttpMonitor`, `attachFetchMonitor`,
|
|
1558
|
-
`captureEncrypted`, `createReduxMonitorMiddleware`, `tapQueryClient`,
|
|
1559
|
-
`tapRealtimeAdapter`, `withInitiatorCapture`, and the supporting types
|
|
1560
|
-
(`EncryptedPayload`, `FetchMonitorOptions`, `FetchIgnoreRule`,
|
|
1561
|
-
`ReduxCaptureOptions`, `RealtimeAdapterLike`, `MonitorEntry`, …).
|
|
55
|
+
- [Install](https://blixjs.dev/docs/install): peer dependencies and build setup
|
|
56
|
+
- [Capture](https://blixjs.dev/docs/capture): where each capture call goes, and why the order matters
|
|
57
|
+
- [Mounting the panel](https://blixjs.dev/docs/mounting-the-panel): props, shortcuts, filtering, export, themes
|
|
58
|
+
- [Production elimination](https://blixjs.dev/docs/production-elimination): how it is removed, and how to keep it on a staging build
|
|
59
|
+
- [Upgrading](https://blixjs.dev/docs/upgrading): what changed for projects coming from 0.10
|
|
1562
60
|
|
|
1563
61
|
---
|
|
1564
62
|
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
MIT — see [LICENSE](https://github.com/Hakam-aldeen-Kh/blix/blob/main/LICENSE).
|
|
63
|
+
[Website](https://blixjs.dev) · [Changelog](https://blixjs.dev/changelog) · [Security](https://github.com/Hakam-aldeen-Kh/blix/blob/main/SECURITY.md) · [MIT License](https://github.com/Hakam-aldeen-Kh/blix/blob/main/LICENSE) · Made by [Hakam aldeen Alkhadraa](https://www.hakam-aldeen.com)
|