@@ -79,6 +79,7 @@ export class CloudTraceSink extends TraceSink {
7979 private traceFileSizeBytes : number = 0 ;
8080 private screenshotTotalSizeBytes : number = 0 ;
8181 private screenshotCount : number = 0 ; // Track number of screenshots extracted
82+ private indexFileSizeBytes : number = 0 ; // Track index file size
8283
8384 // Upload success flag
8485 private uploadSuccessful : boolean = false ;
@@ -332,7 +333,164 @@ export class CloudTraceSink extends TraceSink {
332333 }
333334
334335 /**
335- * Call /v1/traces/complete to report file sizes to gateway.
336+ * Infer final status from trace events by reading the trace file.
337+ * @returns Final status: "success", "failure", "partial", or "unknown"
338+ */
339+ private _inferFinalStatusFromTrace ( ) : string {
340+ try {
341+ // Read trace file to analyze events
342+ const traceContent = fs . readFileSync ( this . tempFilePath , 'utf-8' ) ;
343+ const lines = traceContent . split ( '\n' ) . filter ( line => line . trim ( ) ) ;
344+ const events : any [ ] = [ ] ;
345+
346+ for ( const line of lines ) {
347+ try {
348+ const event = JSON . parse ( line ) ;
349+ events . push ( event ) ;
350+ } catch {
351+ continue ;
352+ }
353+ }
354+
355+ if ( events . length === 0 ) {
356+ return 'unknown' ;
357+ }
358+
359+ // Check for run_end event with status
360+ for ( let i = events . length - 1 ; i >= 0 ; i -- ) {
361+ const event = events [ i ] ;
362+ if ( event . type === 'run_end' ) {
363+ const status = event . data ?. status ;
364+ if ( [ 'success' , 'failure' , 'partial' , 'unknown' ] . includes ( status ) ) {
365+ return status ;
366+ }
367+ }
368+ }
369+
370+ // Infer from error events
371+ const hasErrors = events . some ( e => e . type === 'error' ) ;
372+ if ( hasErrors ) {
373+ // Check if there are successful steps too (partial success)
374+ const stepEnds = events . filter ( e => e . type === 'step_end' ) ;
375+ if ( stepEnds . length > 0 ) {
376+ return 'partial' ;
377+ }
378+ return 'failure' ;
379+ }
380+
381+ // If we have step_end events and no errors, likely success
382+ const stepEnds = events . filter ( e => e . type === 'step_end' ) ;
383+ if ( stepEnds . length > 0 ) {
384+ return 'success' ;
385+ }
386+
387+ return 'unknown' ;
388+ } catch {
389+ // If we can't read the trace, default to unknown
390+ return 'unknown' ;
391+ }
392+ }
393+
394+ /**
395+ * Extract execution statistics from trace file.
396+ * @returns Dictionary with stats fields for /v1/traces/complete
397+ */
398+ private _extractStatsFromTrace ( ) : Record < string , any > {
399+ try {
400+ // Read trace file to extract stats
401+ const traceContent = fs . readFileSync ( this . tempFilePath , 'utf-8' ) ;
402+ const lines = traceContent . split ( '\n' ) . filter ( line => line . trim ( ) ) ;
403+ const events : any [ ] = [ ] ;
404+
405+ for ( const line of lines ) {
406+ try {
407+ const event = JSON . parse ( line ) ;
408+ events . push ( event ) ;
409+ } catch {
410+ continue ;
411+ }
412+ }
413+
414+ if ( events . length === 0 ) {
415+ return {
416+ total_steps : 0 ,
417+ total_events : 0 ,
418+ duration_ms : null ,
419+ final_status : 'unknown' ,
420+ started_at : null ,
421+ ended_at : null ,
422+ } ;
423+ }
424+
425+ // Find run_start and run_end events
426+ const runStart = events . find ( e => e . type === 'run_start' ) ;
427+ const runEnd = events . find ( e => e . type === 'run_end' ) ;
428+
429+ // Extract timestamps
430+ const startedAt = runStart ?. ts || null ;
431+ const endedAt = runEnd ?. ts || null ;
432+
433+ // Calculate duration
434+ let durationMs : number | null = null ;
435+ if ( startedAt && endedAt ) {
436+ try {
437+ const startDt = new Date ( startedAt ) ;
438+ const endDt = new Date ( endedAt ) ;
439+ durationMs = endDt . getTime ( ) - startDt . getTime ( ) ;
440+ } catch {
441+ // Ignore parse errors
442+ }
443+ }
444+
445+ // Count steps (from step_start events, only first attempt)
446+ const stepIndices = new Set < number > ( ) ;
447+ for ( const event of events ) {
448+ if ( event . type === 'step_start' ) {
449+ const stepIndex = event . data ?. step_index ;
450+ if ( stepIndex !== undefined ) {
451+ stepIndices . add ( stepIndex ) ;
452+ }
453+ }
454+ }
455+ let totalSteps = stepIndices . size ;
456+
457+ // If run_end has steps count, use that (more accurate)
458+ if ( runEnd ) {
459+ const stepsFromEnd = runEnd . data ?. steps ;
460+ if ( stepsFromEnd !== undefined ) {
461+ totalSteps = Math . max ( totalSteps , stepsFromEnd ) ;
462+ }
463+ }
464+
465+ // Count total events
466+ const totalEvents = events . length ;
467+
468+ // Infer final status
469+ const finalStatus = this . _inferFinalStatusFromTrace ( ) ;
470+
471+ return {
472+ total_steps : totalSteps ,
473+ total_events : totalEvents ,
474+ duration_ms : durationMs ,
475+ final_status : finalStatus ,
476+ started_at : startedAt ,
477+ ended_at : endedAt ,
478+ } ;
479+ } catch ( error : any ) {
480+ this . logger ?. warn ( `Error extracting stats from trace: ${ error . message } ` ) ;
481+ return {
482+ total_steps : 0 ,
483+ total_events : 0 ,
484+ duration_ms : null ,
485+ final_status : 'unknown' ,
486+ started_at : null ,
487+ ended_at : null ,
488+ } ;
489+ }
490+ }
491+
492+ /**
493+ * Call /v1/traces/complete to report file sizes and stats to gateway.
336494 *
337495 * This is a best-effort call - failures are logged but don't affect upload success.
338496 */
@@ -346,13 +504,21 @@ export class CloudTraceSink extends TraceSink {
346504 const url = new URL ( `${ this . apiUrl } /v1/traces/complete` ) ;
347505 const protocol = url . protocol === 'https:' ? https : http ;
348506
507+ // Extract stats from trace file
508+ const stats = this . _extractStatsFromTrace ( ) ;
509+
510+ // Add file size fields
511+ const completeStats = {
512+ ...stats ,
513+ trace_file_size_bytes : this . traceFileSizeBytes ,
514+ screenshot_total_size_bytes : this . screenshotTotalSizeBytes ,
515+ screenshot_count : this . screenshotCount ,
516+ index_file_size_bytes : this . indexFileSizeBytes ,
517+ } ;
518+
349519 const body = JSON . stringify ( {
350520 run_id : this . runId ,
351- stats : {
352- trace_file_size_bytes : this . traceFileSizeBytes ,
353- screenshot_total_size_bytes : this . screenshotTotalSizeBytes ,
354- screenshot_count : this . screenshotCount ,
355- } ,
521+ stats : completeStats ,
356522 } ) ;
357523
358524 const options = {
@@ -447,6 +613,7 @@ export class CloudTraceSink extends TraceSink {
447613 const indexData = await fsPromises . readFile ( indexPath ) ;
448614 const compressedIndex = zlib . gzipSync ( indexData ) ;
449615 const indexSize = compressedIndex . length ;
616+ this . indexFileSizeBytes = indexSize ; // Track index file size
450617
451618 this . logger ?. info ( `Index file size: ${ ( indexSize / 1024 ) . toFixed ( 2 ) } KB` ) ;
452619 if ( this . logger ) {
0 commit comments