@zerotal/devtools 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/LICENSE +21 -0
- package/README.md +111 -0
- package/package.json +53 -0
- package/src/DevtoolsInjectionMiddleware.ts +167 -0
- package/src/RequestTrace.ts +148 -0
- package/src/TraceStore.ts +272 -0
- package/src/client-auto.ts +9 -0
- package/src/client.ts +1048 -0
- package/src/config.ts +60 -0
- package/src/dashboard-auto.ts +13 -0
- package/src/index.ts +27 -0
- package/src/panel-app.js +519 -0
- package/src/panel.html +26 -0
- package/src/provider/DevtoolsProvider.ts +153 -0
- package/src/redaction.ts +208 -0
- package/src/tracing.ts +347 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Changelog — @zerotal/devtools
|
|
2
|
+
|
|
3
|
+
All notable changes to this package are documented here. The format is
|
|
4
|
+
based on [Keep a Changelog](https://keepachangelog.com/); this package
|
|
5
|
+
follows the Zerotal monorepo's unified versioning.
|
|
6
|
+
|
|
7
|
+
**Maturity: `experimental`**
|
|
8
|
+
|
|
9
|
+
## [Unreleased]
|
|
10
|
+
|
|
11
|
+
## [1.0.0] — 2026-08-05
|
|
12
|
+
|
|
13
|
+
_First public release._
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- **Extensible panel — other packages can add their own tab.** The injected panel now exposes a global registry, `window.__zerotalDevtools`, that any package's browser code pushes a panel into: `window.__zerotalDevtools?.register({ id, title, badge?, render })` adds a tab alongside Queries/Logs/Request/Mail/Cache/Jobs, and `refresh(id)` pushes a live update (badge + re-render the open tab). Registration is order-independent (the registry is created by whichever runs first) and optional-peer friendly (guard with `?.`; a no-op when devtools isn't present). Panels render into the shared Shadow-DOM content area, so the devtools CSS classes/variables are available and contributed tabs match the panel without shipping styles. The `DevtoolsPanelPlugin` type is exported for TypeScript consumers. First consumer: `@zerotal/flow`'s time-travel Timeline. See [Extending the panel](/docs/devtools#extending-the-panel).
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- Moved service provider to `src/provider/`.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Zerotal
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# @zerotal/devtools
|
|
2
|
+
|
|
3
|
+
> A live, in-page development panel that traces every request — SQL queries, N+1 warnings, logs, mail, cache, and jobs.
|
|
4
|
+
|
|
5
|
+
`@zerotal/devtools` injects a floating debug panel into every HTML response during development. No browser extension required: it shows per-request traces and exposes a `TraceStore` you can read programmatically. `DevtoolsProvider` is a no-op when `APP_ENV=production` or `APP_ENV=prod`.
|
|
6
|
+
|
|
7
|
+
Part of the [Zerotal](../../README.md) framework. Requires **Bun ≥ 1.3.14**.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
bun add @zerotal/devtools
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Setup
|
|
16
|
+
|
|
17
|
+
### 1. Register the provider
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
// bootstrap/providers.ts
|
|
21
|
+
import { DatabaseProvider } from "@zerotal/orm";
|
|
22
|
+
import { DevtoolsProvider } from "@zerotal/devtools";
|
|
23
|
+
|
|
24
|
+
const providers = [
|
|
25
|
+
// …your other providers
|
|
26
|
+
DatabaseProvider,
|
|
27
|
+
DevtoolsProvider,
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
export default providers;
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
`DevtoolsProvider` automatically registers `DevtoolsInjectionMiddleware` — you do not need to add it to `.use([…])` manually.
|
|
34
|
+
|
|
35
|
+
### 2. Start the client panel
|
|
36
|
+
|
|
37
|
+
In your frontend entry (e.g. `resources/js/app.js`):
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
import { DevTools } from "@zerotal/devtools/client";
|
|
41
|
+
DevTools.start(); // optionally { endpoint: "/__zerotal/devtools" }
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
This connects to the SSE stream and mounts the floating panel. Press `Alt+D` (or `Cmd+D` on Mac) to toggle it.
|
|
45
|
+
|
|
46
|
+
## Usage
|
|
47
|
+
|
|
48
|
+
### Read traces programmatically
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
import { traceStore } from "@zerotal/devtools";
|
|
52
|
+
|
|
53
|
+
// All traces stored in memory (up to 100, most recent first)
|
|
54
|
+
const traces = traceStore().all();
|
|
55
|
+
|
|
56
|
+
// Find slow requests / N+1 offenders
|
|
57
|
+
const slow = traces.filter((t) => t.durationMs > 500);
|
|
58
|
+
const nplus = traces.filter((t) => t.warnings.length > 0);
|
|
59
|
+
|
|
60
|
+
// Subscribe to new traces (fn receives null on 'clear')
|
|
61
|
+
const unsub = traceStore().subscribe((trace) => {
|
|
62
|
+
if (trace === null) return;
|
|
63
|
+
console.log(`${trace.method} ${trace.path} → ${trace.statusCode} (${trace.durationMs}ms)`);
|
|
64
|
+
});
|
|
65
|
+
unsub();
|
|
66
|
+
|
|
67
|
+
traceStore().clear();
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Traces persist to `.zerotal/devtools.sqlite` and reload on restart. Configure in
|
|
71
|
+
`config/devtools.ts`:
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
import { DevtoolsConfig } from "@zerotal/devtools";
|
|
75
|
+
|
|
76
|
+
export default DevtoolsConfig({
|
|
77
|
+
capacity: 250,
|
|
78
|
+
dbPath: ".data/devtools.sqlite", // null keeps traces in memory only
|
|
79
|
+
pruneHours: 48,
|
|
80
|
+
redact: { allow: ["email"] }, // query bindings are masked by default
|
|
81
|
+
});
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`ZT_DEVTOOLS_DB` and `ZT_DEVTOOLS_PRUNE_HOURS` still apply when no config
|
|
85
|
+
file is present.
|
|
86
|
+
|
|
87
|
+
## Exports
|
|
88
|
+
|
|
89
|
+
The package exposes two subpaths:
|
|
90
|
+
|
|
91
|
+
### `@zerotal/devtools` (`.`)
|
|
92
|
+
|
|
93
|
+
| Export | Kind | Description |
|
|
94
|
+
| ----------------------------- | ---------------- | ---------------------------------------------------------------------------------------------------------------- |
|
|
95
|
+
| `DevtoolsProvider` | provider | Registers the injection middleware and internal `/__zerotal/devtools` routes (dev only). |
|
|
96
|
+
| `DevtoolsInjectionMiddleware` | middleware | Injects the panel into HTML responses. Type: `DevtoolsInjectionOptions`. |
|
|
97
|
+
| `TraceStore`, `traceStore` | class / accessor | In-memory + SQLite-backed trace store (`all`, `push`, `clear`, `subscribe`, `dispose`). |
|
|
98
|
+
| `DevtoolsConfig` | config | Typed `config/devtools.ts` factory. Type: `DevtoolsConfigShape`. |
|
|
99
|
+
| `traceSink`, `traceChannels` | sink / registry | The `devtools.trace` surface other packages contribute through. |
|
|
100
|
+
| `redactBindings` | function | Mask the bindings of a statement. Type: `RedactionOptions`. |
|
|
101
|
+
| Trace types | types | `RequestTrace`, `QuerySpan`, `NPlusOneWarning`, `MailEntry`, `CacheEntry`, `JobEntry`, `TraceChannelDescriptor`. |
|
|
102
|
+
|
|
103
|
+
### `@zerotal/devtools/client` (`./client`)
|
|
104
|
+
|
|
105
|
+
| Export | Description |
|
|
106
|
+
| ---------- | --------------------------------------------------------------------- |
|
|
107
|
+
| `DevTools` | Browser-side panel; call `DevTools.start()` from your frontend entry. |
|
|
108
|
+
|
|
109
|
+
## Documentation
|
|
110
|
+
|
|
111
|
+
- [DevTools](../../docs/devtools.md)
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@zerotal/devtools",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"maturity": "experimental",
|
|
6
|
+
"private": false,
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "./src/index.ts",
|
|
9
|
+
"types": "./src/index.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": "./src/index.ts",
|
|
12
|
+
"./client": "./src/client.ts"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"CHANGELOG.md",
|
|
16
|
+
"src",
|
|
17
|
+
"!src/**/*.test.ts",
|
|
18
|
+
"!src/**/*.test.tsx",
|
|
19
|
+
"!src/**/*.spec.ts",
|
|
20
|
+
"!src/**/__fixtures__/**"
|
|
21
|
+
],
|
|
22
|
+
"publishConfig": {
|
|
23
|
+
"access": "public"
|
|
24
|
+
},
|
|
25
|
+
"engines": {
|
|
26
|
+
"bun": ">=1.3.14"
|
|
27
|
+
},
|
|
28
|
+
"scripts": {
|
|
29
|
+
"test": "bun test",
|
|
30
|
+
"typecheck": "tsc --noEmit"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@zerotal/core": "1.0.0"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"typescript": "^5.8.0",
|
|
37
|
+
"@zerotal/orm": "1.0.0"
|
|
38
|
+
},
|
|
39
|
+
"description": "In-browser developer tools for Zerotal — request traces, an inspector panel, and an extensible tab registry.",
|
|
40
|
+
"keywords": [
|
|
41
|
+
"zerotal",
|
|
42
|
+
"bun",
|
|
43
|
+
"typescript",
|
|
44
|
+
"framework"
|
|
45
|
+
],
|
|
46
|
+
"repository": {
|
|
47
|
+
"type": "git",
|
|
48
|
+
"url": "git+https://github.com/zerotaldev/zerotal.git",
|
|
49
|
+
"directory": "packages/devtools"
|
|
50
|
+
},
|
|
51
|
+
"homepage": "https://github.com/zerotaldev/zerotal/tree/main/packages/devtools#readme",
|
|
52
|
+
"bugs": "https://github.com/zerotaldev/zerotal/issues"
|
|
53
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { fileURLToPath } from "node:url";
|
|
2
|
+
import type { NextFn, HttpContext } from "@zerotal/core";
|
|
3
|
+
import { BaseMiddleware } from "@zerotal/core";
|
|
4
|
+
import { traceStore } from "./TraceStore.ts";
|
|
5
|
+
import { traceChannels } from "./tracing.ts";
|
|
6
|
+
|
|
7
|
+
// ── Injected browser client bundle ────────────────────────────────────────────
|
|
8
|
+
// The in-page devtools panel is bundled for the browser on first request and
|
|
9
|
+
// cached for the process lifetime (dev only — the provider is a no-op in prod).
|
|
10
|
+
let _clientJs: string | null = null;
|
|
11
|
+
|
|
12
|
+
async function _buildClientJs(): Promise<string> {
|
|
13
|
+
if (_clientJs !== null) return _clientJs;
|
|
14
|
+
try {
|
|
15
|
+
const entry = fileURLToPath(new URL("./client-auto.ts", import.meta.url));
|
|
16
|
+
const result = await Bun.build({ entrypoints: [entry], target: "browser", minify: true });
|
|
17
|
+
const out = result.outputs[0];
|
|
18
|
+
_clientJs =
|
|
19
|
+
result.success && out ? await out.text() : "/* [Zerotal DevTools] client build failed */";
|
|
20
|
+
} catch (error) {
|
|
21
|
+
_clientJs = `/* [Zerotal DevTools] client build error: ${String(error)} */`;
|
|
22
|
+
}
|
|
23
|
+
return _clientJs;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** The standalone dashboard bundle — the same panel, mounted full-window. */
|
|
27
|
+
let _dashboardJs: string | null = null;
|
|
28
|
+
|
|
29
|
+
async function _buildDashboardJs(): Promise<string> {
|
|
30
|
+
if (_dashboardJs !== null) return _dashboardJs;
|
|
31
|
+
try {
|
|
32
|
+
const entry = fileURLToPath(new URL("./dashboard-auto.ts", import.meta.url));
|
|
33
|
+
const result = await Bun.build({ entrypoints: [entry], target: "browser", minify: true });
|
|
34
|
+
const out = result.outputs[0];
|
|
35
|
+
_dashboardJs =
|
|
36
|
+
result.success && out ? await out.text() : "/* [Zerotal DevTools] dashboard build failed */";
|
|
37
|
+
} catch (error) {
|
|
38
|
+
_dashboardJs = `/* [Zerotal DevTools] dashboard build error: ${String(error)} */`;
|
|
39
|
+
}
|
|
40
|
+
return _dashboardJs;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface DevtoolsInjectionOptions {
|
|
44
|
+
// reserved for future use
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ── SSE subscribers ───────────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
const _sseClients = new Set<ReadableStreamDefaultController<Uint8Array>>();
|
|
50
|
+
const _enc = new TextEncoder();
|
|
51
|
+
|
|
52
|
+
function _ssePublish(data: unknown): void {
|
|
53
|
+
if (_sseClients.size === 0) return;
|
|
54
|
+
const chunk = _enc.encode(`data: ${JSON.stringify(data)}\n\n`);
|
|
55
|
+
for (const ctrl of _sseClients) {
|
|
56
|
+
try {
|
|
57
|
+
ctrl.enqueue(chunk);
|
|
58
|
+
} catch {
|
|
59
|
+
_sseClients.delete(ctrl);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Bridge the trace store to connected panels. Called by {@link DevtoolsProvider}
|
|
66
|
+
* on boot rather than at module scope — subscribing on import would tie the
|
|
67
|
+
* stream to whichever store existed at import time, before the provider has
|
|
68
|
+
* built the one the app's config asks for.
|
|
69
|
+
*
|
|
70
|
+
* @returns A disposer that unsubscribes and drops connected clients.
|
|
71
|
+
*/
|
|
72
|
+
export function startDevtoolsStream(): () => void {
|
|
73
|
+
const unsubscribe = traceStore().subscribe((trace) => {
|
|
74
|
+
if (trace === null) _ssePublish({ type: "clear" });
|
|
75
|
+
else _ssePublish({ type: "trace", data: trace });
|
|
76
|
+
});
|
|
77
|
+
return () => {
|
|
78
|
+
unsubscribe();
|
|
79
|
+
_sseClients.clear();
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ── Middleware — devtools API routes only ─────────────────────────────────────
|
|
84
|
+
//
|
|
85
|
+
// All request tracing is handled by tracing.ts subscribing to FrameworkEvents.
|
|
86
|
+
// This middleware only serves the devtools panel endpoints.
|
|
87
|
+
|
|
88
|
+
export class DevtoolsInjectionMiddleware extends BaseMiddleware<DevtoolsInjectionOptions> {
|
|
89
|
+
protected options: DevtoolsInjectionOptions = {};
|
|
90
|
+
|
|
91
|
+
async handle(http: HttpContext, next: NextFn): Promise<Response | void> {
|
|
92
|
+
const { pathname } = http.url;
|
|
93
|
+
|
|
94
|
+
if (pathname === "/__zerotal/devtools/api/traces") {
|
|
95
|
+
return Response.json(traceStore().all());
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (pathname === "/__zerotal/devtools/api/channels") {
|
|
99
|
+
return Response.json(traceChannels());
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (pathname === "/__zerotal/devtools/api/clear" && http.request.method === "POST") {
|
|
103
|
+
traceStore().clear();
|
|
104
|
+
return new Response(null, { status: 204 });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (pathname === "/__zerotal/devtools/sse") {
|
|
108
|
+
let ctrl!: ReadableStreamDefaultController<Uint8Array>;
|
|
109
|
+
const stream = new ReadableStream<Uint8Array>({
|
|
110
|
+
start(c) {
|
|
111
|
+
ctrl = c;
|
|
112
|
+
_sseClients.add(ctrl);
|
|
113
|
+
// The opening frame carries the channel descriptors alongside the
|
|
114
|
+
// history, so a panel can render a package's tab on first paint
|
|
115
|
+
// instead of waiting for that package's next entry.
|
|
116
|
+
ctrl.enqueue(
|
|
117
|
+
_enc.encode(
|
|
118
|
+
`data: ${JSON.stringify({
|
|
119
|
+
type: "history",
|
|
120
|
+
data: traceStore().all(),
|
|
121
|
+
channels: traceChannels(),
|
|
122
|
+
})}\n\n`,
|
|
123
|
+
),
|
|
124
|
+
);
|
|
125
|
+
},
|
|
126
|
+
cancel() {
|
|
127
|
+
_sseClients.delete(ctrl);
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
return new Response(stream, {
|
|
131
|
+
headers: {
|
|
132
|
+
"Content-Type": "text/event-stream",
|
|
133
|
+
"Cache-Control": "no-cache",
|
|
134
|
+
Connection: "keep-alive",
|
|
135
|
+
"X-Accel-Buffering": "no",
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (pathname === "/__zerotal/devtools/client.js") {
|
|
141
|
+
return new Response(await _buildClientJs(), {
|
|
142
|
+
headers: {
|
|
143
|
+
"Content-Type": "text/javascript; charset=utf-8",
|
|
144
|
+
"Cache-Control": "no-cache",
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (pathname === "/__zerotal/devtools/dashboard.js") {
|
|
150
|
+
return new Response(await _buildDashboardJs(), {
|
|
151
|
+
headers: {
|
|
152
|
+
"Content-Type": "text/javascript; charset=utf-8",
|
|
153
|
+
"Cache-Control": "no-cache",
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (pathname === "/__zerotal/devtools" || pathname === "/__zerotal/devtools/") {
|
|
159
|
+
const html = await Bun.file(new URL("./panel.html", import.meta.url)).text();
|
|
160
|
+
return new Response(html, {
|
|
161
|
+
headers: { "Content-Type": "text/html; charset=utf-8" },
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return next();
|
|
166
|
+
}
|
|
167
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
export interface QuerySpan {
|
|
2
|
+
sql: string;
|
|
3
|
+
bindings: unknown[];
|
|
4
|
+
startMs: number;
|
|
5
|
+
durationMs: number;
|
|
6
|
+
rowCount: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface NPlusOneWarning {
|
|
10
|
+
sql: string;
|
|
11
|
+
count: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface RouteInfo {
|
|
15
|
+
pattern: string;
|
|
16
|
+
controller: string;
|
|
17
|
+
action: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface AuthInfo {
|
|
21
|
+
id: unknown;
|
|
22
|
+
name?: unknown;
|
|
23
|
+
email?: unknown;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface LogEntry {
|
|
27
|
+
level: "log" | "debug" | "info" | "warn" | "error";
|
|
28
|
+
args: string[];
|
|
29
|
+
offsetMs: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface MailEntry {
|
|
33
|
+
/** Mailable class name (e.g. "WelcomeMail") */
|
|
34
|
+
className: string;
|
|
35
|
+
to: string[];
|
|
36
|
+
subject: string;
|
|
37
|
+
/** Rendered HTML — used for the mail preview panel */
|
|
38
|
+
html: string;
|
|
39
|
+
durationMs: number;
|
|
40
|
+
queued: boolean;
|
|
41
|
+
offsetMs: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface CacheEntry {
|
|
45
|
+
op: "has" | "hit" | "miss" | "write" | "forget" | "flush";
|
|
46
|
+
key: string;
|
|
47
|
+
ttl?: number | undefined;
|
|
48
|
+
durationMs: number;
|
|
49
|
+
offsetMs: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface JobEntry {
|
|
53
|
+
className: string;
|
|
54
|
+
queue: string;
|
|
55
|
+
status: "dispatched" | "completed" | "failed";
|
|
56
|
+
durationMs: number;
|
|
57
|
+
error?: string;
|
|
58
|
+
offsetMs: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ── Open channels ─────────────────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* One entry on an open {@link TraceChannelDescriptor | channel}.
|
|
65
|
+
*
|
|
66
|
+
* The shape is deliberately a bare record: devtools does not know what a
|
|
67
|
+
* channel carries, only how its descriptor says to display it. `offsetMs` is
|
|
68
|
+
* stamped by devtools when the entry is recorded.
|
|
69
|
+
*/
|
|
70
|
+
export interface TraceChannelEntry extends Record<string, unknown> {
|
|
71
|
+
/** Milliseconds from the start of the request. Stamped on record. */
|
|
72
|
+
offsetMs: number;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* How a package's channel is displayed in the panel.
|
|
77
|
+
*
|
|
78
|
+
* The descriptor crosses the wire to the browser, so it names *fields* rather
|
|
79
|
+
* than carrying formatter functions — a channel is declared once on the server
|
|
80
|
+
* and rendered generically, which is what lets a package add a tab without
|
|
81
|
+
* devtools shipping a renderer for it.
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* trace.channel({
|
|
85
|
+
* id: "auth",
|
|
86
|
+
* label: "Auth",
|
|
87
|
+
* badge: "event",
|
|
88
|
+
* title: "detail",
|
|
89
|
+
* meta: ["guard", "ip"],
|
|
90
|
+
* warn: "failed",
|
|
91
|
+
* });
|
|
92
|
+
*/
|
|
93
|
+
export interface TraceChannelDescriptor {
|
|
94
|
+
/** Unique id — also the key under {@link RequestTrace.channels}. */
|
|
95
|
+
id: string;
|
|
96
|
+
/** Tab label in the panel. */
|
|
97
|
+
label: string;
|
|
98
|
+
/** Entry field rendered as the row's leading badge. */
|
|
99
|
+
badge?: string;
|
|
100
|
+
/** Entry field rendered as the row's main text. Defaults to the badge field. */
|
|
101
|
+
title?: string;
|
|
102
|
+
/** Entry fields rendered as dim metadata beneath the title. */
|
|
103
|
+
meta?: string[];
|
|
104
|
+
/** Entry field whose truthiness marks the row — and the tab's badge — as a warning. */
|
|
105
|
+
warn?: string;
|
|
106
|
+
/** Sort order among channel tabs. Lower sorts first. Defaults to 100. */
|
|
107
|
+
order?: number;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface RequestTrace {
|
|
111
|
+
id: string;
|
|
112
|
+
requestId: string;
|
|
113
|
+
method: string;
|
|
114
|
+
path: string;
|
|
115
|
+
statusCode: number;
|
|
116
|
+
startMs: number;
|
|
117
|
+
durationMs: number;
|
|
118
|
+
queries: QuerySpan[];
|
|
119
|
+
warnings: NPlusOneWarning[];
|
|
120
|
+
/** Heap memory used at the end of the request, in bytes */
|
|
121
|
+
memory: number;
|
|
122
|
+
/** URL query string parameters */
|
|
123
|
+
queryParams: Record<string, string>;
|
|
124
|
+
/** Filtered request headers (no auth/cookie values) */
|
|
125
|
+
headers: Record<string, string>;
|
|
126
|
+
/** Matched route pattern, controller, and action */
|
|
127
|
+
route: RouteInfo | null;
|
|
128
|
+
/** Authenticated user at the end of the request, or null for guests */
|
|
129
|
+
auth: AuthInfo | null;
|
|
130
|
+
/** Console log/debug/info/warn/error messages emitted during the request */
|
|
131
|
+
logs: LogEntry[];
|
|
132
|
+
/** Emails sent (or queued) during this request */
|
|
133
|
+
mail: MailEntry[];
|
|
134
|
+
/** Cache operations performed during this request */
|
|
135
|
+
cache: CacheEntry[];
|
|
136
|
+
/** Jobs dispatched (or processed synchronously) during this request */
|
|
137
|
+
jobs: JobEntry[];
|
|
138
|
+
/**
|
|
139
|
+
* Entries recorded on open channels, keyed by channel id.
|
|
140
|
+
*
|
|
141
|
+
* The fields above are the five signals devtools renders with bespoke UI they
|
|
142
|
+
* have earned — a query needs its bindings and duration bar, mail needs its
|
|
143
|
+
* preview. Everything else a package wants to show lands here and is rendered
|
|
144
|
+
* from its {@link TraceChannelDescriptor}, so contributing a tab takes no
|
|
145
|
+
* change inside devtools.
|
|
146
|
+
*/
|
|
147
|
+
channels: Record<string, TraceChannelEntry[]>;
|
|
148
|
+
}
|