Skip to content

Commit 66677af

Browse files
authored
Add browser request coverage artifacts (#1248)
1 parent 020302c commit 66677af

6 files changed

Lines changed: 122 additions & 8 deletions

File tree

packages/runtime-playground/src/browser-actions-runner.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { serializeBrowserError } from "./browser-metrics.js"
1313
import { browserPreviewNetworkPolicyIsActive, browserPreviewNetworkPolicySummary, browserPreviewNeedsContextRouting, browserPreviewTopology, resolveBrowserPreviewUrl, routeBrowserPreviewContextNetwork } from "./browser-preview-routing.js"
1414
import { BROWSER_PROBE_STATE_INIT_SCRIPT, browserProbeReplayability, browserProbeViewport } from "./browser-probe.js"
1515
import { runBrowserProbeCommand, type BrowserProbeRunPlan } from "./browser-probe-runner.js"
16-
import { browserActionTargetUrls, browserAuthRequest, browserProbeWaterfallArtifact, browserRedirectDiagnosticsArtifact, browserStorageStateAuthSummary, browserStorageStateImportFromArgs, browserWordPressDiagnosticsArtifact, createBrowserProbeProgressTracker, fileSha256, installBrowserWordPressDiagnostics, installWordPressAdminAuthCookies, livenessRemainingWallTimeMs, normalizeBrowserProbeScriptCheckpoint, type BrowserCommandProgressEvent, type BrowserStorageStateImport } from "./browser-probe-support.js"
16+
import { browserActionTargetUrls, browserAuthRequest, browserProbeWaterfallArtifact, browserRedirectDiagnosticsArtifact, browserRequestCoverageArtifact, browserStorageStateAuthSummary, browserStorageStateImportFromArgs, browserWordPressDiagnosticsArtifact, createBrowserProbeProgressTracker, fileSha256, installBrowserWordPressDiagnostics, installWordPressAdminAuthCookies, livenessRemainingWallTimeMs, normalizeBrowserProbeScriptCheckpoint, type BrowserCommandProgressEvent, type BrowserStorageStateImport } from "./browser-probe-support.js"
1717
import { positiveIntegerArg } from "./command-args.js"
1818
import { argValue, commaListArg, durationArg, viewportArg } from "./commands.js"
1919
import type { PlaygroundRunResponse } from "./playground-command-errors.js"
@@ -273,6 +273,7 @@ export async function runBrowserActionsCommand({
273273
}
274274
if (capture.has("network")) {
275275
await artifactSession.writeJsonLines("network", "network.jsonl", network)
276+
await artifactSession.writeJson("requestCoverage", "request-coverage.json", browserRequestCoverageArtifact(network, startedAt))
276277
await artifactSession.writeJson("waterfall", "waterfall.json", browserProbeWaterfallArtifact(network, startedAt))
277278
}
278279

@@ -312,9 +313,10 @@ export async function runBrowserActionsCommand({
312313
...(capture.has("steps") ? { steps: "files/browser/steps.jsonl" } : {}),
313314
...(capture.has("console") ? { console: "files/browser/console.jsonl" } : {}),
314315
...(capture.has("errors") ? { errors: "files/browser/errors.jsonl" } : {}),
315-
...(htmlSha256 ? { html: "files/browser/snapshot.html" } : {}),
316-
...(capture.has("network") ? { network: "files/browser/network.jsonl" } : {}),
317-
...(capture.has("network") ? { waterfall: "files/browser/waterfall.json" } : {}),
316+
...(htmlSha256 ? { html: "files/browser/snapshot.html" } : {}),
317+
...(capture.has("network") ? { network: "files/browser/network.jsonl" } : {}),
318+
...(capture.has("network") ? { requestCoverage: "files/browser/request-coverage.json" } : {}),
319+
...(capture.has("network") ? { waterfall: "files/browser/waterfall.json" } : {}),
318320
...(redirectDiagnostics ? { redirectDiagnostics: "files/browser/redirect-diagnostics.json" } : {}),
319321
...(capture.has("screenshot") ? { screenshot: "files/browser/screenshot.png" } : {}),
320322
...(domSnapshots.length > 0 ? { domSnapshots: domSnapshots.map((snapshot) => snapshot.snapshot) } : {}),

packages/runtime-playground/src/browser-artifacts.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ export interface BrowserArtifactFiles {
6666
lifecycle?: string
6767
memory?: string
6868
network?: string
69+
requestCoverage?: string
6970
waterfall?: string
7071
performance?: string
7172
review?: string
@@ -971,6 +972,7 @@ const BROWSER_ARTIFACT_FILE_MANIFEST: Record<keyof BrowserArtifactFiles, Browser
971972
lifecycle: { kind: "browser-lifecycle", contentType: "application/json", redact: true },
972973
memory: { kind: "browser-memory", contentType: "application/json", redact: true },
973974
network: { kind: "browser-network", contentType: "application/x-ndjson", redact: true },
975+
requestCoverage: { kind: "browser-request-coverage", contentType: "application/json", redact: true },
974976
waterfall: { kind: "browser-waterfall", contentType: "application/json", redact: true },
975977
performance: { kind: "browser-performance", contentType: "application/json", redact: true },
976978
review: { kind: "browser-review", contentType: "application/json", redact: true },

packages/runtime-playground/src/browser-probe-runner.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { BROWSER_PROBE_PERFORMANCE_INIT_SCRIPT, BROWSER_PROBE_STATE_INIT_SCRIPT,
1111
import { argValue, commaListArg, durationArg, strictBooleanArg, viewportArg } from "./commands.js"
1212
import type { PlaygroundRunResponse } from "./playground-command-errors.js"
1313
import type { PlaygroundCliServer } from "./preview-server.js"
14-
import { browserAuthRequest, browserProbeWaterfallArtifact, browserRedirectDiagnosticsArtifact, browserStorageStateAuthSummary, browserStorageStateImportFromArgs, createBrowserProbeProgressTracker, fileSha256, installWordPressAdminAuthCookies, now, sha256, withBrowserProbeLiveness, normalizeBrowserProbeScriptCheckpoint, type BrowserCommandProgressEvent, type BrowserProbeScriptCheckpoint, type BrowserStorageStateImport } from "./browser-probe-support.js"
14+
import { browserAuthRequest, browserProbeWaterfallArtifact, browserRedirectDiagnosticsArtifact, browserRequestCoverageArtifact, browserStorageStateAuthSummary, browserStorageStateImportFromArgs, createBrowserProbeProgressTracker, fileSha256, installWordPressAdminAuthCookies, now, sha256, withBrowserProbeLiveness, normalizeBrowserProbeScriptCheckpoint, type BrowserCommandProgressEvent, type BrowserProbeScriptCheckpoint, type BrowserStorageStateImport } from "./browser-probe-support.js"
1515
import { BrowserProbeSessionResultBuilder, browserProbeCaptureSelection } from "./browser-probe-session-result-builder.js"
1616

1717
const BROWSER_PROBE_PROFILE_OVERRIDES = new Set(["browser", "device", "locale", "permissions", "throttle", "timezone", "user-agent", "viewport"])
@@ -488,6 +488,7 @@ export async function runSingleBrowserProbeCommand({
488488
}
489489
if (captureSelection.network) {
490490
await artifactSession.writeJsonLines("network", "network.jsonl", network)
491+
await artifactSession.writeJson("requestCoverage", "request-coverage.json", browserRequestCoverageArtifact(network, startedAt))
491492
await artifactSession.writeJson("waterfall", "waterfall.json", browserProbeWaterfallArtifact(network, startedAt))
492493
}
493494
if (checkpoints.length > 0) {

packages/runtime-playground/src/browser-probe-session-result-builder.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -160,9 +160,10 @@ function browserProbeArtifactFileMap(input: BrowserProbeSessionResultInput): Bro
160160
...(input.capture.has("errors") || input.captureSelection?.errorsForAssertions ? { errors: `${input.browserFilesDirectory}/errors.jsonl` } : {}),
161161
...(input.hashes.htmlSha256 ? { html: `${input.browserFilesDirectory}/snapshot.html` } : {}),
162162
...(input.lifecycleArtifact ? { lifecycle: `${input.browserFilesDirectory}/lifecycle.json` } : {}),
163-
...(input.memoryArtifact ? { memory: `${input.browserFilesDirectory}/memory.json` } : {}),
164-
...(input.capture.has("network") || input.captureSelection?.networkForAssertions ? { network: `${input.browserFilesDirectory}/network.jsonl` } : {}),
165-
...(input.capture.has("network") || input.captureSelection?.networkForAssertions ? { waterfall: `${input.browserFilesDirectory}/waterfall.json` } : {}),
163+
...(input.memoryArtifact ? { memory: `${input.browserFilesDirectory}/memory.json` } : {}),
164+
...(input.capture.has("network") || input.captureSelection?.networkForAssertions ? { network: `${input.browserFilesDirectory}/network.jsonl` } : {}),
165+
...(input.capture.has("network") || input.captureSelection?.networkForAssertions ? { requestCoverage: `${input.browserFilesDirectory}/request-coverage.json` } : {}),
166+
...(input.capture.has("network") || input.captureSelection?.networkForAssertions ? { waterfall: `${input.browserFilesDirectory}/waterfall.json` } : {}),
166167
...(input.performanceArtifact ? { performance: `${input.browserFilesDirectory}/performance.json` } : {}),
167168
...(input.redirectDiagnostics ? { redirectDiagnostics: `${input.browserFilesDirectory}/redirect-diagnostics.json` } : {}),
168169
review: `${input.browserFilesDirectory}/review.json`,

packages/runtime-playground/src/browser-probe-support.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,80 @@ export interface BrowserProbeScriptCheckpoint {
2929
timestamp: string
3030
}
3131

32+
export interface BrowserRequestCoverageArtifact {
33+
schema: "wp-codebox/browser-request-coverage/v1"
34+
version: 1
35+
capturedAt: string
36+
startedAt: string
37+
totals: {
38+
requests: number
39+
responses: number
40+
failures: number
41+
hosts: number
42+
resourceTypes: number
43+
methods: number
44+
transferSizeBytes: number
45+
responseBodySizeBytes: number
46+
}
47+
byHost: Record<string, BrowserProbeNetworkCountSummary>
48+
byResourceType: Record<string, BrowserProbeNetworkCountSummary>
49+
byMethod: Record<string, BrowserProbeNetworkCountSummary>
50+
requests: Array<{
51+
type: BrowserProbeNetworkRecord["type"]
52+
method: string
53+
url: string
54+
host: string
55+
resourceType?: string
56+
status?: number
57+
ok?: boolean
58+
transferSize?: number
59+
responseBodySize?: number
60+
timestamp: string
61+
}>
62+
}
63+
64+
export function browserRequestCoverageArtifact(network: BrowserProbeNetworkRecord[], startedAt: string): BrowserRequestCoverageArtifact {
65+
const byHost: Record<string, BrowserProbeNetworkCountSummary> = {}
66+
const byResourceType: Record<string, BrowserProbeNetworkCountSummary> = {}
67+
const byMethod: Record<string, BrowserProbeNetworkCountSummary> = {}
68+
for (const record of network) {
69+
addBrowserProbeNetworkCount(byHost, requestHost(record.url) || "unknown", record)
70+
addBrowserProbeNetworkCount(byResourceType, record.resourceType || "unknown", record)
71+
addBrowserProbeNetworkCount(byMethod, record.method || "GET", record)
72+
}
73+
return {
74+
schema: "wp-codebox/browser-request-coverage/v1",
75+
version: 1,
76+
capturedAt: now(),
77+
startedAt,
78+
totals: {
79+
requests: network.length,
80+
responses: network.filter((record) => record.type === "response").length,
81+
failures: network.filter((record) => record.type === "requestfailed").length,
82+
hosts: Object.keys(byHost).length,
83+
resourceTypes: Object.keys(byResourceType).length,
84+
methods: Object.keys(byMethod).length,
85+
transferSizeBytes: network.reduce((total, record) => total + finiteNumber(record.transferSize, 0), 0),
86+
responseBodySizeBytes: network.reduce((total, record) => total + finiteNumber(record.responseBodySize, 0), 0),
87+
},
88+
byHost: sortBrowserProbeNetworkCounts(byHost),
89+
byResourceType: sortBrowserProbeNetworkCounts(byResourceType),
90+
byMethod: sortBrowserProbeNetworkCounts(byMethod),
91+
requests: network.map((record) => ({
92+
type: record.type,
93+
method: record.method,
94+
url: redactBrowserArtifactUrl(record.url),
95+
host: requestHost(record.url) || "unknown",
96+
resourceType: record.resourceType,
97+
status: record.status,
98+
ok: record.ok,
99+
transferSize: record.transferSize,
100+
responseBodySize: record.responseBodySize,
101+
timestamp: record.timestamp,
102+
})),
103+
}
104+
}
105+
32106
export function browserProbeWaterfallArtifact(network: BrowserProbeNetworkRecord[], startedAt: string): BrowserProbeWaterfallArtifact {
33107
return {
34108
schema: "wp-codebox/browser-waterfall/v1",

tests/browser-artifact-session.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { resolve } from "node:path"
33

44
import { BrowserArtifactSession } from "../packages/runtime-playground/src/browser-artifact-session.js"
55
import { browserReviewSummary, type BrowserArtifact } from "../packages/runtime-playground/src/browser-artifacts.js"
6+
import { browserRequestCoverageArtifact } from "../packages/runtime-playground/src/browser-probe-support.js"
67
import { assertJsonFile, assertTextFile, withTempDir } from "../scripts/test-kit.js"
78

89
await withTempDir("wp-codebox-browser-artifact-session-", async (artifactRoot) => {
@@ -13,6 +14,7 @@ assert.equal(session.path("/tmp/snapshot.html"), "files/browser/snapshot.html")
1314

1415
await session.writeText("html", "snapshot.html", "<html><body>secret</body></html>")
1516
await session.writeJsonLines("console", "console.jsonl", [{ type: "log", text: "visible" }])
17+
await session.writeJson("requestCoverage", "request-coverage.json", { schema: "wp-codebox/browser-request-coverage/v1", totals: { requests: 1 } })
1618
await session.writeJson("waterfall", "waterfall.json", { schema: "wp-codebox/browser-waterfall/v1", log: { entries: [] } })
1719
await session.writeJson("summary", "summary.json", { schema: "wp-codebox/browser-probe/v1", ok: true })
1820
await session.writeBuffer("screenshot", "screenshot.png", Buffer.from([0, 1, 2]))
@@ -36,6 +38,10 @@ assert.equal(files.get("files/browser/console.jsonl")?.kind, "browser-console")
3638
assert.equal(files.get("files/browser/console.jsonl")?.contentType, "application/x-ndjson")
3739
assert.equal(files.get("files/browser/console.jsonl")?.redaction?.policy, "required")
3840

41+
assert.equal(files.get("files/browser/request-coverage.json")?.kind, "browser-request-coverage")
42+
assert.equal(files.get("files/browser/request-coverage.json")?.contentType, "application/json")
43+
assert.equal(files.get("files/browser/request-coverage.json")?.redaction?.policy, "required")
44+
3945
assert.equal(files.get("files/browser/waterfall.json")?.kind, "browser-waterfall")
4046
assert.equal(files.get("files/browser/waterfall.json")?.contentType, "application/json")
4147
assert.equal(files.get("files/browser/waterfall.json")?.redaction?.policy, "required")
@@ -65,3 +71,31 @@ const review = browserReviewSummary([{
6571
} satisfies BrowserArtifact])
6672
assert.equal(review?.probes[0]?.waterfall, "files/browser/waterfall.json")
6773
})
74+
75+
const requestCoverage = browserRequestCoverageArtifact([{
76+
type: "response",
77+
method: "GET",
78+
url: "https://example.test/wp-json/wp/v2/posts?search=secret#hash",
79+
resourceType: "fetch",
80+
status: 200,
81+
ok: true,
82+
transferSize: 120,
83+
responseBodySize: 80,
84+
timestamp: "2026-01-01T00:00:01.000Z",
85+
}, {
86+
type: "requestfailed",
87+
method: "POST",
88+
url: "https://api.example.test/submit?token=secret",
89+
resourceType: "xhr",
90+
timestamp: "2026-01-01T00:00:02.000Z",
91+
}], "2026-01-01T00:00:00.000Z")
92+
93+
assert.equal(requestCoverage.schema, "wp-codebox/browser-request-coverage/v1")
94+
assert.equal(requestCoverage.totals.requests, 2)
95+
assert.equal(requestCoverage.totals.responses, 1)
96+
assert.equal(requestCoverage.totals.failures, 1)
97+
assert.equal(requestCoverage.totals.hosts, 2)
98+
assert.equal(requestCoverage.byResourceType.fetch.responses, 1)
99+
assert.equal(requestCoverage.byMethod.POST.failures, 1)
100+
assert.equal(requestCoverage.requests[0].url, "https://example.test/wp-json/wp/v2/posts?search=[redacted]#[redacted]")
101+
assert.equal(requestCoverage.requests[1].url, "https://api.example.test/submit?token=[redacted]")

0 commit comments

Comments
 (0)