@hakam-aldeen-kh/blix 0.10.0 → 0.11.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 CHANGED
@@ -43,7 +43,7 @@ no reason to carry a second build output.
43
43
 
44
44
  ### Peer dependencies
45
45
 
46
- `react` and `react-dom` (v19) are required. `axios`, `@reduxjs/toolkit` and
46
+ `react` and `react-dom` (v18 or v19) are required. `axios`, `@reduxjs/toolkit` and
47
47
  `@tanstack/react-query` are **optional** peers — you only need the ones whose
48
48
  capture you actually use. The capture layer is structurally typed against each
49
49
  of them and imports none of them — not at runtime, and not in its published
@@ -58,6 +58,31 @@ axios installed, not even for types.
58
58
 
59
59
  ---
60
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
+
61
86
  ## The module-init call-order contract
62
87
 
63
88
  **This is the part that is easy to get wrong.** Capture functions must be
@@ -175,59 +200,56 @@ ordering is actually correct.
175
200
 
176
201
  #### Failed requests and the shape of your rejection
177
202
 
178
- Blix correlates a response — success **or** error — back to its entry through
179
- `error.config`. If your response interceptor normalises errors into your own
180
- domain type, a very common pattern, the value Blix receives is a plain object
181
- with no `config` on it:
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:
182
207
 
183
208
  ```ts
184
- // ❌ Blix can no longer see the entry: no `.config` on the rejected value
209
+ // Fine since 0.11: the failure was recorded before this ran
185
210
  instance.interceptors.response.use(undefined, (error) =>
186
211
  Promise.reject(error.response?.data ?? fallbackError),
187
212
  );
188
213
  ```
189
214
 
190
- The consequence is silent and total: **every non-2xx request stays `pending`
191
- in the panel for the rest of the session.** No error, no warning, no Failed
192
- filter. Nothing times the row out either: its duration keeps counting for as
193
- long as the page stays open.
194
-
195
- Two ways out, and you currently have to choose one:
196
-
197
- 1. **Keep the config on your normalised error** — attach `config` (or the
198
- original `AxiosError`) to the object you reject with. This preserves
199
- plaintext request capture and is the recommended fix.
200
- 2. **Register Blix before your normalising handler**, so it runs first on the
201
- response path:
202
-
203
- ```ts
204
- attachHttpMonitor(apiClient); // first on the response path
205
- apiClient.interceptors.request.use(addAuth);
206
- apiClient.interceptors.response.use(undefined, normaliseError);
207
- ```
208
-
209
- This costs you plaintext request capture, because Blix's request interceptor
210
- now runs last — after encryption.
211
-
212
- From a single `attachHttpMonitor` call you cannot currently have both plaintext
213
- request bodies and correlated errors while also discarding the `AxiosError`.
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.
214
238
 
215
239
  #### Response interceptors that unwrap
216
240
 
217
- The same thing happens on the success path. A response interceptor registered
218
- before Blix that returns something other than the response —
219
- `(response) => response.data` is the usual one — hands Blix a value with no
220
- `config` on it, so there is nothing to correlate:
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:
221
246
 
222
247
  - your request is unaffected: your code receives exactly what your interceptor
223
248
  returned. (Before 0.7.0 Blix threw at this point, and every request on the
224
249
  instance failed.)
225
- - the entry **stays `pending`** for the rest of the session. Blix does not
226
- settle a request it cannot see settle.
227
-
228
- Register the unwrapping interceptor **after** `attachHttpMonitor` — response
229
- interceptors run in registration order, so Blix then sees the full response
230
- first — or unwrap at the call site instead.
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.
231
253
 
232
254
  #### Which request headers the panel shows
233
255
 
@@ -585,8 +607,72 @@ logged exactly once. The order of the two calls does not matter.
585
607
  request.
586
608
  - **No `captureEncrypted`** — see
587
609
  [Encrypted payloads](#encrypted-payloads--captureencryptedconfig-payload).
588
- - **No timeout.** A request that never answers stays pending, exactly as an
589
- axios request does.
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.
590
676
 
591
677
  ### Redux — `createReduxMonitorMiddleware(options?)`
592
678
 
@@ -816,8 +902,56 @@ Query, each with its rail icon — and, in red beside a count, how many of that
816
902
  source's entries failed. The dot is capture state: green while capturing,
817
903
  amber while paused, pulsing while something is in flight.
818
904
 
819
- The panel opens **floating** the first time. Dock it to the bottom or the
820
- right from the header; the choice is saved per project.
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.
821
955
 
822
956
  **`Ctrl/⌘ K` opens the command palette**, and for several things it is the only
823
957
  way in — sort order, row density, dock position, the copy formats, the filter
@@ -869,7 +1003,9 @@ frames, the encrypted envelope, Redux actions, and both Redux state views:
869
1003
 
870
1004
  Right-click any request for **Copy as cURL** or **Copy as fetch** — the latter
871
1005
  pastes straight into the browser console, with the body as an editable object
872
- literal rather than a pre-serialized string.
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.
873
1009
 
874
1010
  ### Exporting the log
875
1011
 
@@ -1043,6 +1179,11 @@ its first client render matches the server's HTML; the panel mounts on the
1043
1179
  render right after. (Before 0.9.0 it branched on `typeof window` instead,
1044
1180
  which Next.js reported as a hydration mismatch on every load.)
1045
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
+
1046
1187
  ### Enabling Blix in a production build (staging)
1047
1188
 
1048
1189
  A staging or test server usually runs a production build, where the panel is
@@ -1088,6 +1229,11 @@ The variable is read under its `NEXT_PUBLIC_` name, which is a Next.js
1088
1229
  convention. The opt-in has been verified with Next.js 16 (Turbopack) only;
1089
1230
  other bundlers do not inline that name by default.
1090
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
+
1091
1237
  ### Guard your call sites
1092
1238
 
1093
1239
  **No-op is not the same as eliminated.** The argument above works for the
@@ -1232,6 +1378,18 @@ Shorter values are replaced outright. The Headers tab tags every masked row
1232
1378
  `MASKED` rather than leaving you to infer it from an ellipsis, and the stored
1233
1379
  value keeps a `(masked)` suffix so every export path carries the fact too.
1234
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
+
1235
1393
  #### Showing `Authorization` in full
1236
1394
 
1237
1395
  Blix is development-only, and a developer reading their own token on their own
@@ -1280,12 +1438,12 @@ as an unsigned token. The claims
1280
1438
  are part of the entry, so — unlike the token — they are saved with
1281
1439
  preserve-log on and included in the JSON and NDJSON exports.
1282
1440
 
1283
- **Nothing inside a body is redacted.** Request bodies, response bodies, error
1284
- payloads, the encrypted request/response values, Redux payloads and diffs, and
1285
- realtime frames all pass through a size-only walker — it truncates large
1286
- values and never once inspects a key name. A `password`, `ssn` or
1287
- `refreshToken` field is captured, and with preserve-log on written to disk,
1288
- exactly as it appears. Blix has no mechanism to do otherwise: it has no schema
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
1289
1447
  for your payloads and no way to tell a secret from any other string.
1290
1448
 
1291
1449
  ### Retention and clearing