Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 78 additions & 3 deletions packages/runtime-playground/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -802,9 +802,22 @@ interface BrowserProbeNetworkRecord {
statusText?: string
ok?: boolean
contentType?: string | null
timing?: Record<string, number>
sizes?: BrowserProbeNetworkSizes
transferSize?: number
bodySize?: number
requestBodySize?: number
responseBodySize?: number
failure?: ReturnType<Request["failure"]>
}

interface BrowserProbeNetworkSizes {
requestBodySize: number
requestHeadersSize: number
responseBodySize: number
responseHeadersSize: number
}

interface ThemeCheckArtifact {
theme: string
files: {
Expand Down Expand Up @@ -1385,6 +1398,7 @@ class PlaygroundRuntime implements Runtime {
const consoleMessages: Record<string, unknown>[] = []
const errors: BrowserProbeErrorRecord[] = []
const network: BrowserProbeNetworkRecord[] = []
const networkTasks: Array<Promise<void>> = []
const checkpoints: BrowserProbeCheckpointRecord[] = []
const consolePath = join(browserDirectory, "console.jsonl")
const checkpointsPath = join(browserDirectory, "checkpoints.jsonl")
Expand Down Expand Up @@ -1420,7 +1434,12 @@ class PlaygroundRuntime implements Runtime {
page.on("pageerror", (error) => errors.push(serializeBrowserError("pageerror", error)))
}
if (capture.has("network")) {
page.on("response", (response) => network.push(serializeBrowserResponse(response)))
page.on("requestfinished", (request) => {
const task = serializeBrowserFinishedRequest(request).then((record) => {
network.push(record)
}).catch(() => undefined)
networkTasks.push(task)
})
page.on("requestfailed", (request) => network.push(serializeBrowserRequestFailure(request)))
}

Expand Down Expand Up @@ -1472,6 +1491,9 @@ class PlaygroundRuntime implements Runtime {
screenshotSha256 = await fileSha256(screenshotPath)
}
}
if (networkTasks.length > 0) {
await Promise.all(networkTasks)
}
await browser.close()
if (capture.has("console")) {
await writeFile(consolePath, jsonLines(consoleMessages))
Expand Down Expand Up @@ -1601,6 +1623,7 @@ class PlaygroundRuntime implements Runtime {
const consoleMessages: Record<string, unknown>[] = []
const errors: BrowserProbeErrorRecord[] = []
const network: BrowserProbeNetworkRecord[] = []
const networkTasks: Array<Promise<void>> = []
const stepsPath = join(browserDirectory, "steps.jsonl")
const consolePath = join(browserDirectory, "console.jsonl")
const errorsPath = join(browserDirectory, "errors.jsonl")
Expand Down Expand Up @@ -1629,7 +1652,12 @@ class PlaygroundRuntime implements Runtime {
page.on("pageerror", (error) => errors.push(serializeBrowserError("pageerror", error)))
}
if (capture.has("network")) {
page.on("response", (response) => network.push(serializeBrowserResponse(response)))
page.on("requestfinished", (request) => {
const task = serializeBrowserFinishedRequest(request).then((record) => {
network.push(record)
}).catch(() => undefined)
networkTasks.push(task)
})
page.on("requestfailed", (request) => network.push(serializeBrowserRequestFailure(request)))
}

Expand Down Expand Up @@ -1680,6 +1708,9 @@ class PlaygroundRuntime implements Runtime {
screenshotSha256 = await fileSha256(screenshotPath)
}
} finally {
if (networkTasks.length > 0) {
await Promise.all(networkTasks)
}
await browser.close()
if (capture.has("steps")) {
await writeFile(stepsPath, jsonLines(stepRecords))
Expand Down Expand Up @@ -3596,8 +3627,26 @@ function sumMetric(metricSets: Array<Record<string, number>>, name: string): num
return metricSets.reduce((total, metrics) => total + (metrics[name] ?? 0), 0)
}

function serializeBrowserResponse(response: Response): BrowserProbeNetworkRecord {
async function serializeBrowserFinishedRequest(request: Request): Promise<BrowserProbeNetworkRecord> {
const response = await request.response()
if (!response) {
return {
type: "response",
url: request.url(),
method: request.method(),
resourceType: request.resourceType(),
timestamp: now(),
timing: browserRequestTiming(request),
}
}

return serializeBrowserResponse(response)
}

async function serializeBrowserResponse(response: Response): Promise<BrowserProbeNetworkRecord> {
const request = response.request()
const sizes = await browserRequestSizes(request)
const transferSize = sizes ? sizes.responseHeadersSize + sizes.responseBodySize : undefined
return {
type: "response",
url: response.url(),
Expand All @@ -3607,6 +3656,12 @@ function serializeBrowserResponse(response: Response): BrowserProbeNetworkRecord
statusText: response.statusText(),
ok: response.ok(),
contentType: response.headers()["content-type"] ?? null,
timing: browserRequestTiming(request),
...(sizes ? { sizes } : {}),
...(typeof transferSize === "number" ? { transferSize } : {}),
...(sizes ? { bodySize: sizes.responseBodySize } : {}),
...(sizes ? { requestBodySize: sizes.requestBodySize } : {}),
...(sizes ? { responseBodySize: sizes.responseBodySize } : {}),
timestamp: now(),
}
}
Expand All @@ -3617,11 +3672,31 @@ function serializeBrowserRequestFailure(request: Request): BrowserProbeNetworkRe
url: request.url(),
method: request.method(),
resourceType: request.resourceType(),
timing: browserRequestTiming(request),
failure: request.failure(),
timestamp: now(),
}
}

function browserRequestTiming(request: Request): Record<string, number> {
return Object.fromEntries(
Object.entries(request.timing()).filter((entry): entry is [string, number] => typeof entry[1] === "number" && Number.isFinite(entry[1])),
)
}

async function browserRequestSizes(request: Request): Promise<BrowserProbeNetworkSizes | undefined> {
const maybeSizedRequest = request as Request & { sizes?: () => Promise<BrowserProbeNetworkSizes> }
if (typeof maybeSizedRequest.sizes !== "function") {
return undefined
}

try {
return await maybeSizedRequest.sizes()
} catch {
return undefined
}
}

async function fileSha256(path: string): Promise<string> {
return sha256(await readFile(path))
}
Expand Down
18 changes: 18 additions & 0 deletions scripts/browser-probe-artifact-smoke.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,24 @@ assert.match(networkLog, /"type":"response"/)
assert.match(checkpointsLog, /"schema":"wp-codebox\/browser-checkpoint\/v1"/)
assert.match(checkpointsLog, /"name":"fixture-before-return"/)

const networkEvents = networkLog.trim().split("\n").filter(Boolean).map((line) => JSON.parse(line)) as Array<{
type: string
timing?: { startTime?: number }
sizes?: { responseBodySize?: number; responseHeadersSize?: number }
transferSize?: number
bodySize?: number
responseBodySize?: number
}>
const responseEvent = networkEvents.find((event) => event.type === "response")
assert.ok(responseEvent, "network log should include a response event")
assert.equal(typeof responseEvent.timing?.startTime, "number", "network response should include Playwright request timing")
assert.equal(typeof responseEvent.sizes?.responseHeadersSize, "number", "network response should include Playwright response header size")
assert.equal(typeof responseEvent.sizes?.responseBodySize, "number", "network response should include Playwright response body size")
assert.equal(typeof responseEvent.transferSize, "number", "network response should include generic transfer size")
assert.equal(typeof responseEvent.bodySize, "number", "network response should include generic body size")
assert.equal(typeof responseEvent.responseBodySize, "number", "network response should include direct response body size")
assert.equal(responseEvent.bodySize, responseEvent.responseBodySize, "generic body size should mirror response body size")

const memory = JSON.parse(await readFile(memoryPath, "utf8")) as { schema: string; final: { domCounters: { nodes: number | null } }; peak: { domNodes: { final: number | null; peak: number | null } }; checkpoints: unknown[] }
const performance = JSON.parse(await readFile(performancePath, "utf8")) as { schema: string; final: { resources: { count: number }; dom: { nodes: number } }; peak: { resources: number; domNodes: { final: number | null; peak: number | null } }; checkpoints: unknown[] }
assert.equal(memory.schema, "wp-codebox/browser-memory/v1")
Expand Down