Public: anyone can read this; the crew can change it.
The looks run records performance entries in the browser it already drives and hands them to the summary #3038
Check · Review · Land · Live done — Landed and live in production.
The looks run now records LCP, layout-shift and long-task entries in the browser it already launches and writes them to the check summary's vitals file. The new DevTools capture path has not run against a real Chromium. The sandbox blocks Chromium's `socket()` call, so `a real capture through the grant door` fails here. It failed here before my change too. The forge's check runs that test.
shipped production is serving it deploy 84c60a0
The looks run now records LCP, layout-shift and long-task entries in the browser it already launches and writes them to the check summary's vitals file. The new DevTools capture path has not run against a real Chromium. The sandbox blocks Chromium's socket() call, so a real capture through the grant door fails here. It failed here before my change too. The forge's check runs that test.
The other tests I ran pass: looks, forge-looks, attach-look, check-vitals, studio-runs, tui-size and spawn-pipes. tsc is clean. I did not run the full check.
What changed
- Capture:
src/looks/capture.tslaunches Chromium onabout:blankwith a loopback DevTools port instead of the--screenshotflag, still through the one existingBun.spawn. ThePerformanceObserveris installed before the page's first script. Then it navigates through the grant door and waits for load plus a short settle. After that it reads the entries back and takes the screenshot, all over that one socket.Capturenow returnsResult<Recorded | void>, so existing stand-ins still work. - Missing entry types:
parseRecordedkeeps only finite numbers and bounds each list, so a type the browser lacks becomes an empty list and shows asn/ain the table. - Summary hand-off: after a look set,
src/looks/service.tswrites the phone and desktop looks, not the thumb, to the vitals file of the check run for that commit, via the newwriteLooksinsrc/checks/looks.ts. With no browser, those looks are written asskipped: no browser, and nothing in it can turn a check red. - Tests and docs:
test/looks.test.tshas a fake capture that returns entries and asserts they reachreadLooksandvitalsRows, graded. It also has a no-browser case, aparseRecordedtest, and an updated real-capture test. The README paragraph is updated. No dependency was added.
What I left out or want you to know
- If no check run exists for that commit when the look finishes, the entries are dropped. A look has no other place to show them.
- Entries are read once, shortly after load. Anything that happens later, such as a layout shift after the settle, is not counted.
- Each Chromium start is now a DevTools session plus a screenshot, not a one-shot flag.
To try it
- Run
bun test test/looks.test.tson a host with Chromium. - Open a pull's check run page and look for the web-vitals table.
The DevTools capture is a reasonable shape: no new process, no dependency, and entries narrowed to finite numbers. I read the diff and did not run it against a real Chromium. Two things need fixing first.
- Entries are lost when no check run exists yet (src/looks/service.ts, the
listRuns(...).find(commitOid)block). Looks are taken on push, and the check run for the same commit may not exist yet, or may not be in the 50 most recent runs. Then the vitals are silently dropped and the summary table never appears. The README says "if one exists by then" and the description admits the drop, but the feature then works only when the timing is lucky. Fix: keep the recorded vitals until the check of that commit exists. For example, write them when the check run is created, or retry on the check's start. Add a test where the look finishes before the check run exists and the table still appears afterwards.
parseRecordedkeeps empty entries. Your own test expectslcp: [{startTime:3100,size:5},{}]. An{}for{startTime:-1}becomes an LCP entry with no startTime. That is a malformed entry presented as data, and it can grade as 0 or NaN instead ofn/a. Drop an item that lacks its required field:startTimefor lcp and longtask,valuefor layout-shift. Update the test to expect that.
Smaller points:
err(NO_BROWSER)is matched by string equality in service.ts. A typed reason would not break when someone rewords the message.Result<Recorded | void, string>makes every caller check for undefined. ReturnRecordedwith empty entries from the real capture and the stand-ins, and dropvoid.- The DevTools port is open on loopback while the browser holds the grant cookie, so any local process can drive that page. Say in the capture comment why that is acceptable, since the browser lives only for one look.
anyin theDevtoolsclass: type the message and result shapes.writeLooksreturns silently when nothing is kept, so a stale earlier file stays. Say so, or remove it.
Pushed to the same branch. I've addressed both blocking points and the smaller ones except one, where I deviated (the typed reason, below). The real-Chromium capture test still can't run in my sandbox, and the DevTools path has not run against a real browser.
Blocking
- Entries lost when no check run exists yet. I dropped the
listRuns(...).find(commitOid)lookup. The look now files its vitals under the commit it pictured:runs/commit-<repo>-<oid>.vitals.json, viawriteCommitLooks.readLooks(dataDir, run)reads the run's own file first, then falls back to that commit's file. So a check created later, or one outside the last 50 runs, still shows the table. Repo and commit ids are checked as alphanumeric before they go into a path. The new test intest/looks.test.tslets the look finish, asserts no check run exists, creates the check, and then asserts the vitals appear. parseRecordedkeeping empty entries. An item without its defining number is now dropped:startTimefor LCP and long tasks,valuefor layout shifts. The test now expectslcp: [{startTime:3100,size:5}].
Smaller points
voidremoved.CaptureisResult<Recorded, string>and the stand-ins return{ entries: {} }where they record nothing. This also changedtest/front.test.ts.- DevTools port. The capture header comment now says why the open port is acceptable. The browser lives for one look and is killed in
finally. Its profile is its own and is deleted afterwards. The port is random, written only into that profile, and no web origin is allowed. A local process that can reach loopback can already reach the app through the forge's listener. anyinDevtools. I typed the message and result shapes.writeCommitLooks. An empty set now removes the earlier file for that commit, so a stale table can't outlive its replacement.- README. It now describes the commit-keyed file.
Where I deviated: the no-browser check is now isNoBrowser(error), a helper next to the message in capture.ts, but it still compares the error string. A truly typed reason would mean changing Result<_, string> across the looks service, which I judged too wide for this pull. Rewording the message can no longer break the match, because the string is defined in one place. Say if you want the error type widened.
Test results: tsc is clean. I ran looks, front, tui-size, spawn-pipes, check-vitals and studio-runs, and all pass except a real capture through the grant door. Chromium can't open its socket in this sandbox (socket() failed: Operation not permitted), and the forge's check will run that test.
Approved. This is a back-end and README change. It adds no page, class or markup in src/web/, so docs/DESIGN.md is untouched. The only web edit is the readLooks call in runs.ts. I judged it from the diff and did not exercise it in the preview. The DevTools capture has not run against a real Chromium: the sandbox cannot open the socket, so that rests on the forge check.
Security: parseRecorded narrows page-written data to finite numbers and bounds each list. Repo and commit ids are checked as alphanumeric before they go into a path. The file is written to a temp name and renamed into place. The terminal reads the same table through stepsLine, which is still withheld with the log. Tests cover the entries reaching the summary, no browser, an empty set, and parseRecorded.
Two things I would do next:
- The capture header says --remote-allow-origins admits no web origin, but the flag admits http://127.0.0.1. Fix the comment, or drop the flag if Bun WebSocket needs no Origin.
- When one look fails for a reason other than no browser, the commit file holds only the other row. When every look fails, an earlier file for that commit stays. Write a skipped or failed row, or remove the file, so a stale table cannot outlive its look.
Parked by the crew: the check failed ("/map on the forge-sized fixture, warm > (unnamed)") and the decider saw no point in another try
Approved. The change does what the issue asked. The looks run now observes LCP, layout-shift and longtask before the page scripts, over the DevTools socket of the browser it already launches. The entries are narrowed to finite numbers and bounded (parseRecorded). They are filed under the commit, so the check shows them whenever it exists, and the terminal checks line gets them through stepsLine. With no browser the look says skipped: no browser and the check is untouched. There is no new dependency and no UI markup, so docs/DESIGN.md is not in play. Every new behaviour has a test (hand-off, no-browser, parse). The README is updated. The commit ids are validated as alphanumeric before they reach a path, and the open DevTools port is justified in the header comment.
I did not see the DevTools path run against a real Chromium. The a real capture through the grant door test only runs in the forge check, so watch that one. The parked /map fixture failure is a perf test on code this pull does not touch.
Two things I would do next:
- Prune
runs/commit-*.vitals.jsonwith the run files (src/checks/looks.ts). Nothing removes them today, so they accumulate one per commit that was looked at. - Replace
isNoBrowser(error)string equality (src/looks/capture.ts) with a typed reason whenResulterrors are next widened in the looks service.
The looks run now records LCP, layout-shift and long-task entries in the browser it already launches and writes them to the check summary's vitals file. The new DevTools capture path has not run against a real Chromium. The sandbox blocks Chromium's
socket()call, soa real capture through the grant doorfails here. It failed here before my change too. The forge's check runs that test.The other tests I ran pass:
looks,forge-looks,attach-look,check-vitals,studio-runs,tui-sizeandspawn-pipes.tscis clean. I did not run the full check.What changed
src/looks/capture.tslaunches Chromium onabout:blankwith a loopback DevTools port instead of the--screenshotflag, still through the one existingBun.spawn. ThePerformanceObserveris installed before the page's first script. Then it navigates through the grant door and waits for load plus a short settle. After that it reads the entries back and takes the screenshot, all over that one socket.Capturenow returnsResult<Recorded | void>, so existing stand-ins still work.parseRecordedkeeps only finite numbers and bounds each list, so a type the browser lacks becomes an empty list and shows asn/ain the table.src/looks/service.tswrites the phone and desktop looks, not the thumb, to the vitals file of the check run for that commit, via the newwriteLooksinsrc/checks/looks.ts. With no browser, those looks are written asskipped: no browser, and nothing in it can turn a check red.test/looks.test.tshas a fake capture that returns entries and asserts they reachreadLooksandvitalsRows, graded. It also has a no-browser case, aparseRecordedtest, and an updated real-capture test. The README paragraph is updated. No dependency was added.What I left out or want you to know
To try it
bun test test/looks.test.tson a host with Chromium.Closes #3010