@hakam-aldeen-kh/blix 0.3.2
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/LICENSE +21 -0
- package/README.md +720 -0
- package/dist/DevToolsPanel-GBMCZRMW.js +5895 -0
- package/dist/capture/index.d.ts +548 -0
- package/dist/capture/index.js +18 -0
- package/dist/chunk-MRTSIXR7.js +1076 -0
- package/dist/chunk-OQHKPRNI.js +287 -0
- package/dist/chunk-PQIOIMAQ.js +7 -0
- package/dist/index.d.ts +75 -0
- package/dist/index.js +40 -0
- package/package.json +106 -0
|
@@ -0,0 +1,548 @@
|
|
|
1
|
+
import { AxiosInstance } from 'axios';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Blix — HTTP capture entry point.
|
|
5
|
+
*
|
|
6
|
+
* Registers axios interceptors that feed the monitor store with plaintext
|
|
7
|
+
* request/response data. Call once at module-init time, before your app's
|
|
8
|
+
* encryption or auth interceptors, so this interceptor runs last in axios's
|
|
9
|
+
* LIFO request stack and therefore sees the plaintext request body.
|
|
10
|
+
*
|
|
11
|
+
* No-op when `process.env.NODE_ENV !== "development"`.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Attaches HTTP-capture interceptors to an axios instance.
|
|
16
|
+
*
|
|
17
|
+
* Call this at module init time (e.g. at the bottom of your `axios.ts`),
|
|
18
|
+
* after registering any encryption interceptors so that the monitor sees the
|
|
19
|
+
* plaintext body first.
|
|
20
|
+
*/
|
|
21
|
+
declare function attachHttpMonitor(instance: AxiosInstance, options?: {
|
|
22
|
+
dbName?: string;
|
|
23
|
+
}): void;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Blix — encrypted-payload capture (opt-in).
|
|
27
|
+
*
|
|
28
|
+
* Blix cannot capture ciphertext by itself. It has no knowledge of any app's
|
|
29
|
+
* encryption scheme, and — by design — its own interceptors sit on the
|
|
30
|
+
* *plaintext* side of the pipeline, so the encrypted form simply does not exist
|
|
31
|
+
* at the moment Blix captures. This is the hand-off: the host calls
|
|
32
|
+
* `captureEncrypted` from inside its own encrypt/decrypt interceptors, where
|
|
33
|
+
* the ciphertext does exist, and passes back the same axios config object Blix
|
|
34
|
+
* already stamped.
|
|
35
|
+
*
|
|
36
|
+
* Entirely optional. An app that never calls it behaves exactly as before, and
|
|
37
|
+
* the panel hides the Encrypted tab for every entry that has nothing in it.
|
|
38
|
+
*
|
|
39
|
+
* Like the rest of the capture layer, this must never throw, never warn, and
|
|
40
|
+
* never be reachable in a production build.
|
|
41
|
+
*/
|
|
42
|
+
/**
|
|
43
|
+
* What the host hands over. Both sides are optional and independent: the
|
|
44
|
+
* request-side ciphertext is produced early (on the way out) and the
|
|
45
|
+
* response-side late (on the way back), so they normally arrive in two separate
|
|
46
|
+
* calls.
|
|
47
|
+
*/
|
|
48
|
+
interface EncryptedPayload {
|
|
49
|
+
/** What actually went on the wire, in place of `requestPayload`. */
|
|
50
|
+
request?: unknown;
|
|
51
|
+
/** What actually came back, before the host decrypted it. */
|
|
52
|
+
response?: unknown;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Attaches the encrypted form of a request and/or response to the entry Blix
|
|
56
|
+
* already captured for `config`.
|
|
57
|
+
*
|
|
58
|
+
* ```ts
|
|
59
|
+
* apiClient.interceptors.request.use((config) => {
|
|
60
|
+
* const encrypted = encrypt(config.data);
|
|
61
|
+
* captureEncrypted(config, { request: encrypted });
|
|
62
|
+
* return { ...config, data: encrypted };
|
|
63
|
+
* });
|
|
64
|
+
* ```
|
|
65
|
+
*
|
|
66
|
+
* Correlation is by identity of the config object, never by URL or timing —
|
|
67
|
+
* two concurrent requests to the same endpoint stay correctly apart. Pass the
|
|
68
|
+
* object axios gave you; a `{ ...config }` copy made by your own interceptor
|
|
69
|
+
* also resolves (see `monitorStamp.ts`). Call it as many times as you like, in
|
|
70
|
+
* either order, before or after the response arrives; each call merges into the
|
|
71
|
+
* existing entry and refreshes the panel. It never creates an entry of its own.
|
|
72
|
+
*
|
|
73
|
+
* A silent no-op — no throw, no console output — in every failure mode:
|
|
74
|
+
* outside development, when `attachHttpMonitor` was never called, when the
|
|
75
|
+
* config carries no stamp (a retry that built a fresh config, say), and when
|
|
76
|
+
* the entry has already been evicted from the ring buffer.
|
|
77
|
+
*/
|
|
78
|
+
declare function captureEncrypted(config: unknown, payload: EncryptedPayload): void;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Network monitor — Redux action capture.
|
|
82
|
+
*
|
|
83
|
+
* A structurally-typed middleware, following the same contract-restatement
|
|
84
|
+
* pattern `realtimeCapture.ts` uses for the realtime adapter: this module
|
|
85
|
+
* never imports `@reduxjs/toolkit`, so the capture layer — which the host's
|
|
86
|
+
* HTTP-client module also depends on — stays free of a dependency on the
|
|
87
|
+
* host's state-management choice. That is what lets `@reduxjs/toolkit` be an
|
|
88
|
+
* optional peer rather than a hard one.
|
|
89
|
+
*
|
|
90
|
+
* Cost per dispatch, dev-only: a handful of reference compares, plus (only
|
|
91
|
+
* when the action actually changed something) a bounded structural diff — see
|
|
92
|
+
* `stateDiff.ts`. RTK's own `immutableCheck`/`serializableCheck` dev
|
|
93
|
+
* middleware already deep-walk the *entire* state on every dispatch; this tap
|
|
94
|
+
* does strictly less work than that, and is entirely absent in production —
|
|
95
|
+
* see the pass-through returned below when `MONITOR_ENABLED` is false.
|
|
96
|
+
*/
|
|
97
|
+
/** The subset of a Redux store/middleware API this tap needs, restated
|
|
98
|
+
* structurally so this file never imports `redux` or `@reduxjs/toolkit`. */
|
|
99
|
+
interface MiddlewareApiLike<S> {
|
|
100
|
+
getState(): S;
|
|
101
|
+
}
|
|
102
|
+
type NextLike = (action: unknown) => unknown;
|
|
103
|
+
type MiddlewareLike<S> = (api: MiddlewareApiLike<S>) => (next: NextLike) => (action: unknown) => unknown;
|
|
104
|
+
interface ReduxCaptureOptions {
|
|
105
|
+
/** Action types (exact) or `"prefix/*"` globs never captured. Merged with
|
|
106
|
+
* the built-in defaults (redux-toolkit's own init/replace actions). */
|
|
107
|
+
ignore?: readonly string[];
|
|
108
|
+
/** Consecutive dispatches of the same action type within this window fold
|
|
109
|
+
* into the existing row (`redux.batchCount`) instead of flooding the log. */
|
|
110
|
+
coalesceMs?: number;
|
|
111
|
+
/** Above this dispatch rate, capture drops to type + timing only — no
|
|
112
|
+
* diff — until the rate falls back under it. */
|
|
113
|
+
maxActionsPerSecond?: number;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Builds the Redux-capture middleware. Returns a pure pass-through when the
|
|
117
|
+
* monitor is disabled (production, or SSR), so the `.concat()` call at the
|
|
118
|
+
* host's store setup costs nothing at runtime outside development.
|
|
119
|
+
*/
|
|
120
|
+
declare function createReduxMonitorMiddleware<S>(options?: ReduxCaptureOptions): MiddlewareLike<S>;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Network monitor — TanStack Query cache capture.
|
|
124
|
+
*
|
|
125
|
+
* Structurally typed against the cache-event shape, exactly like
|
|
126
|
+
* `realtimeCapture.ts` restates the realtime adapter contract: this module
|
|
127
|
+
* never imports `@tanstack/react-query`, so the capture layer — which the
|
|
128
|
+
* host's HTTP-client module also depends on — stays free of a dependency on
|
|
129
|
+
* the host's data-fetching library. That is what lets `@tanstack/react-query`
|
|
130
|
+
* be an optional peer rather than a hard one.
|
|
131
|
+
*
|
|
132
|
+
* One entry per `queryHash` (or `mutationId`), accumulating lifecycle frames —
|
|
133
|
+
* the same "one long-lived row" model `realtimeCapture.ts` uses for a socket
|
|
134
|
+
* connection. Under an aggressive `gcTime` a query is added to and removed
|
|
135
|
+
* from the cache repeatedly; hashing by key means the *same row* accumulates
|
|
136
|
+
* that whole session's history instead of flickering in and out of the log —
|
|
137
|
+
* and turns that churn into the panel's most useful signal: a row can show
|
|
138
|
+
* "fetched 7 times this session, 5 of them cache misses".
|
|
139
|
+
*
|
|
140
|
+
* The Query section is backed entirely by this module's own capture, never by
|
|
141
|
+
* a live read of `queryCache.getAll()` — under `gcTime: 0` that read is empty
|
|
142
|
+
* almost all the time, which is the whole reason this capture exists.
|
|
143
|
+
*/
|
|
144
|
+
/** Structural subset of a TanStack Query `Query` this tap needs. */
|
|
145
|
+
interface QueryStateLike {
|
|
146
|
+
status: string;
|
|
147
|
+
fetchStatus?: string;
|
|
148
|
+
data?: unknown;
|
|
149
|
+
error?: unknown;
|
|
150
|
+
dataUpdatedAt?: number;
|
|
151
|
+
errorUpdatedAt?: number;
|
|
152
|
+
fetchFailureCount?: number;
|
|
153
|
+
isInvalidated?: boolean;
|
|
154
|
+
}
|
|
155
|
+
interface QueryLike {
|
|
156
|
+
queryHash: string;
|
|
157
|
+
queryKey: readonly unknown[];
|
|
158
|
+
state: QueryStateLike;
|
|
159
|
+
getObserversCount?(): number;
|
|
160
|
+
}
|
|
161
|
+
interface MutationStateLike {
|
|
162
|
+
status: string;
|
|
163
|
+
variables?: unknown;
|
|
164
|
+
data?: unknown;
|
|
165
|
+
error?: unknown;
|
|
166
|
+
}
|
|
167
|
+
interface MutationLike {
|
|
168
|
+
mutationId: number;
|
|
169
|
+
state: MutationStateLike;
|
|
170
|
+
}
|
|
171
|
+
interface CacheEventLike {
|
|
172
|
+
type: string;
|
|
173
|
+
query?: QueryLike;
|
|
174
|
+
mutation?: MutationLike;
|
|
175
|
+
action?: {
|
|
176
|
+
type: string;
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
interface CacheLike {
|
|
180
|
+
subscribe(listener: (event: CacheEventLike) => void): () => void;
|
|
181
|
+
getAll(): unknown[];
|
|
182
|
+
}
|
|
183
|
+
/** A TanStack `QueryFilters`-shaped filter — structurally typed, same as
|
|
184
|
+
* everything else in this module, so this file still never imports
|
|
185
|
+
* `@tanstack/react-query`. */
|
|
186
|
+
interface QueryFilterLike {
|
|
187
|
+
queryKey?: readonly unknown[];
|
|
188
|
+
exact?: boolean;
|
|
189
|
+
}
|
|
190
|
+
interface QueryClientLike {
|
|
191
|
+
getQueryCache(): CacheLike;
|
|
192
|
+
getMutationCache(): CacheLike;
|
|
193
|
+
invalidateQueries(filters?: QueryFilterLike): Promise<void>;
|
|
194
|
+
refetchQueries(filters?: QueryFilterLike): Promise<void>;
|
|
195
|
+
removeQueries(filters?: QueryFilterLike): void;
|
|
196
|
+
resetQueries(filters?: QueryFilterLike): Promise<void>;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Installs the tap on a `QueryClient`. Idempotent, and safe to call from a
|
|
200
|
+
* React effect in your query provider rather than the `useState` initializer
|
|
201
|
+
* that creates the client — React Strict Mode double-invokes initializers, and
|
|
202
|
+
* tapping there would tap a client that gets discarded.
|
|
203
|
+
*
|
|
204
|
+
* Because parent effects run *after* child effects, a child's `useQuery` has
|
|
205
|
+
* already fired by the time this installs — so it backfills from
|
|
206
|
+
* `getQueryCache().getAll()` first, rather than starting blind.
|
|
207
|
+
*/
|
|
208
|
+
declare function tapQueryClient(client: QueryClientLike): () => void;
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Network monitor — shared types.
|
|
212
|
+
*
|
|
213
|
+
* `MonitorEntry` is both the in-memory shape and (minus a few live-only fields)
|
|
214
|
+
* the persisted shape. Anything added here that should survive a reload must
|
|
215
|
+
* also be JSON-serializable.
|
|
216
|
+
*/
|
|
217
|
+
type MonitorState = "pending" | "success" | "error" | "aborted";
|
|
218
|
+
/**
|
|
219
|
+
* HTTP requests, realtime connections, Redux actions and TanStack Query
|
|
220
|
+
* activity all share one table.
|
|
221
|
+
*
|
|
222
|
+
* A WebSocket connection is modelled as a single long-lived entry that
|
|
223
|
+
* accumulates frames — the same shape Chrome uses, where the connection is a
|
|
224
|
+
* row and its traffic lives in a Messages tab. Modelling each frame as its own
|
|
225
|
+
* row would drown the HTTP requests. A Query cache entry reuses that exact
|
|
226
|
+
* model: one row per `queryHash`/`mutationId`, accumulating lifecycle frames
|
|
227
|
+
* (fetch → success/error → invalidate → removed) — because under an aggressive
|
|
228
|
+
* `gcTime` the same key is added to and removed from the cache repeatedly, and
|
|
229
|
+
* hashing by key lets one row carry that whole session's history instead of
|
|
230
|
+
* flickering in and out of the log.
|
|
231
|
+
*
|
|
232
|
+
* A Redux action, by contrast, gets its own row per dispatch: the detail pane,
|
|
233
|
+
* selection, pinning and search are all entry-keyed, and a per-action diff
|
|
234
|
+
* needs a first-class row rather than a frame buried inside one.
|
|
235
|
+
*/
|
|
236
|
+
type MonitorKind = "http" | "ws" | "redux" | "query";
|
|
237
|
+
type FrameDirection = "in" | "out" | "system";
|
|
238
|
+
interface WsFrame {
|
|
239
|
+
id: string;
|
|
240
|
+
at: number;
|
|
241
|
+
direction: FrameDirection;
|
|
242
|
+
/** Event/channel name where the transport provides one. */
|
|
243
|
+
event: string;
|
|
244
|
+
data: unknown;
|
|
245
|
+
/** Approximate size, estimated once at capture. */
|
|
246
|
+
sizeBytes: number;
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Where a request's wall-clock time actually went.
|
|
250
|
+
*
|
|
251
|
+
* Worth breaking out when the host transforms bodies in its interceptors —
|
|
252
|
+
* encryption, compression, heavy serialization — because "slow request" then
|
|
253
|
+
* means a slow backend *or* a slow client-side pipeline, and the two need
|
|
254
|
+
* telling apart.
|
|
255
|
+
*/
|
|
256
|
+
interface TimingMarks {
|
|
257
|
+
encryptMs?: number;
|
|
258
|
+
networkMs?: number;
|
|
259
|
+
decryptMs?: number;
|
|
260
|
+
}
|
|
261
|
+
/** One frame of a captured call stack, already trimmed and made repo-relative. */
|
|
262
|
+
interface InitiatorFrame {
|
|
263
|
+
/** Function or component name, when V8 gave us one. */
|
|
264
|
+
fn?: string;
|
|
265
|
+
/** Repo-relative path, e.g. `src/hooks/useCheckoutForm.ts`. */
|
|
266
|
+
file: string;
|
|
267
|
+
line?: number;
|
|
268
|
+
col?: number;
|
|
269
|
+
}
|
|
270
|
+
type DiffOp = "add" | "change" | "remove";
|
|
271
|
+
/** One changed path from a bounded structural diff — see `stateDiff.ts`. */
|
|
272
|
+
interface StateDiffEntry {
|
|
273
|
+
/** Dotted/bracketed path, e.g. `conversations.allConversations[3].unreadCount`. */
|
|
274
|
+
path: string;
|
|
275
|
+
op: DiffOp;
|
|
276
|
+
/** Bounded, truncated *copies* — never a reference into live state. */
|
|
277
|
+
before?: unknown;
|
|
278
|
+
after?: unknown;
|
|
279
|
+
}
|
|
280
|
+
interface StateDiff {
|
|
281
|
+
changes: StateDiffEntry[];
|
|
282
|
+
/** Hit a depth/node/change ceiling — the diff is a partial picture. */
|
|
283
|
+
truncated: boolean;
|
|
284
|
+
/** Top-level slices touched by this action. */
|
|
285
|
+
slices: string[];
|
|
286
|
+
}
|
|
287
|
+
/** Redux only: the dispatched action and what it changed. */
|
|
288
|
+
interface ReduxActionMeta {
|
|
289
|
+
/** `action.type`, mirrored into `entry.url` so free-text search works free. */
|
|
290
|
+
type: string;
|
|
291
|
+
/** Bounded copy of `action.payload` + `action.meta`. */
|
|
292
|
+
payload?: unknown;
|
|
293
|
+
/** RTK rejected-thunk marker (`action.error === true`). */
|
|
294
|
+
isError?: boolean;
|
|
295
|
+
diff?: StateDiff;
|
|
296
|
+
/** Wall time spent inside `next(action)`. */
|
|
297
|
+
reducerMs: number;
|
|
298
|
+
/** >1 when identical consecutive actions were coalesced into this row. */
|
|
299
|
+
batchCount?: number;
|
|
300
|
+
/** False when the captured payload was truncated — re-dispatching it would
|
|
301
|
+
* not faithfully reproduce the original action, so the panel's Re-dispatch
|
|
302
|
+
* action is disabled. */
|
|
303
|
+
replayable?: boolean;
|
|
304
|
+
}
|
|
305
|
+
/** Query/mutation only: the cache row's current lifecycle state. Deliberately
|
|
306
|
+
* not folded into `MonitorState` — `MonitorState` drives the shared state
|
|
307
|
+
* filter and waterfall colour, and pending/success/error covers this fine;
|
|
308
|
+
* the richer status (fresh/stale/fetching/removed) lives here instead. */
|
|
309
|
+
interface QueryMeta {
|
|
310
|
+
sub: "query" | "mutation";
|
|
311
|
+
/** `query.queryHash` / `String(mutation.mutationId)` — the entry's stable
|
|
312
|
+
* identity, and the correlation handle from an HTTP entry's `ownerId`. */
|
|
313
|
+
hash: string;
|
|
314
|
+
key: unknown[];
|
|
315
|
+
status: "pending" | "success" | "error";
|
|
316
|
+
fetchStatus?: "fetching" | "paused" | "idle";
|
|
317
|
+
observers: number;
|
|
318
|
+
isInvalidated?: boolean;
|
|
319
|
+
dataUpdatedAt?: number;
|
|
320
|
+
errorUpdatedAt?: number;
|
|
321
|
+
failureCount?: number;
|
|
322
|
+
/** The query left the cache — routine under an aggressive `gcTime`, and
|
|
323
|
+
* exactly the fact this panel exists to make visible. */
|
|
324
|
+
gcRemoved?: boolean;
|
|
325
|
+
/** How many times this key has been fetched this session. */
|
|
326
|
+
fetchCount?: number;
|
|
327
|
+
/** Ids of the HTTP entries this query/mutation caused — best-effort, see
|
|
328
|
+
* `monitorContext.ts`. */
|
|
329
|
+
causedIds?: string[];
|
|
330
|
+
}
|
|
331
|
+
interface MonitorEntry {
|
|
332
|
+
id: string;
|
|
333
|
+
/** HTTP request or realtime connection. Defaults to `"http"` when absent, so
|
|
334
|
+
* entries persisted before realtime capture existed still load. */
|
|
335
|
+
kind?: MonitorKind;
|
|
336
|
+
/** Monotonic capture order. `id` embeds `Date.now()` and is unique but not
|
|
337
|
+
* order-comparable; `seq` is what sorting and oldest-first eviction use. */
|
|
338
|
+
seq: number;
|
|
339
|
+
/** Bumped on every `update()`. Lets the deep-search index cache tell whether
|
|
340
|
+
* an entry actually changed without stringifying it. */
|
|
341
|
+
rev: number;
|
|
342
|
+
method: string;
|
|
343
|
+
url: string;
|
|
344
|
+
baseURL?: string;
|
|
345
|
+
/** Wall-clock epoch ms when the request started (for display). */
|
|
346
|
+
at: number;
|
|
347
|
+
/** `performance.now()` at start, used to compute duration. */
|
|
348
|
+
startTime: number;
|
|
349
|
+
endTime?: number;
|
|
350
|
+
durationMs?: number;
|
|
351
|
+
/** `performance.timeOrigin` of the page load that captured this entry. */
|
|
352
|
+
timeOrigin: number;
|
|
353
|
+
/**
|
|
354
|
+
* Start/end as absolute epoch ms (`timeOrigin + startTime`).
|
|
355
|
+
*
|
|
356
|
+
* The waterfall must use these, never `startTime`/`endTime`: those are
|
|
357
|
+
* relative to a per-page-load origin, so a persisted entry's `startTime` is
|
|
358
|
+
* meaningless against the current load's.
|
|
359
|
+
*/
|
|
360
|
+
startAbs: number;
|
|
361
|
+
endAbs?: number;
|
|
362
|
+
/** Which page load captured this. Drives the divider rows and the per-load
|
|
363
|
+
* waterfall timeline. */
|
|
364
|
+
loadId: string;
|
|
365
|
+
status?: number;
|
|
366
|
+
state: MonitorState;
|
|
367
|
+
/** Plaintext request body, before encryption. */
|
|
368
|
+
requestPayload?: unknown;
|
|
369
|
+
/** What is actually sent over the wire (encData / aesKey). */
|
|
370
|
+
encryptedRequest?: unknown;
|
|
371
|
+
/** Decrypted response body. */
|
|
372
|
+
responsePayload?: unknown;
|
|
373
|
+
/** Raw encrypted response body from the server. */
|
|
374
|
+
encryptedResponse?: unknown;
|
|
375
|
+
/** Decrypted/raw error payload, when the request failed. */
|
|
376
|
+
error?: unknown;
|
|
377
|
+
/** Outgoing request headers (sanitized to a flat string map). */
|
|
378
|
+
requestHeaders?: Record<string, string>;
|
|
379
|
+
/** Response headers returned by the server. */
|
|
380
|
+
responseHeaders?: Record<string, string>;
|
|
381
|
+
/** Approximate payload size in bytes, estimated once at capture. Computing
|
|
382
|
+
* this lazily in the detail pane meant a full `JSON.stringify` per render. */
|
|
383
|
+
sizeBytes?: number;
|
|
384
|
+
/** Whether the request opted out of the encryption pipeline. Needed to replay
|
|
385
|
+
* a request faithfully — it's a config flag, not a header, so it isn't
|
|
386
|
+
* recoverable from `requestHeaders`. */
|
|
387
|
+
skipEncryption?: boolean;
|
|
388
|
+
/** Whether the body was `FormData`. Such requests can't be replayed: file
|
|
389
|
+
* contents are summarized, not captured. */
|
|
390
|
+
hadFormData?: boolean;
|
|
391
|
+
/** Set when this entry is a replay of another; holds the parent's id. */
|
|
392
|
+
replayOf?: string;
|
|
393
|
+
/** How many replays this entry has spawned. */
|
|
394
|
+
replayCount?: number;
|
|
395
|
+
/** Trimmed call stack captured at the call site. */
|
|
396
|
+
initiator?: InitiatorFrame[];
|
|
397
|
+
/** Where the time went — encryption vs. network vs. decryption. */
|
|
398
|
+
marks?: TimingMarks;
|
|
399
|
+
/**
|
|
400
|
+
* Pinned entries survive Clear and buffer eviction, so a reference response
|
|
401
|
+
* can be held on screen while reproducing an issue.
|
|
402
|
+
*/
|
|
403
|
+
pinned?: boolean;
|
|
404
|
+
/** Realtime/Query only: frames/lifecycle events on this connection or key. */
|
|
405
|
+
frames?: WsFrame[];
|
|
406
|
+
/** Realtime only: transport name, shown in the Method column. */
|
|
407
|
+
transport?: string;
|
|
408
|
+
/** Redux only: the dispatched action and its bounded diff. */
|
|
409
|
+
redux?: ReduxActionMeta;
|
|
410
|
+
/** Query only: the cache row's key, status and observer count. */
|
|
411
|
+
query?: QueryMeta;
|
|
412
|
+
/** HTTP only: id of the query/mutation entry that caused this request,
|
|
413
|
+
* when it could be determined — see `monitorContext.ts`. Absence means
|
|
414
|
+
* "unknown", never "not caused by a query". */
|
|
415
|
+
ownerId?: string;
|
|
416
|
+
/** HTTP only: whether the raw call stack (before NOISE-filtering) passed
|
|
417
|
+
* through `@tanstack/query`, independent of whether `ownerId` resolved. */
|
|
418
|
+
initiatorKind?: "query" | "mutation" | "direct";
|
|
419
|
+
}
|
|
420
|
+
/** Shape written to IndexedDB. Payloads may be truncated relative to the live
|
|
421
|
+
* entry; `bytes` is what the storage budget accounts against. */
|
|
422
|
+
interface PersistedEntry extends MonitorEntry {
|
|
423
|
+
schema: number;
|
|
424
|
+
bytes: number;
|
|
425
|
+
}
|
|
426
|
+
type DockMode = "bottom" | "right" | "float";
|
|
427
|
+
type Corner = "bottom-left" | "bottom-right" | "top-left" | "top-right";
|
|
428
|
+
interface Size {
|
|
429
|
+
w: number;
|
|
430
|
+
h: number;
|
|
431
|
+
}
|
|
432
|
+
interface Pos {
|
|
433
|
+
x: number;
|
|
434
|
+
y: number;
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Everything the panel remembers between sessions.
|
|
438
|
+
*
|
|
439
|
+
* Stored in IndexedDB (`meta.prefs`) as the source of truth, mirrored into
|
|
440
|
+
* `localStorage["nm:prefs"]` purely as a synchronous read cache so the panel
|
|
441
|
+
* can restore its geometry without a frame of flash. The mirror is expendable —
|
|
442
|
+
* any host that clears web storage wipes it, and the IndexedDB copy silently
|
|
443
|
+
* restores it on next boot.
|
|
444
|
+
*/
|
|
445
|
+
interface MonitorPrefs {
|
|
446
|
+
/** Bumped on every write; used to reconcile the two copies. */
|
|
447
|
+
rev: number;
|
|
448
|
+
mode: DockMode;
|
|
449
|
+
/** Dock sizes, per axis. Sharing one value across docks yields absurd
|
|
450
|
+
* geometry when switching. */
|
|
451
|
+
bottomH: number;
|
|
452
|
+
rightW: number;
|
|
453
|
+
float: {
|
|
454
|
+
pos: Pos | null;
|
|
455
|
+
size: Size;
|
|
456
|
+
maximized: boolean;
|
|
457
|
+
};
|
|
458
|
+
/** List/detail splitter, one per split axis. */
|
|
459
|
+
splitW: number;
|
|
460
|
+
splitH: number;
|
|
461
|
+
corner: Corner;
|
|
462
|
+
preserveLog: boolean;
|
|
463
|
+
deepSearch: boolean;
|
|
464
|
+
followLatest: boolean;
|
|
465
|
+
/** Opt out of call-stack capture when profiling a large burst of requests. */
|
|
466
|
+
captureInitiator: boolean;
|
|
467
|
+
/** Last pinned request; restored only if the id still resolves. */
|
|
468
|
+
selectedId: string | null;
|
|
469
|
+
/** Active top-level section — `"network"` (HTTP) or `"realtime"` (sockets). */
|
|
470
|
+
section: string;
|
|
471
|
+
/** Row height preset: compact | normal | comfy. */
|
|
472
|
+
density: string;
|
|
473
|
+
/** User-resized column widths, in px, keyed by column id. */
|
|
474
|
+
columnWidths: Record<string, number>;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Network monitor — realtime (WebSocket / ActionCable) capture.
|
|
479
|
+
*
|
|
480
|
+
* Realtime traffic routed through an adapter is typically invisible: the
|
|
481
|
+
* browser's own Network tab shows the socket but not the decoded envelope, and
|
|
482
|
+
* an app's own logging goes to the console where it is drowned by everything
|
|
483
|
+
* else.
|
|
484
|
+
*
|
|
485
|
+
* ## Shape of the capture
|
|
486
|
+
*
|
|
487
|
+
* A connection is one long-lived `MonitorEntry` (`kind: "ws"`) that accumulates
|
|
488
|
+
* frames, rather than one entry per frame. That is how Chrome models it — the
|
|
489
|
+
* connection is a row, its traffic lives in a Messages tab — and it stops a
|
|
490
|
+
* chatty channel from burying every HTTP request in the table.
|
|
491
|
+
*
|
|
492
|
+
* ## Why a structural adapter type
|
|
493
|
+
*
|
|
494
|
+
* The host's adapter interface lives in the host, and this package must not
|
|
495
|
+
* import from it. The adapter shape is therefore restated structurally here,
|
|
496
|
+
* so the tap stays a one-line change at the call site with no dependency
|
|
497
|
+
* pointing the wrong way — any object with the right methods satisfies it.
|
|
498
|
+
*/
|
|
499
|
+
|
|
500
|
+
/** The subset of the realtime adapter contract this tap needs. */
|
|
501
|
+
interface RealtimeAdapterLike {
|
|
502
|
+
connect(token: string, url: string): void;
|
|
503
|
+
disconnect(): void;
|
|
504
|
+
subscribe(channelName: string): void;
|
|
505
|
+
onMessage(callback: (payload: {
|
|
506
|
+
event: string;
|
|
507
|
+
data: unknown;
|
|
508
|
+
}) => void): void;
|
|
509
|
+
onPresenceUpdate(callback: (status: string) => void): void;
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* Wraps a realtime adapter so its lifecycle and traffic appear in the monitor.
|
|
513
|
+
*
|
|
514
|
+
* Returns the adapter untouched in production, where `MONITOR_ENABLED` is a
|
|
515
|
+
* constant `false` and the whole wrapper folds away.
|
|
516
|
+
*/
|
|
517
|
+
declare function tapRealtimeAdapter<T extends RealtimeAdapterLike>(adapter: T, transport: string): T;
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Network monitor — "who fired this request?"
|
|
521
|
+
*
|
|
522
|
+
* ## Why the stack is taken at the call site, not in the interceptor
|
|
523
|
+
*
|
|
524
|
+
* By the time the axios request interceptor runs, we are already inside axios's
|
|
525
|
+
* promise chain: the interceptor callback executes in a microtask, so
|
|
526
|
+
* `new Error().stack` taken there is `at Object.onFulfilled (axios)` plus a
|
|
527
|
+
* microtask boundary. V8's async stack traces sometimes recover the real
|
|
528
|
+
* caller, but not reliably, and never through React Query's internals.
|
|
529
|
+
*
|
|
530
|
+
* So `withInitiatorCapture` wraps the axios instance, takes the stack
|
|
531
|
+
* synchronously in the caller's own frame, and stashes it on the config for
|
|
532
|
+
* `beginMonitor` to read.
|
|
533
|
+
*
|
|
534
|
+
* The whole module is behind a statically-foldable `NODE_ENV` check at its call
|
|
535
|
+
* site, so it is dead-code-eliminated from production builds.
|
|
536
|
+
*/
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Wraps the axios instance so every call site records its own stack.
|
|
540
|
+
*
|
|
541
|
+
* Wraps the callable form (`apiClient(config)`), plus `request`, `get`, `post`,
|
|
542
|
+
* `put`, `patch`, `delete` and `head`. Other entry points (`options`, the
|
|
543
|
+
* `*Form` helpers) pass through unwrapped — requests made through them are
|
|
544
|
+
* still captured, they just carry no initiator stack.
|
|
545
|
+
*/
|
|
546
|
+
declare function withInitiatorCapture(instance: AxiosInstance): AxiosInstance;
|
|
547
|
+
|
|
548
|
+
export { type CacheLike, type Corner, type DiffOp, type DockMode, type EncryptedPayload, type FrameDirection, type InitiatorFrame, type MiddlewareApiLike, type MiddlewareLike, type MonitorEntry, type MonitorKind, type MonitorPrefs, type MonitorState, type PersistedEntry, type Pos, type QueryClientLike, type QueryMeta, type RealtimeAdapterLike, type ReduxActionMeta, type ReduxCaptureOptions, type Size, type StateDiff, type StateDiffEntry, type TimingMarks, type WsFrame, attachHttpMonitor, captureEncrypted, createReduxMonitorMiddleware, tapQueryClient, tapRealtimeAdapter, withInitiatorCapture };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import {
|
|
2
|
+
attachHttpMonitor,
|
|
3
|
+
captureEncrypted,
|
|
4
|
+
tapRealtimeAdapter
|
|
5
|
+
} from "../chunk-OQHKPRNI.js";
|
|
6
|
+
import {
|
|
7
|
+
createReduxMonitorMiddleware,
|
|
8
|
+
tapQueryClient,
|
|
9
|
+
withInitiatorCapture
|
|
10
|
+
} from "../chunk-MRTSIXR7.js";
|
|
11
|
+
export {
|
|
12
|
+
attachHttpMonitor,
|
|
13
|
+
captureEncrypted,
|
|
14
|
+
createReduxMonitorMiddleware,
|
|
15
|
+
tapQueryClient,
|
|
16
|
+
tapRealtimeAdapter,
|
|
17
|
+
withInitiatorCapture
|
|
18
|
+
};
|