44// everywhere), per-step screenshots, and a failure screenshot. The scenario
55// drives `page` directly; assertions are vitest's job.
66import { execFile } from "node:child_process" ;
7- import { copyFileSync , mkdirSync , rmSync , writeFileSync } from "node:fs" ;
7+ import { copyFileSync , mkdirSync , rmSync } from "node:fs" ;
88import { join } from "node:path" ;
99import { promisify } from "node:util" ;
1010
1111import { Effect } from "effect" ;
1212import { chromium , type Page } from "playwright" ;
1313
1414import { markFocus , markRecordingStart } from "../timeline" ;
15+ import { appendTraces , type TraceEntry } from "../trace-harvest" ;
1516import type { Identity , Target } from "../target" ;
1617
1718export interface BrowserSession {
@@ -60,10 +61,17 @@ export const makeBrowserSurface = (dir: string, target: Target): BrowserSurface
6061 recordVideo : { dir : videoTmp , size : { width : 1280 , height : 800 } } ,
6162 baseURL : target . baseUrl ,
6263 } ) ;
63- await context . tracing . start ( { screenshots : true , snapshots : true , sources : true } ) ;
64+ await context . tracing . start ( {
65+ screenshots : true ,
66+ snapshots : true ,
67+ sources : true ,
68+ } ) ;
6469 if ( identity . cookies ?. length ) {
6570 await context . addCookies (
66- identity . cookies . map ( ( cookie ) => ( { ...cookie , url : target . baseUrl } ) ) ,
71+ identity . cookies . map ( ( cookie ) => ( {
72+ ...cookie ,
73+ url : target . baseUrl ,
74+ } ) ) ,
6775 ) ;
6876 }
6977 const page = await context . newPage ( ) ;
@@ -73,17 +81,48 @@ export const makeBrowserSurface = (dir: string, target: Target): BrowserSurface
7381 // Harvest distributed-trace ids: every app API request carries a W3C
7482 // traceparent (Effect's HttpClient), and each id names one
7583 // click→server→DB trace in whatever OTLP store the run exported to
76- // (motel locally). Written to traces.json so the runs viewer can
77- // link a recording to its traces.
78- const traceIds : { id : string ; at : number ; url : string } [ ] = [ ] ;
84+ // (motel locally). Appended to traces.json (shared with the MCP
85+ // surface's terminal-side entries) so the runs viewer can link a
86+ // recording to its traces. Duration comes from the finished/failed
87+ // event so the viewer can answer "why did that take so long"
88+ // without leaving the run page.
89+ const traceIds : Array < TraceEntry & { ms ?: number ; status ?: number } > = [ ] ;
90+ const inflight = new Map < unknown , ( typeof traceIds ) [ number ] > ( ) ;
7991 page . on ( "request" , ( request ) => {
8092 const traceparent = request . headers ( ) [ "traceparent" ] ;
8193 const match = traceparent ? / ^ [ 0 - 9 a - f ] { 2 } - ( [ 0 - 9 a - f ] { 32 } ) - / . exec ( traceparent ) : null ;
8294 if ( match ?. [ 1 ] ) {
83- traceIds . push ( { id : match [ 1 ] , at : Date . now ( ) , url : request . url ( ) } ) ;
95+ const entry : ( typeof traceIds ) [ number ] = {
96+ id : match [ 1 ] ,
97+ at : Date . now ( ) ,
98+ url : request . url ( ) ,
99+ source : "browser" ,
100+ } ;
101+ traceIds . push ( entry ) ;
102+ inflight . set ( request , entry ) ;
84103 }
85104 } ) ;
86- return { browser, context, page, videoTmp, shots : { count : 0 } , traceIds } ;
105+ page . on ( "requestfinished" , async ( request ) => {
106+ const entry = inflight . get ( request ) ;
107+ if ( ! entry ) return ;
108+ inflight . delete ( request ) ;
109+ entry . ms = Date . now ( ) - entry . at ;
110+ entry . status = ( await request . response ( ) . catch ( ( ) => null ) ) ?. status ( ) ;
111+ } ) ;
112+ page . on ( "requestfailed" , ( request ) => {
113+ const entry = inflight . get ( request ) ;
114+ if ( ! entry ) return ;
115+ inflight . delete ( request ) ;
116+ entry . ms = Date . now ( ) - entry . at ;
117+ } ) ;
118+ return {
119+ browser,
120+ context,
121+ page,
122+ videoTmp,
123+ shots : { count : 0 } ,
124+ traceIds,
125+ } ;
87126 } ) ,
88127 ( { page, context, shots } ) =>
89128 Effect . promise ( async ( ) => {
@@ -110,9 +149,7 @@ export const makeBrowserSurface = (dir: string, target: Target): BrowserSurface
110149 } ) ,
111150 ( { browser, context, page, videoTmp, traceIds } ) =>
112151 Effect . promise ( async ( ) => {
113- if ( traceIds . length > 0 ) {
114- writeFileSync ( join ( dir , "traces.json" ) , JSON . stringify ( traceIds , null , 1 ) ) ;
115- }
152+ appendTraces ( dir , traceIds ) ;
116153 await context . tracing . stop ( { path : join ( dir , "trace.zip" ) } ) . catch ( ( ) => { } ) ;
117154 const video = page . video ( ) ;
118155 await context . close ( ) ; // flushes the recording
0 commit comments