@@ -773,6 +773,57 @@ export class CloudTraceSink extends TraceSink {
773773 } ) ;
774774 }
775775
776+ /**
777+ * Normalize screenshot data by extracting base64 from data URL if needed.
778+ *
779+ * Handles both formats:
780+ * - Data URL: "data:image/jpeg;base64,/9j/4AAQ..."
781+ * - Pure base64: "/9j/4AAQ..."
782+ *
783+ * @param screenshotRaw - Raw screenshot data (data URL or base64)
784+ * @param defaultFormat - Default format if not detected from data URL
785+ * @returns Tuple of [base64String, formatString]
786+ */
787+ private _normalizeScreenshotData (
788+ screenshotRaw : string ,
789+ defaultFormat : string = 'jpeg'
790+ ) : [ string , string ] {
791+ if ( ! screenshotRaw ) {
792+ return [ '' , defaultFormat ] ;
793+ }
794+
795+ // Check if it's a data URL
796+ if ( screenshotRaw . startsWith ( 'data:image' ) ) {
797+ // Extract format from "data:image/jpeg;base64,..." or "data:image/png;base64,..."
798+ try {
799+ // Split on comma to get the base64 part
800+ if ( screenshotRaw . includes ( ',' ) ) {
801+ const [ header , base64Data ] = screenshotRaw . split ( ',' , 2 ) ;
802+ // Extract format from header: "data:image/jpeg;base64"
803+ if ( header . includes ( '/' ) && header . includes ( ';' ) ) {
804+ const formatPart = header . split ( '/' ) [ 1 ] ?. split ( ';' ) [ 0 ] ;
805+ if ( formatPart === 'jpeg' || formatPart === 'jpg' ) {
806+ return [ base64Data , 'jpeg' ] ;
807+ } else if ( formatPart === 'png' ) {
808+ return [ base64Data , 'png' ] ;
809+ }
810+ }
811+ return [ base64Data , defaultFormat ] ;
812+ } else {
813+ // Malformed data URL - return as-is with warning
814+ this . logger ?. warn ( 'Malformed data URL in screenshot_base64 (missing comma)' ) ;
815+ return [ screenshotRaw , defaultFormat ] ;
816+ }
817+ } catch ( error : any ) {
818+ this . logger ?. warn ( `Error parsing screenshot data URL: ${ error . message } ` ) ;
819+ return [ screenshotRaw , defaultFormat ] ;
820+ }
821+ }
822+
823+ // Already pure base64
824+ return [ screenshotRaw , defaultFormat ] ;
825+ }
826+
776827 /**
777828 * Extract screenshots from trace events.
778829 *
@@ -798,15 +849,24 @@ export class CloudTraceSink extends TraceSink {
798849 // Check if this is a snapshot event with screenshot
799850 if ( event . type === 'snapshot' ) {
800851 const data = event . data || { } ;
801- const screenshotBase64 = data . screenshot_base64 ;
802-
803- if ( screenshotBase64 ) {
804- sequence += 1 ;
805- screenshots . set ( sequence , {
806- base64 : screenshotBase64 ,
807- format : data . screenshot_format || 'jpeg' ,
808- stepId : event . step_id ,
809- } ) ;
852+ const screenshotRaw = data . screenshot_base64 ;
853+
854+ if ( screenshotRaw ) {
855+ // Normalize: extract base64 from data URL if needed
856+ // Handles both "data:image/jpeg;base64,..." and pure base64
857+ const [ screenshotBase64 , screenshotFormat ] = this . _normalizeScreenshotData (
858+ screenshotRaw ,
859+ data . screenshot_format || 'jpeg'
860+ ) ;
861+
862+ if ( screenshotBase64 ) {
863+ sequence += 1 ;
864+ screenshots . set ( sequence , {
865+ base64 : screenshotBase64 ,
866+ format : screenshotFormat ,
867+ stepId : event . step_id ,
868+ } ) ;
869+ }
810870 }
811871 }
812872 } catch {
0 commit comments