@zerotal/devtools 1.8.0 → 1.9.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 +44 -0
- package/api-surface.md +1 -10
- package/package.json +3 -3
- package/src/DevtoolsInjectionMiddleware.ts +3 -0
- package/src/client/filter.ts +25 -2
- package/src/client/kind.ts +138 -0
- package/src/client/state.ts +20 -2
- package/src/client/tabs/all.ts +19 -0
- package/src/redaction.ts +2 -0
- package/src/tracing.ts +8 -0
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,50 @@ follows the Zerotal monorepo's unified versioning.
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [1.9.0] — 2026-08-29
|
|
12
|
+
|
|
13
|
+
### Documented
|
|
14
|
+
|
|
15
|
+
- **Every promised export is documented.** The `docs-coverage` gate reads `maturity: stable` as a
|
|
16
|
+
promise about a package's exports, and measures how much of that promise is written down. It
|
|
17
|
+
was 798 gaps across the suite; it is now zero. This package's share is covered on its own
|
|
18
|
+
pages — types named, options shapes described, and the decisions behind them recorded where
|
|
19
|
+
somebody looking for them will find them.
|
|
20
|
+
|
|
21
|
+
## [1.8.1] — 2026-08-26
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- **A page keeps the panel while its own assets load.** Opening `/login` selected `/login`,
|
|
26
|
+
then `/favicon.ico` a few milliseconds later, then `/css/app.css`: live mode selected every
|
|
27
|
+
trace as it arrived, and a page's sub-resources arrive right behind it. The bar named a
|
|
28
|
+
request nobody asked about, the detail below described that request's headers and its empty
|
|
29
|
+
session, and the page under inspection had scrolled into the list. Nothing shown was wrong;
|
|
30
|
+
it was all about the wrong request.
|
|
31
|
+
|
|
32
|
+
Traces are classified into three kinds rather than two, because "not the document" would
|
|
33
|
+
have suppressed the form post and the Inertia visit — the requests most worth watching.
|
|
34
|
+
What is skipped over is narrower: a sub-resource the browser fetched on its own initiative.
|
|
35
|
+
The browser is asked rather than the URL, since an app may serve an API from a `.js` route
|
|
36
|
+
and a build that hashes its asset names has no extension to read; response content type is
|
|
37
|
+
the fallback, so a page fetched by curl still reads as a page. Anything unclassifiable is
|
|
38
|
+
`api`, never `asset`. An asset still takes the selection when nothing is selected, so a
|
|
39
|
+
panel opened mid-load shows a request rather than an empty pane, and a paused panel still
|
|
40
|
+
counts assets toward its pending badge.
|
|
41
|
+
|
|
42
|
+
### Added
|
|
43
|
+
|
|
44
|
+
- **A `kind` facet on the All tab**, beside method and status. Assets were never the problem,
|
|
45
|
+
only their claim on the selection, so they are not hidden: pick `page` and `api` for a list
|
|
46
|
+
without fifty stylesheet fetches in it, or `asset` alone for what the browser pulled in,
|
|
47
|
+
what it cost and which of it 404'd — which was not visible anywhere before. `Facets.kinds`
|
|
48
|
+
is optional, because that interface is exported from `@zerotal/devtools/client` and a
|
|
49
|
+
required field added to a published interface breaks whoever builds one by hand.
|
|
50
|
+
|
|
51
|
+
- **`sec-fetch-dest` and `sec-fetch-mode` are recorded**, joining the safe header list. They
|
|
52
|
+
are where the classification above comes from, and they state what a request was for
|
|
53
|
+
without saying anything about who made it.
|
|
54
|
+
|
|
11
55
|
## [1.7.4] — 2026-08-21
|
|
12
56
|
|
|
13
57
|
### Fixed
|
package/api-surface.md
CHANGED
|
@@ -45,10 +45,6 @@ class TraceStore = {
|
|
|
45
45
|
subscribe: (fn: Subscriber) => () => void
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
const traceSink = TraceSink
|
|
49
|
-
|
|
50
|
-
function attributeBindings = (sql: string, count: number) => Array<string | undefined>
|
|
51
|
-
|
|
52
48
|
function DevtoolsConfig = (options?: Partial<DevtoolsConfigShape>) => DevtoolsConfigShape
|
|
53
49
|
|
|
54
50
|
function devtoolsEnabled = () => boolean
|
|
@@ -61,10 +57,6 @@ function redactCacheKey = (key: string, options?: RedactionOptions) => string
|
|
|
61
57
|
|
|
62
58
|
function redactValue = (value: unknown, options?: RedactionOptions) => unknown
|
|
63
59
|
|
|
64
|
-
function startDevtoolsStream = () => () => void
|
|
65
|
-
|
|
66
|
-
function traceChannels = () => TraceChannelDescriptor[]
|
|
67
|
-
|
|
68
60
|
function traceStore = () => TraceStore
|
|
69
61
|
|
|
70
62
|
interface AuthInfo = {
|
|
@@ -94,8 +86,6 @@ interface DevtoolsConfigShape = {
|
|
|
94
86
|
redact: RedactionOptions
|
|
95
87
|
}
|
|
96
88
|
|
|
97
|
-
interface DevtoolsInjectionOptions = {}
|
|
98
|
-
|
|
99
89
|
interface DevtoolsPanelPlugin = {
|
|
100
90
|
badge?: () => number | string | undefined
|
|
101
91
|
id: string
|
|
@@ -276,6 +266,7 @@ interface DevtoolsPanelPlugin = {
|
|
|
276
266
|
|
|
277
267
|
interface Facets = {
|
|
278
268
|
errors: boolean
|
|
269
|
+
kinds?: RequestKind[]
|
|
279
270
|
methods: string[]
|
|
280
271
|
nPlusOne: boolean
|
|
281
272
|
slow: boolean
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zerotal/devtools",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"maturity": "stable",
|
|
6
6
|
"private": false,
|
|
@@ -31,11 +31,11 @@
|
|
|
31
31
|
"typecheck": "tsc --noEmit"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@zerotal/core": "1.
|
|
34
|
+
"@zerotal/core": "1.9.0"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"typescript": "^5.8.0",
|
|
38
|
-
"@zerotal/orm": "1.
|
|
38
|
+
"@zerotal/orm": "1.9.0"
|
|
39
39
|
},
|
|
40
40
|
"description": "In-browser developer tools for Zerotal — request traces, an inspector panel, and an extensible tab registry.",
|
|
41
41
|
"keywords": [
|
|
@@ -43,6 +43,7 @@ async function _buildDashboardJs(): Promise<string> {
|
|
|
43
43
|
return _dashboardJs;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
/** @internal */
|
|
46
47
|
export interface DevtoolsInjectionOptions {
|
|
47
48
|
// reserved for future use
|
|
48
49
|
}
|
|
@@ -102,6 +103,8 @@ const SSE_HEARTBEAT_MS = 25_000;
|
|
|
102
103
|
* built the one the app's config asks for.
|
|
103
104
|
*
|
|
104
105
|
* @returns A disposer that unsubscribes and closes connected clients.
|
|
106
|
+
*
|
|
107
|
+
* @internal
|
|
105
108
|
*/
|
|
106
109
|
export function startDevtoolsStream(): () => void {
|
|
107
110
|
const unsubscribe = traceStore().subscribe((trace) => {
|
package/src/client/filter.ts
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* browser.
|
|
12
12
|
*/
|
|
13
13
|
import type { RequestTrace } from "../RequestTrace.ts";
|
|
14
|
+
import { requestKind, type RequestKind } from "./kind.ts";
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* A request slower than this reads as slow.
|
|
@@ -33,16 +34,37 @@ export interface Facets {
|
|
|
33
34
|
slow: boolean;
|
|
34
35
|
/** Only requests with an N+1 warning. */
|
|
35
36
|
nPlusOne: boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Request kinds to show. Empty means every kind.
|
|
39
|
+
*
|
|
40
|
+
* The one facet whose usual job is *subtraction*: an app's own traffic is
|
|
41
|
+
* `document` and `api`, and picking those two is how you get a list without
|
|
42
|
+
* fifty stylesheet fetches in it. Picking `asset` alone is the other half —
|
|
43
|
+
* what the browser pulled in, what it cost, and which of it 404'd, which is
|
|
44
|
+
* not visible anywhere else.
|
|
45
|
+
*
|
|
46
|
+
* Optional because {@link Facets} is exported from `@zerotal/devtools/client`
|
|
47
|
+
* and a required field added to a published interface breaks whoever builds one
|
|
48
|
+
* by hand. `noFacets()` always sets it; reads here tolerate its absence.
|
|
49
|
+
*/
|
|
50
|
+
kinds?: RequestKind[];
|
|
36
51
|
}
|
|
37
52
|
|
|
38
53
|
/** No narrowing at all — what a fresh panel starts with. */
|
|
39
54
|
export function noFacets(): Facets {
|
|
40
|
-
return { methods: [], statusClasses: [], errors: false, slow: false, nPlusOne: false };
|
|
55
|
+
return { methods: [], statusClasses: [], errors: false, slow: false, nPlusOne: false, kinds: [] };
|
|
41
56
|
}
|
|
42
57
|
|
|
43
58
|
/** Whether any facet is actually narrowing, for the "clear" affordance. */
|
|
44
59
|
export function facetsActive(f: Facets): boolean {
|
|
45
|
-
return
|
|
60
|
+
return (
|
|
61
|
+
f.methods.length > 0 ||
|
|
62
|
+
f.statusClasses.length > 0 ||
|
|
63
|
+
(f.kinds?.length ?? 0) > 0 ||
|
|
64
|
+
f.errors ||
|
|
65
|
+
f.slow ||
|
|
66
|
+
f.nPlusOne
|
|
67
|
+
);
|
|
46
68
|
}
|
|
47
69
|
|
|
48
70
|
/**
|
|
@@ -88,6 +110,7 @@ export function matchesFacets(trace: RequestTrace, f: Facets): boolean {
|
|
|
88
110
|
if (f.errors && !trace.exception && trace.statusCode < 400) return false;
|
|
89
111
|
if (f.slow && trace.durationMs <= SLOW_MS) return false;
|
|
90
112
|
if (f.nPlusOne && !trace.warnings.length) return false;
|
|
113
|
+
if (f.kinds?.length && !f.kinds.includes(requestKind(trace))) return false;
|
|
91
114
|
return true;
|
|
92
115
|
}
|
|
93
116
|
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What kind of request a trace is — the page, the app talking, or a file the
|
|
3
|
+
* browser fetched because the page said to.
|
|
4
|
+
*
|
|
5
|
+
* The panel had no such notion, and one behaviour made that expensive: live mode
|
|
6
|
+
* selects every trace as it arrives, so opening `/login` selected `/login` for a
|
|
7
|
+
* few milliseconds and then `/favicon.ico`, `/css/app.css`, and whatever else the
|
|
8
|
+
* page pulled. The bar named a request nobody asked about, the detail below it
|
|
9
|
+
* described that request's headers and its empty session, and the page you were
|
|
10
|
+
* actually looking at had scrolled into the list. The information was never
|
|
11
|
+
* wrong; it was about the wrong thing.
|
|
12
|
+
*
|
|
13
|
+
* Three kinds, because two would not do the job. Suppressing "not the document"
|
|
14
|
+
* would suppress the form post and the Inertia visit — the requests most worth
|
|
15
|
+
* watching. What deserves to be skipped over is narrower than that: a
|
|
16
|
+
* sub-resource the browser fetched on its own initiative.
|
|
17
|
+
*
|
|
18
|
+
* ## Why the browser is asked rather than the URL
|
|
19
|
+
*
|
|
20
|
+
* `Sec-Fetch-Dest` is the purpose-built answer — the browser states what it
|
|
21
|
+
* wanted the response for, and it is the only source here that cannot be fooled.
|
|
22
|
+
* A `.js` path can serve an API, a route with no extension can serve a
|
|
23
|
+
* stylesheet, and an app that hashes its asset names has neither. The extension
|
|
24
|
+
* rule is last and exists for the clients that send no such header at all.
|
|
25
|
+
*/
|
|
26
|
+
import type { RequestTrace } from "../RequestTrace.ts";
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* - `document` — a navigation. The page in the address bar.
|
|
30
|
+
* - `api` — the app talking: form posts, Inertia visits, fetch, anything a
|
|
31
|
+
* script asked for on purpose.
|
|
32
|
+
* - `asset` — a sub-resource the browser fetched by itself.
|
|
33
|
+
*/
|
|
34
|
+
export type RequestKind = "document" | "api" | "asset";
|
|
35
|
+
|
|
36
|
+
/** `Sec-Fetch-Dest` values that mean the browser wanted a file, not an answer. */
|
|
37
|
+
const ASSET_DESTINATIONS = new Set([
|
|
38
|
+
"image",
|
|
39
|
+
"style",
|
|
40
|
+
"script",
|
|
41
|
+
"font",
|
|
42
|
+
"manifest",
|
|
43
|
+
"audio",
|
|
44
|
+
"video",
|
|
45
|
+
"track",
|
|
46
|
+
"embed",
|
|
47
|
+
"object",
|
|
48
|
+
"worker",
|
|
49
|
+
"sharedworker",
|
|
50
|
+
"serviceworker",
|
|
51
|
+
"paintworklet",
|
|
52
|
+
"audioworklet",
|
|
53
|
+
"xslt",
|
|
54
|
+
"report",
|
|
55
|
+
]);
|
|
56
|
+
|
|
57
|
+
/** Content types served as files. Checked as prefixes — parameters follow. */
|
|
58
|
+
const ASSET_TYPES = [
|
|
59
|
+
"text/css",
|
|
60
|
+
"image/",
|
|
61
|
+
"font/",
|
|
62
|
+
"audio/",
|
|
63
|
+
"video/",
|
|
64
|
+
"application/javascript",
|
|
65
|
+
"text/javascript",
|
|
66
|
+
"application/font",
|
|
67
|
+
"application/manifest",
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
/** The last resort, for a client that announces nothing about itself. */
|
|
71
|
+
const ASSET_EXTENSIONS = new Set([
|
|
72
|
+
"css",
|
|
73
|
+
"js",
|
|
74
|
+
"mjs",
|
|
75
|
+
"map",
|
|
76
|
+
"ico",
|
|
77
|
+
"png",
|
|
78
|
+
"jpg",
|
|
79
|
+
"jpeg",
|
|
80
|
+
"gif",
|
|
81
|
+
"svg",
|
|
82
|
+
"webp",
|
|
83
|
+
"avif",
|
|
84
|
+
"woff",
|
|
85
|
+
"woff2",
|
|
86
|
+
"ttf",
|
|
87
|
+
"otf",
|
|
88
|
+
"eot",
|
|
89
|
+
"mp3",
|
|
90
|
+
"mp4",
|
|
91
|
+
"webm",
|
|
92
|
+
"wasm",
|
|
93
|
+
"webmanifest",
|
|
94
|
+
]);
|
|
95
|
+
|
|
96
|
+
/** Case-insensitive header read — a trace's headers are whatever the client sent. */
|
|
97
|
+
function header(trace: RequestTrace, name: string): string {
|
|
98
|
+
const headers = trace.headers ?? {};
|
|
99
|
+
const hit = Object.keys(headers).find((k) => k.toLowerCase() === name);
|
|
100
|
+
return hit ? String(headers[hit] ?? "").toLowerCase() : "";
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function responseType(trace: RequestTrace): string {
|
|
104
|
+
const headers = trace.responseHeaders ?? {};
|
|
105
|
+
const hit = Object.keys(headers).find((k) => k.toLowerCase() === "content-type");
|
|
106
|
+
return hit ? String(headers[hit] ?? "").toLowerCase() : "";
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Classify one trace. Never throws, and never guesses `asset` without a reason. */
|
|
110
|
+
export function requestKind(trace: RequestTrace): RequestKind {
|
|
111
|
+
// 1. What the browser says it wanted the response for. Unfoolable, and absent
|
|
112
|
+
// only from clients that are not browsers.
|
|
113
|
+
const dest = header(trace, "sec-fetch-dest");
|
|
114
|
+
if (dest === "document" || dest === "iframe" || dest === "frame") return "document";
|
|
115
|
+
if (ASSET_DESTINATIONS.has(dest)) return "asset";
|
|
116
|
+
if (dest === "empty") return "api";
|
|
117
|
+
|
|
118
|
+
// 2. What was actually served. A page is a page whatever asked for it, which
|
|
119
|
+
// is what makes this right for curl and for a server-rendered form post.
|
|
120
|
+
const type = responseType(trace);
|
|
121
|
+
if (type.startsWith("text/html")) return "document";
|
|
122
|
+
if (ASSET_TYPES.some((prefix) => type.startsWith(prefix))) return "asset";
|
|
123
|
+
|
|
124
|
+
// 3. The path, for a client that announced nothing and a response that carried
|
|
125
|
+
// no type — a 404 for a missing stylesheet, most often.
|
|
126
|
+
const extension = (trace.path ?? "").split("?")[0]?.split(".").pop()?.toLowerCase() ?? "";
|
|
127
|
+
if (extension && ASSET_EXTENSIONS.has(extension)) return "asset";
|
|
128
|
+
|
|
129
|
+
// Unclassifiable is `api`, never `asset`. Being wrong here decides whether a
|
|
130
|
+
// request is skipped over, and skipping the wrong one is how the panel stops
|
|
131
|
+
// showing you what you came to see.
|
|
132
|
+
return "api";
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Whether live mode should move the selection onto this trace. */
|
|
136
|
+
export function worthSelecting(trace: RequestTrace): boolean {
|
|
137
|
+
return requestKind(trace) !== "asset";
|
|
138
|
+
}
|
package/src/client/state.ts
CHANGED
|
@@ -11,6 +11,7 @@ import type { RequestTrace, TraceChannelDescriptor } from "../RequestTrace.ts";
|
|
|
11
11
|
import type { EditorName } from "../editor.ts";
|
|
12
12
|
import type { ClientMetric } from "./metrics.ts";
|
|
13
13
|
import { noFacets, traceMatches, type Facets } from "./filter.ts";
|
|
14
|
+
import { worthSelecting } from "./kind.ts";
|
|
14
15
|
import type { ThemeChoice } from "./ui/theme.ts";
|
|
15
16
|
|
|
16
17
|
/**
|
|
@@ -335,11 +336,28 @@ export class Store {
|
|
|
335
336
|
addTrace(trace: RequestTrace): void {
|
|
336
337
|
this.traces.unshift(trace);
|
|
337
338
|
if (this.traces.length > this.capacity) this.traces.length = this.capacity;
|
|
338
|
-
if (this.live) this.selected = trace;
|
|
339
|
-
else this.pending++;
|
|
339
|
+
if (this.live && this._takesSelection(trace)) this.selected = trace;
|
|
340
|
+
else if (!this.live) this.pending++;
|
|
340
341
|
this.changed();
|
|
341
342
|
}
|
|
342
343
|
|
|
344
|
+
/**
|
|
345
|
+
* Whether a newly arrived trace should become the selection in live mode.
|
|
346
|
+
*
|
|
347
|
+
* Everything did, which meant a page selected itself and was then replaced by
|
|
348
|
+
* its own favicon a few milliseconds later. The bar named a request nobody
|
|
349
|
+
* asked about while the page you were looking at scrolled into the list.
|
|
350
|
+
*
|
|
351
|
+
* A sub-resource yields to whatever is selected — but only to something. With
|
|
352
|
+
* nothing selected an asset is still better than an empty panel, and a page
|
|
353
|
+
* whose document 304s while its assets do not would otherwise show nothing at
|
|
354
|
+
* all.
|
|
355
|
+
*/
|
|
356
|
+
private _takesSelection(trace: RequestTrace): boolean {
|
|
357
|
+
if (worthSelecting(trace)) return true;
|
|
358
|
+
return this.selected === null || !worthSelecting(this.selected);
|
|
359
|
+
}
|
|
360
|
+
|
|
343
361
|
clear(): void {
|
|
344
362
|
this.traces = [];
|
|
345
363
|
this.selected = null;
|
package/src/client/tabs/all.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { facetsActive, methodsPresent, noFacets, type Facets } from "../filter.ts";
|
|
10
10
|
import { foldTraceRows, type TraceRow } from "../tree.ts";
|
|
11
|
+
import type { RequestKind } from "../kind.ts";
|
|
11
12
|
import { dCls, esc, fmt, scCls } from "../ui/format.ts";
|
|
12
13
|
import { el, reconcile } from "../ui/render.ts";
|
|
13
14
|
import type { TabContext, TabView } from "./types.ts";
|
|
@@ -36,6 +37,16 @@ const STATUS_CLASSES: Array<[string, string]> = [
|
|
|
36
37
|
["5", "5xx"],
|
|
37
38
|
];
|
|
38
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Labelled for what they are from the reader's side rather than by their code
|
|
42
|
+
* name: "page" is what someone is looking at, "asset" is what came with it.
|
|
43
|
+
*/
|
|
44
|
+
const KIND_CHIPS: Array<[RequestKind, string]> = [
|
|
45
|
+
["document", "page"],
|
|
46
|
+
["api", "api"],
|
|
47
|
+
["asset", "asset"],
|
|
48
|
+
];
|
|
49
|
+
|
|
39
50
|
// ── Skeleton ──────────────────────────────────────────────────────────────────
|
|
40
51
|
//
|
|
41
52
|
// Built once per visit to the tab and then only updated. The filter input in
|
|
@@ -82,11 +93,17 @@ function facetChips(store: TabContext["store"]): string {
|
|
|
82
93
|
chip("status", digit, label, f.statusClasses.includes(digit)),
|
|
83
94
|
).join("");
|
|
84
95
|
|
|
96
|
+
const kinds = KIND_CHIPS.map(([kind, label]) =>
|
|
97
|
+
chip("kind", kind, label, f.kinds?.includes(kind) ?? false),
|
|
98
|
+
).join("");
|
|
99
|
+
|
|
85
100
|
return (
|
|
86
101
|
methods +
|
|
87
102
|
(methods ? `<span class="fsep"></span>` : "") +
|
|
88
103
|
statuses +
|
|
89
104
|
`<span class="fsep"></span>` +
|
|
105
|
+
kinds +
|
|
106
|
+
`<span class="fsep"></span>` +
|
|
90
107
|
chip("errors", "", "errors", f.errors, true) +
|
|
91
108
|
chip("slow", "", "slow", f.slow, true) +
|
|
92
109
|
chip("nplus", "", "n+1", f.nPlusOne, true) +
|
|
@@ -103,6 +120,8 @@ export function toggleFacet(f: Facets, kind: string, value: string): Facets {
|
|
|
103
120
|
return { ...f, methods: drop(f.methods, value) };
|
|
104
121
|
case "status":
|
|
105
122
|
return { ...f, statusClasses: drop(f.statusClasses, value) };
|
|
123
|
+
case "kind":
|
|
124
|
+
return { ...f, kinds: drop(f.kinds ?? [], value as RequestKind) };
|
|
106
125
|
case "errors":
|
|
107
126
|
return { ...f, errors: !f.errors };
|
|
108
127
|
case "slow":
|
package/src/redaction.ts
CHANGED
|
@@ -247,6 +247,8 @@ export function redactBindings(
|
|
|
247
247
|
* `SET col = ?`, and `WHERE col <op> ?` (including `IN (?, ?)`, where every
|
|
248
248
|
* placeholder belongs to the same column). Returns `undefined` at any position it
|
|
249
249
|
* cannot attribute.
|
|
250
|
+
*
|
|
251
|
+
* @internal
|
|
250
252
|
*/
|
|
251
253
|
export function attributeBindings(sql: string, count: number): Array<string | undefined> {
|
|
252
254
|
const out: Array<string | undefined> = new Array(count).fill(undefined);
|
package/src/tracing.ts
CHANGED
|
@@ -99,6 +99,8 @@ const _channels = new Map<string, TraceChannelDescriptor>();
|
|
|
99
99
|
*
|
|
100
100
|
* A `hidden` channel is left out: its entries are still recorded and still reach
|
|
101
101
|
* the panel on the trace, but whatever renders them is not this generic row list.
|
|
102
|
+
*
|
|
103
|
+
* @internal
|
|
102
104
|
*/
|
|
103
105
|
export function traceChannels(): TraceChannelDescriptor[] {
|
|
104
106
|
return [...(_channels.values() as Iterable<TraceChannelDescriptor>)]
|
|
@@ -199,6 +201,7 @@ export interface TraceSink {
|
|
|
199
201
|
// nothing: by the time a panel draws a row, the unredacted copy has already been
|
|
200
202
|
// streamed to the browser and written to `.zerotal/devtools.sqlite`, where it
|
|
201
203
|
// sits for a day. The sink is the last point where "not recorded" is still true.
|
|
204
|
+
/** @internal */
|
|
202
205
|
export const traceSink: TraceSink = {
|
|
203
206
|
channel(descriptor: TraceChannelDescriptor): void {
|
|
204
207
|
_channels.set(descriptor.id, descriptor);
|
|
@@ -261,6 +264,11 @@ function _cleanupBuffers(ctx: object): void {
|
|
|
261
264
|
*/
|
|
262
265
|
const SAFE_HEADERS = new Set([
|
|
263
266
|
"accept",
|
|
267
|
+
// What the browser wanted the response for — `document`, `image`, `style`.
|
|
268
|
+
// The only unfoolable way to tell a page from a file the page pulled in, and
|
|
269
|
+
// devtools classifies every trace by it.
|
|
270
|
+
"sec-fetch-dest",
|
|
271
|
+
"sec-fetch-mode",
|
|
264
272
|
"content-type",
|
|
265
273
|
"content-length",
|
|
266
274
|
"user-agent",
|