@zerotal/devtools 1.6.3 → 1.7.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.
Files changed (45) hide show
  1. package/CHANGELOG.md +329 -0
  2. package/api-surface.md +298 -0
  3. package/package.json +5 -4
  4. package/src/DevtoolsInjectionMiddleware.ts +41 -4
  5. package/src/RequestTrace.ts +106 -1
  6. package/src/TraceStore.ts +12 -0
  7. package/src/activity.ts +116 -0
  8. package/src/callsite.ts +146 -0
  9. package/src/client/filter.ts +108 -0
  10. package/src/client/index.ts +127 -0
  11. package/src/client/metrics.ts +98 -0
  12. package/src/client/registry.ts +87 -0
  13. package/src/client/state.ts +350 -0
  14. package/src/client/tabs/all.ts +323 -0
  15. package/src/client/tabs/app.ts +293 -0
  16. package/src/client/tabs/cache.ts +50 -0
  17. package/src/client/tabs/channel.ts +264 -0
  18. package/src/client/tabs/exceptions.ts +69 -0
  19. package/src/client/tabs/jobs.ts +51 -0
  20. package/src/client/tabs/live.ts +66 -0
  21. package/src/client/tabs/logs.ts +45 -0
  22. package/src/client/tabs/mail.ts +60 -0
  23. package/src/client/tabs/queries.ts +125 -0
  24. package/src/client/tabs/request.ts +75 -0
  25. package/src/client/tabs/sections.ts +115 -0
  26. package/src/client/tabs/timeline.ts +133 -0
  27. package/src/client/tabs/types.ts +68 -0
  28. package/src/client/transport.ts +81 -0
  29. package/src/client/tree.ts +138 -0
  30. package/src/client/ui/format.ts +137 -0
  31. package/src/client/ui/render.ts +87 -0
  32. package/src/client/ui/shell.ts +560 -0
  33. package/src/client/ui/theme.ts +445 -0
  34. package/src/client-auto.ts +1 -1
  35. package/src/config.ts +77 -2
  36. package/src/dashboard-auto.ts +1 -1
  37. package/src/editor.ts +107 -0
  38. package/src/enabled.ts +59 -0
  39. package/src/index.ts +19 -3
  40. package/src/map.ts +213 -0
  41. package/src/provider/DevtoolsProvider.ts +32 -7
  42. package/src/redaction.ts +161 -20
  43. package/src/tracing.ts +261 -29
  44. package/src/client.ts +0 -1048
  45. package/src/panel-app.js +0 -519
package/CHANGELOG.md CHANGED
@@ -8,6 +8,335 @@ follows the Zerotal monorepo's unified versioning.
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [1.7.1] — 2026-08-16
12
+
13
+ ### Changed
14
+
15
+ - **A request is a place, not twelve tabs.** The Requests strip carried fourteen tabs, twelve
16
+ of which described a single request — Queries, Timeline, Logs, Request, Exception, Mail,
17
+ Cache, Jobs, and every channel. They were empty for most requests, they answered a question
18
+ you can only ask about a request you have already picked, and under about 1100px they
19
+ scrolled off the right edge with nothing to say they had.
20
+
21
+ Every view now declares a `scope`. A request-scoped one is drawn inside whichever request
22
+ you are reading, as that request's own small strip of tabs; a session-scoped one keeps its
23
+ place in the panel's strip, because it keeps reading as you move between requests. Nothing
24
+ is reimplemented — a `TabView` was already "draw this trace into this element", which is
25
+ exactly what a section is.
26
+
27
+ Only the views with something to say appear, so the strip doubles as the summary: a request
28
+ showing Queries, Logs and Exception has told you what happened before you click anything. A
29
+ view that counts nothing is skipped before it renders, and anything that comes back as its
30
+ own empty-state line is dropped after.
31
+
32
+ Requests is now **Live**, **All** and whatever plugins are installed. App is untouched —
33
+ Routes, Config, Container, Providers, Events and Commands describe the app rather than a
34
+ request, which is exactly why they are tabs.
35
+
36
+ - **A Live view, first and default.** The newest request, already open. Opening the panel now
37
+ shows the page you are looking at instead of a heading you navigate away from, and it
38
+ follows: click something and it is showing what just happened. It ignores pinning on
39
+ purpose — pinning is how you hold still and read something older.
40
+
41
+ - **A request states its identity once.** The Request view opened with the method, path and
42
+ status — right for a tab that had to say which request it was describing, pure repetition
43
+ for a section sitting directly under the row that just said it, where it read as a second
44
+ heading competing with the real one.
45
+
46
+ - **Clear sits with the list it clears.** A labelled button beside the filter, rather than
47
+ only the icon at the far end of the status bar.
48
+
49
+ ### Fixed
50
+
51
+ - **Durations printed their whole float.** `fmt()` interpolated the number raw, so anything
52
+ measured with `performance.now()` — every Flow action, in the always-visible status bar —
53
+ read `3.6370999999926426ms`, while anything a caller had already rounded read `0ms` for a
54
+ query that plainly took time. Precision now follows magnitude: `0.42ms`, `3.6ms`, `143ms`,
55
+ `1.4s`. Numeric cells also use tabular figures, which is what made the request list's
56
+ right-aligned column ragged.
57
+
58
+ - **Timeline legend swatches floated away from their labels.** The legend reuses `.tmark` for
59
+ its colour, and `.tmark` is absolutely positioned for the waterfall — so in the legend,
60
+ whose rows are not positioned, all seven squares escaped to the nearest positioned ancestor
61
+ and stacked above their own text.
62
+
63
+ - **The panel covered the bottom of the page.** It is fixed to the viewport and reserved no
64
+ space, so the last strip of any page — 32px collapsed, the panel's full height open — could
65
+ not be scrolled to. The host page now gets matching bottom padding, updated on toggle and
66
+ resize, and `--zt-dt-height` is published on `<html>` for an app that would rather move
67
+ something of its own.
68
+
69
+ - **Muted text failed WCAG AA, and focus was invisible.** `--muted` sat at 2.35:1 on the tab
70
+ strip — below AA for text of any size, and this is 10–11px text. It now measures 4.69:1 on
71
+ the strip and 5.50:1 on the status bar in dark, 5.38:1 in light. `:focus-visible` was
72
+ suppressed panel-wide, leaving keyboard navigation with no indicator at all; focusable
73
+ controls now draw a ring.
74
+
75
+ ### Added
76
+
77
+ - **A contributed panel can read the selected trace.** `DevtoolsPanelPlugin.render` now
78
+ receives a second `context` argument carrying the trace selected in the request list. A
79
+ plugin exists because it owns live browser state, but the same events usually have a
80
+ server half recorded against a trace, and a plugin that could not reach it had to either
81
+ measure again client-side or show half the story in a tab of its own. `@zerotal/flow`'s
82
+ time-travel frames now print what each action cost on the server. The argument is
83
+ optional, so a plugin written against the one-argument form is untouched.
84
+
85
+ - **`hidden` on a channel descriptor.** Records the entries on the trace but gives them no
86
+ tab, for a package that renders the data somewhere better than a generic row list. Flow
87
+ declares its actions this way now that its own panel prints them on the frame they belong
88
+ to.
89
+
90
+ - **`TraceSink.finalise` — a trace for work that never was an HTTP request.** The sink let
91
+ a package buffer against a context but gave it no way to say that context was finished,
92
+ and a trace was only ever built from core's `RequestHandled` / `RequestFailed`. Anything
93
+ running against its own context outside the HTTP lifecycle therefore buffered its
94
+ evidence and dropped it: a Flow action over the WebSocket, and by the same mechanism a
95
+ queue job or a scheduled task.
96
+
97
+ `finalise(ctx, { startMs, durationMs, method })` builds the trace and pushes it. `method`
98
+ labels a synthetic request in the list — `@zerotal/flow` passes `FLOW`, which gets its own
99
+ colour so an action does not read as a second `GET` of the page it ran on. Finalising is
100
+ once per context, whichever claims it first, so a context that finalises itself can never
101
+ push a duplicate carrying none of the evidence.
102
+
103
+ Found by wiring DevTools into this repo's own `apps/docs`: the Flow tab could only ever
104
+ report "No flow activity during this request".
105
+
106
+ ## [1.7.0] — 2026-08-16
107
+
108
+ ### Added
109
+
110
+ - **An App section: the framework as it _is_, not only as it just behaved.** Every surface
111
+ until now read the trace stream — what one request did. The framework's own registries
112
+ were CLI-only or invisible, so "is that route even registered", "who bound `cache`",
113
+ "which provider is costing 200ms of boot" and "does anything actually listen to
114
+ `OrderPlaced`" were all answered by reading source.
115
+
116
+ Six tabs behind a **Requests | App** switch in the tab strip — two sections rather than
117
+ fifteen tabs in one scrolling strip, because they answer different questions:
118
+
119
+ - **Routes** — method, path, name, handler, middleware. GETs are clickable.
120
+ - **Config** — the resolved tree, flattened to dotted paths, with secrets masked.
121
+ - **Container** — every binding, its kind, and **which provider bound it**.
122
+ - **Providers** — boot order and per-provider cost.
123
+ - **Events** — application listeners and framework subscribers in one list.
124
+ - **Commands** — console commands and scheduled tasks. A task that fails at 03:00 used
125
+ to leave no trace in the tool whose job is to show you what your app did.
126
+
127
+ One read of one map shared by all six, since six requests for it would be six answers
128
+ that can disagree. Behind the same gate as everything else.
129
+
130
+ - **Every location in the panel is a link into your editor.** A repo-wide search for any
131
+ editor URL scheme used to return nothing: no stack frame, query, log line, or prop in any
132
+ Zerotal surface was clickable to source. Going from "this query is slow" to the line that
133
+ ran it is the most frequent move in a debugging session, and it was two manual searches.
134
+
135
+ `editor` in `config/devtools.ts` takes `vscode`, `vscode-insiders`, `cursor`, `windsurf`,
136
+ `zed`, or `webstorm`; `editorPathMap` rewrites paths for editing on a machine that is not
137
+ the one running the code.
138
+
139
+ - **Queries and log lines know where they came from.** One stack walk per recorded event,
140
+ filtered to application frames — the first frame that is _not_ framework code is the
141
+ answer, and every frame above it is noise.
142
+
143
+ This was the plan's one item of genuinely unknown viability, so it was measured before it
144
+ was built: **~2µs per capture, flat from stack depth 5 to 80**, because the engine builds
145
+ the trace lazily. A request running forty queries pays about 0.08ms. On by default;
146
+ `captureSource: false` turns it off.
147
+
148
+ - **An Exception tab.** Type, message, and the full stack with every frame a link. Framework
149
+ frames are kept and dimmed rather than dropped — you read a stack trace to find out how you
150
+ got somewhere, and a trace with the middle removed does not tell you that.
151
+
152
+ - **Three more tabs, from events already on the bus and going nowhere.** **Models**
153
+ (`ModelChanged`, grouped per model — a request that wrote four rows and one that wrote none
154
+ looked identical), **Transactions** (`TransactionCommitted` / `RolledBack` — a rollback
155
+ showed only as queries that appeared to succeed), and **Outgoing** (`OutgoingRequestCompleted`).
156
+
157
+ - **The Request tab shows the whole exchange.** Response headers and the status line
158
+ alongside the request, plus **session key names** — "is the CSRF token there, did the flash
159
+ survive the redirect" are answered by the keys, and the values are the request's real state
160
+ on a trace kept for a day. `headers: ["x-tenant"]` (or `["*"]`) opens up the request headers
161
+ the built-in allowlist withholds; `cookie` and `authorization` are never recorded whatever
162
+ you ask for.
163
+
164
+ - **What the browser measured, on the Timeline.** The panel reported server duration as
165
+ though it were the user's experience. A 12ms response the browser spends 900ms painting is
166
+ a slow page, and nothing in the trace said so — so TTFB, parse, load, and first paint now
167
+ sit above the waterfall, labelled as the page's rather than this request's.
168
+
169
+ ### Security
170
+
171
+ - **The Config tab masks a bare `key`, which the shared rule does not.** `app.key` is the
172
+ application's encryption key; the package-wide list covers `api_key` and `private_key` but
173
+ not `key` alone, which is right for a query binding — a column called `key` is usually a
174
+ lookup key — and wrong for config, where secrets are _supposed_ to live. Config gets the
175
+ stricter rule, `dsn` with it, on top of whatever the app's own `allow`/`deny` say.
176
+
177
+ - **An access gate, so running the inspector outside development is a supported thing to do
178
+ rather than a lie about `APP_ENV`.** It was all-or-nothing on the dev-surface check, which
179
+ is the right default and the only option — and Phase 4 widens what it exposes considerably.
180
+
181
+ ```ts
182
+ export default DevtoolsConfig({
183
+ enabled: null, // null → follow the dev-surface gate (unchanged default)
184
+ gate: async (req) => …, // required anywhere else; absent is a refusal
185
+ });
186
+ ```
187
+
188
+ A development process always passes — a gate that can lock a developer out of their own
189
+ machine gets switched off, and then nothing is gated. Anywhere else the absence of a gate
190
+ is a **refusal**, a throwing gate is a refusal, and one gate answers for every endpoint:
191
+ the stream, the trace JSON, the dashboard, and the panel bundle are the same secret.
192
+ Unauthorised requests get 404, not 403 — outside development the honest answer to a
193
+ stranger is that there is nothing here.
194
+
195
+ Auto-injection of the panel script is now dev-only. On a gated environment it would go into
196
+ every visitor's HTML and then 404 in their console; there the way in is the dashboard.
197
+
198
+ - **The panel remembers, follows, and gets out of the way.** Five things it could not do:
199
+
200
+ - **Resize.** The height was 380px, which is either too little to read a stack trace in or
201
+ too much to see the page under. Drag the strip above the tab row; the height is kept.
202
+ - **Light theme.** Follows `prefers-color-scheme` by default, with a toggle in the bar
203
+ cycling auto → dark → light for when the panel and the page you are debugging disagree.
204
+ - **Facets.** Method chips (only the verbs actually recorded), status-class chips, and
205
+ `errors` / `slow` / `n+1` toggles, composing with the text box rather than replacing it.
206
+ `POST` plus `5xx` means failing writes, not writes-or-failures.
207
+ - **Keyboard navigation.** `j`/`k` through the filtered list, `1`–`9` for tabs, `/` to
208
+ focus the filter, `Esc` to close. Only while the panel has focus — it is an overlay on
209
+ somebody's application, and binding `j` globally would navigate the trace list every time
210
+ a developer typed into their own form.
211
+ - **Copy buttons** on every SQL statement (and the whole statement list), log line, header
212
+ block, channel entry, and prop path.
213
+
214
+ - **New traces are offered, not forced.** While pinned, the bar shows `⤒ N new` instead of
215
+ silently accumulating. A list that scrolls away from what you were reading is the one
216
+ thing a request inspector must not do.
217
+
218
+ ### Changed
219
+
220
+ - **The browser client is a directory, not a 1,400-line closure.** `client.ts` held its
221
+ state, its transport, its styles, eight renderers, and every helper in one function scope
222
+ — which made adding a tab an edit to the middle of it, and made none of its logic reachable
223
+ from a test. It is now `src/client/`: a store, a transport, a shell, and one file per tab.
224
+ Adding a tab is adding a file.
225
+
226
+ No behaviour was dropped. Both mount modes still run one set of renderers, both extension
227
+ doors are unchanged, and `@zerotal/devtools/client` still resolves — the subpath now points
228
+ at `src/client/index.ts`.
229
+
230
+ - **Rows are reconciled rather than rebuilt.** Every arriving request used to replace the
231
+ whole content pane, which threw away the scroll position, every open `<details>`, every
232
+ loaded mail-preview iframe, and the caret in the filter box — the last of which the old
233
+ panel worked around by re-focusing and re-selecting the field after each keystroke. A keyed
234
+ list diff (about sixty lines, no dependency) now inserts one node and touches nothing else,
235
+ and tabs that read a single trace are not redrawn at all until that trace changes.
236
+
237
+ - **The request list is windowed above 200 rows**, so `DevtoolsConfig({ capacity: 5000 })` is
238
+ a list you can scroll rather than five thousand nodes.
239
+
240
+ ### Fixed
241
+
242
+ - **A negative render window.** Whenever the request list shrank under a scrolled viewport —
243
+ which is every "clear" — the windowing arithmetic asked for a slice starting past the end
244
+ of the list. Found by the test that could finally be written for it.
245
+
246
+ - **An Inertia tab, with the prop tree.** `@zerotal/inertia` has always resolved the richest
247
+ data in the framework — per-prop metadata, request classification, batch correlation — and
248
+ shipped no UI for it, while this panel shipped the UI and knew nothing about Inertia. A
249
+ developer got a panel that could not show a prop and an extension that could not show a
250
+ query, and neither could answer "this page is slow — is it the query or the deferred prop?"
251
+
252
+ Inertia now contributes a channel, and one row shows the component, its props, **and** the
253
+ SQL that produced them. Nothing matches a key: the entry is recorded against the same
254
+ request context as the queries, so the two cannot disagree about which request they
255
+ describe.
256
+
257
+ - **Channels choose how they are drawn.** A flat list of rows is right for an audit feed and
258
+ wrong for a prop map or a route table, and a package that needs a tree should not have to
259
+ ship a renderer into devtools to get one. `TraceChannelDescriptor` gains `render` —
260
+ `"rows"` (the default, unchanged), `"tree"`, `"table"`, `"kv"`, `"grouped"` — plus
261
+ `treeField`, `treeBadge`, `groupBy`, and `flags`. All of it crosses the wire as data, so
262
+ this remains the only channel-rendering code in the panel however many packages contribute.
263
+
264
+ - **Correlated requests fold together on the All tab.** A channel names the field that
265
+ relates traces (`traceGroup`); everything sharing a value there collapses under the oldest
266
+ member with a `+N` toggle. One thing you did is often several requests — a visit and the
267
+ deferred props that follow it — and listing them as unrelated siblings is how the request
268
+ you are reading gets pushed off the top.
269
+
270
+ - **Badge chips are accented by hashing their own text**, so `partial` and `deferred` are
271
+ tellable apart at a glance without devtools holding a list of every value any package
272
+ might use. Stable per value, so a badge keeps its colour between requests.
273
+
274
+ - `buildPathTree` and `traceGroupKey` are exported from `@zerotal/devtools/client`, for the
275
+ same reason `matchesFilter` is: they are the parts of the panel that are logic rather than
276
+ markup, and they are worth testing without a DOM.
277
+
278
+ ### Changed
279
+
280
+ - **The redaction walk moved to `redactGraph` in `@zerotal/core/security`**, and the Inertia
281
+ recorder runs the same one. Both had independently solved the same three problems — cycles,
282
+ depth, and values that read better flat than walked — and only one of them had to.
283
+ Markers are still each caller's own: a panel's `‹redacted›` is a display choice, while an
284
+ adapter implementing a published protocol has `[REDACTED]` specified for it. Sharing the
285
+ traversal does not mean agreeing on the words. No behaviour change.
286
+
287
+ - **Failed requests say what failed.** `RequestFailed` has always carried the error
288
+ message; the trace dropped it, so a request that threw showed as a red status code with
289
+ nothing to read beside it. Traces now carry an `exception` (`message` and `status`), the
290
+ Queries tab leads with it, and the All tab marks the row and shows the message inline —
291
+ so you can find the request that broke without opening each one in turn.
292
+
293
+ - **Mail previews.** The rendered HTML of every captured email has crossed the wire on
294
+ every request since mail capture landed, and has never been shown. Each mail now has a
295
+ **Preview**, collapsed by default, rendered in a fully sandboxed frame — no scripts, no
296
+ same-origin access, no navigation. The sandbox is not optional hardening: the panel lives
297
+ in a shadow root on the app's own origin, so inserting a template's markup inline would
298
+ make any user input inside a mail a self-XSS on every dev machine.
299
+
300
+ - **N+1 warnings say what to do about them again.** Each warning now carries the eager-load
301
+ that removes it and the `DB.allowNPlusOne(…)` call that suppresses it when the repetition
302
+ is intended. A warning without a remedy is half a feature.
303
+
304
+ - **The panel remembers where you were.** Whether it was open, which tab you were on, and
305
+ what you had filtered to survive a reload. On a page you are reloading _because_ you are
306
+ debugging it, that was the wrong moment to lose your place.
307
+
308
+ ### Fixed
309
+
310
+ - **`capacity` did not do what it said.** The browser panel trimmed its list at a hardcoded
311
+ 100 regardless of config, so an app with `DevtoolsConfig({ capacity: 250 })` received 250
312
+ traces in the opening frame and then silently lost everything past 100 as soon as the next
313
+ request arrived. The stream now sends the store's capacity and the panel trims to it.
314
+
315
+ - **A circular argument to `console.log` threw out of the log capture** and into the
316
+ caller. The capture ran `JSON.stringify` on the raw argument, so anything
317
+ self-referential — a model with a loaded relation back to its parent, a request object —
318
+ raised `Converting circular structure to JSON` from inside a patched `console.log`.
319
+
320
+ ### Security
321
+
322
+ - **Redaction covered query bindings only.** Console log arguments, channel entries, and
323
+ cache keys were streamed to the browser and written to `.zerotal/devtools.sqlite`
324
+ unredacted, where they sat for a day — so one `console.log(user)` during a debug session
325
+ wrote a full user record, password hash included, to disk.
326
+
327
+ All four are now masked at the sink, where the value enters the trace rather than where
328
+ the panel draws it: redacting in a renderer protects nothing, because the unredacted copy
329
+ is already persisted by then. One rule decides all four, so `allow` and `deny` in
330
+ `config/devtools.ts` mean the same thing everywhere. A cache key keeps its name and loses
331
+ only what follows it, so the Cache tab stays legible.
332
+
333
+ New exports: `redactValue`, `redactCacheKey`, and `isSensitiveName`.
334
+
335
+ ### Removed
336
+
337
+ - **`src/panel-app.js`** — ~500 lines of the pre-`client.ts` panel, unimported, unbundled,
338
+ and unserved since the rewrite.
339
+
11
340
  ## [1.6.2] — 2026-08-15
12
341
 
13
342
  ### Fixed
package/api-surface.md ADDED
@@ -0,0 +1,298 @@
1
+ # @zerotal/devtools — public API surface
2
+
3
+ <!-- AUTO-GENERATED by scripts/api-surface.ts. Do not edit by hand.
4
+ Run `bun run api:surface` to regenerate after an intentional API change. -->
5
+
6
+ ## . `(./src/index.ts)`
7
+
8
+ class DevtoolsInjectionMiddleware = {
9
+ new (): DevtoolsInjectionMiddleware
10
+ static with: <T extends new (...args: any[]) => BaseMiddleware<any>, Opts = T extends new (...args: any[]) => BaseMiddleware<infer U> ? U : object>(this: T, options: DeepPartial<NoInfer<Opts>>) => new () => InstanceType<T>
11
+ afterResponse?: (ctx: HttpContext) => Promise<void>
12
+ handle: (http: HttpContext, next: NextFn) => Promise<Response | void>
13
+ onError?: (ctx: HttpContext, error: Error) => Promise<void>
14
+ }
15
+
16
+ class DevtoolsProvider = {
17
+ new (app: Application): DevtoolsProvider
18
+ static dependsOn?: (new (app: Application) => ServiceProvider)[]
19
+ static environments: AppEnvironment[]
20
+ static priority?: number
21
+ static provides: readonly []
22
+ devProcesses: () => DevProcessDefinition[]
23
+ doctorChecks: () => DoctorCheck[]
24
+ onBooted: () => Promise<void>
25
+ onBooting: () => Promise<void>
26
+ onRegister: () => void
27
+ onRequestProcessed: (_ctx: HttpContext) => Promise<void>
28
+ onRequestReceived: (_ctx: HttpContext) => Promise<void>
29
+ onResponseSent: (_ctx: HttpContext) => Promise<void>
30
+ onStarted: () => Promise<void>
31
+ onStarting: () => Promise<void>
32
+ onStopped: () => Promise<void>
33
+ onStopping: () => Promise<void>
34
+ replContext: () => Record<string, unknown>
35
+ }
36
+
37
+ class TraceStore = {
38
+ new (options?: TraceStoreOptions | number): TraceStore
39
+ all: () => RequestTrace[]
40
+ capacity: number
41
+ clear: () => void
42
+ dispose: () => void
43
+ persisting: boolean
44
+ push: (trace: RequestTrace) => void
45
+ subscribe: (fn: Subscriber) => () => void
46
+ }
47
+
48
+ const traceSink = TraceSink
49
+
50
+ function _setTraceStore = (store: TraceStore | null) => void
51
+
52
+ function attributeBindings = (sql: string, count: number) => Array<string | undefined>
53
+
54
+ function DevtoolsConfig = (options?: Partial<DevtoolsConfigShape>) => DevtoolsConfigShape
55
+
56
+ function devtoolsEnabled = () => boolean
57
+
58
+ function isSensitiveName = (name: string, options?: RedactionOptions) => boolean
59
+
60
+ function redactBindings = (sql: string, bindings: unknown[], options?: RedactionOptions) => unknown[]
61
+
62
+ function redactCacheKey = (key: string, options?: RedactionOptions) => string
63
+
64
+ function redactValue = (value: unknown, options?: RedactionOptions) => unknown
65
+
66
+ function startDevtoolsStream = () => () => void
67
+
68
+ function traceChannels = () => TraceChannelDescriptor[]
69
+
70
+ function traceStore = () => TraceStore
71
+
72
+ interface AuthInfo = {
73
+ email?: unknown
74
+ id: unknown
75
+ name?: unknown
76
+ }
77
+
78
+ interface CacheEntry = {
79
+ durationMs: number
80
+ key: string
81
+ offsetMs: number
82
+ op: 'hit' | 'miss' | 'write' | 'forget' | 'flush' | 'has'
83
+ ttl?: number | undefined
84
+ }
85
+
86
+ interface DevtoolsConfigShape = {
87
+ capacity: number
88
+ captureSource: boolean
89
+ dbPath: string | null
90
+ editor: EditorName | null
91
+ editorPathMap: Record<string, string>
92
+ enabled: boolean | null
93
+ gate: DevtoolsGate | null
94
+ headers: string[]
95
+ pruneHours: number
96
+ redact: RedactionOptions
97
+ }
98
+
99
+ interface DevtoolsInjectionOptions = {}
100
+
101
+ interface DevtoolsPanelPlugin = {
102
+ badge?: () => number | string | undefined
103
+ id: string
104
+ render: (el: HTMLElement, context?: DevtoolsPanelContext) => void
105
+ title: string
106
+ }
107
+
108
+ interface ExceptionInfo = {
109
+ frames?: SourceLocation[]
110
+ message: string
111
+ status: number
112
+ type?: string
113
+ }
114
+
115
+ interface JobEntry = {
116
+ className: string
117
+ durationMs: number
118
+ error?: string
119
+ offsetMs: number
120
+ queue: string
121
+ status: 'dispatched' | 'completed' | 'failed'
122
+ }
123
+
124
+ interface LogEntry = {
125
+ args: string[]
126
+ level: 'error' | 'info' | 'log' | 'warn' | 'debug'
127
+ offsetMs: number
128
+ source?: SourceLocation
129
+ }
130
+
131
+ interface MailEntry = {
132
+ className: string
133
+ durationMs: number
134
+ html: string
135
+ offsetMs: number
136
+ queued: boolean
137
+ subject: string
138
+ to: string[]
139
+ }
140
+
141
+ interface NPlusOneWarning = {
142
+ count: number
143
+ sql: string
144
+ }
145
+
146
+ interface QuerySpan = {
147
+ bindings: unknown[]
148
+ durationMs: number
149
+ rowCount: number
150
+ source?: SourceLocation
151
+ sql: string
152
+ startMs: number
153
+ }
154
+
155
+ interface RedactionOptions = {
156
+ allow?: string[]
157
+ deny?: string[]
158
+ enabled?: boolean
159
+ }
160
+
161
+ interface RequestTrace = {
162
+ auth: AuthInfo | null
163
+ cache: CacheEntry[]
164
+ channels: Record<string, TraceChannelEntry[]>
165
+ durationMs: number
166
+ exception: ExceptionInfo | null
167
+ headers: Record<string, string>
168
+ id: string
169
+ jobs: JobEntry[]
170
+ logs: LogEntry[]
171
+ mail: MailEntry[]
172
+ memory: number
173
+ method: string
174
+ path: string
175
+ queries: QuerySpan[]
176
+ queryParams: Record<string, string>
177
+ requestId: string
178
+ responseHeaders: Record<string, string>
179
+ route: RouteInfo | null
180
+ session: string[]
181
+ startMs: number
182
+ statusCode: number
183
+ warnings: NPlusOneWarning[]
184
+ }
185
+
186
+ interface RouteInfo = {
187
+ action: string
188
+ controller: string
189
+ pattern: string
190
+ }
191
+
192
+ interface SourceLocation = {
193
+ column?: number
194
+ file: string
195
+ function?: string
196
+ line: number
197
+ }
198
+
199
+ interface TraceChannelDescriptor = {
200
+ badge?: string
201
+ flags?: string[]
202
+ groupBy?: string
203
+ hidden?: boolean
204
+ id: string
205
+ label: string
206
+ meta?: string[]
207
+ order?: number
208
+ render?: 'table' | 'rows' | 'tree' | 'kv' | 'grouped'
209
+ title?: string
210
+ traceGroup?: string
211
+ treeBadge?: string
212
+ treeField?: string
213
+ warn?: string
214
+ }
215
+
216
+ interface TraceChannelEntry = {
217
+ [key: string]: unknown
218
+ offsetMs: number
219
+ }
220
+
221
+ interface TraceSink = {
222
+ bufferCache: (ctx: object, c: Omit<CacheEntry, 'offsetMs'>) => void
223
+ bufferJob: (ctx: object, j: Omit<JobEntry, 'offsetMs'>) => void
224
+ bufferMail: (ctx: object, m: Omit<MailEntry, 'offsetMs'>) => void
225
+ bufferQuery: (ctx: object, q: QuerySpan) => void
226
+ bufferWarning: (ctx: object, w: NPlusOneWarning) => void
227
+ channel: (descriptor: TraceChannelDescriptor) => void
228
+ finalise: (ctx: object, meta: { startMs: number; durationMs: number; method?: string;}) => void
229
+ record: (ctx: object, channel: string, entry: Record<string, unknown>) => void
230
+ }
231
+
232
+ interface TraceStoreOptions = {
233
+ capacity?: number
234
+ dbPath?: string | null
235
+ pruneHours?: number
236
+ }
237
+
238
+ type DevtoolsGate = (request: Request) => boolean | Promise<boolean>
239
+
240
+ type EditorName = 'vscode' | 'vscode-insiders' | 'cursor' | 'windsurf' | 'zed' | 'webstorm'
241
+
242
+ ## ./client `(./src/client/index.ts)`
243
+
244
+ const DevTools = { start(opts?: DevtoolsClientOptions): void;}
245
+
246
+ const SLOW_MS = 300
247
+
248
+ function buildPathTree = (paths: Array<[string, unknown]>) => Map<string, PathTreeNode>
249
+
250
+ function facetsActive = (f: Facets) => boolean
251
+
252
+ function foldTraceRows = (matches: Array<{ trace: RequestTrace; index: number;}>, channels: TraceChannelDescriptor[], expanded: ReadonlySet<string>) => TraceRow[]
253
+
254
+ function matchesFacets = (trace: RequestTrace, f: Facets) => boolean
255
+
256
+ function matchesFilter = (trace: RequestTrace, query: string) => boolean
257
+
258
+ function methodsPresent = (traces: RequestTrace[]) => string[]
259
+
260
+ function noFacets = () => Facets
261
+
262
+ function traceGroupKey = (trace: RequestTrace, channels: TraceChannelDescriptor[]) => string | null
263
+
264
+ function traceMatches = (trace: RequestTrace, query: string, f: Facets) => boolean
265
+
266
+ interface DevtoolsClientOptions = {
267
+ endpoint?: string
268
+ mode?: 'floating' | 'standalone'
269
+ mount?: HTMLElement
270
+ }
271
+
272
+ interface DevtoolsPanelPlugin = {
273
+ badge?: () => number | string | undefined
274
+ id: string
275
+ render: (el: HTMLElement, context?: DevtoolsPanelContext) => void
276
+ title: string
277
+ }
278
+
279
+ interface Facets = {
280
+ errors: boolean
281
+ methods: string[]
282
+ nPlusOne: boolean
283
+ slow: boolean
284
+ statusClasses: string[]
285
+ }
286
+
287
+ interface PathTreeNode = {
288
+ attrs: Record<string, unknown> | null
289
+ children: Map<string, PathTreeNode>
290
+ }
291
+
292
+ interface TraceRow = {
293
+ child: boolean
294
+ groupKey?: string
295
+ groupSize?: number
296
+ index: number
297
+ trace: RequestTrace
298
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zerotal/devtools",
3
- "version": "1.6.3",
3
+ "version": "1.7.2",
4
4
  "license": "MIT",
5
5
  "maturity": "stable",
6
6
  "private": false,
@@ -9,10 +9,11 @@
9
9
  "types": "./src/index.ts",
10
10
  "exports": {
11
11
  ".": "./src/index.ts",
12
- "./client": "./src/client.ts"
12
+ "./client": "./src/client/index.ts"
13
13
  },
14
14
  "files": [
15
15
  "CHANGELOG.md",
16
+ "api-surface.md",
16
17
  "src",
17
18
  "!src/**/*.test.ts",
18
19
  "!src/**/*.test.tsx",
@@ -30,11 +31,11 @@
30
31
  "typecheck": "tsc --noEmit"
31
32
  },
32
33
  "dependencies": {
33
- "@zerotal/core": "1.6.3"
34
+ "@zerotal/core": "1.7.2"
34
35
  },
35
36
  "devDependencies": {
36
37
  "typescript": "^5.8.0",
37
- "@zerotal/orm": "1.6.3"
38
+ "@zerotal/orm": "1.7.2"
38
39
  },
39
40
  "description": "In-browser developer tools for Zerotal — request traces, an inspector panel, and an extensible tab registry.",
40
41
  "keywords": [