@@ -22,6 +22,7 @@ import { parseRx } from "./util/seeds.js";
2222import { getFileOrUrlJson } from "./util/file_reader.js" ;
2323import { WACZLoader } from "./util/wacz.js" ;
2424import { CSR_CLUES } from "./util/clientSideRenderingClues.js" ;
25+ import { extractTextFromDOM } from "./util/textextract.js" ;
2526
2627// RWP Replay Prefix
2728const REPLAY_PREFIX = "http://localhost:9990/replay/w/replay/" ;
@@ -44,6 +45,7 @@ type ComparisonData = {
4445 comparison : {
4546 screenshotMatch ?: number ;
4647 textMatch ?: number ;
48+ rawTextMatch ?: number ;
4749 resourceCounts : {
4850 crawlGood ?: number ;
4951 crawlBad ?: number ;
@@ -467,6 +469,8 @@ export class ReplayCrawler extends Crawler {
467469
468470 await this . compareText ( page , data , url , date ) ;
469471
472+ await this . compareRawText ( page , data , url , date ) ;
473+
470474 await this . compareResources ( page , data , url , date ) ;
471475
472476 if ( this . params . qaDetectClientSideRendering )
@@ -596,6 +600,112 @@ export class ReplayCrawler extends Crawler {
596600 }
597601 }
598602
603+ async compareRawText ( page : Page , state : PageState , url : string , date : Date ) {
604+ // Try to fetch the stored crawl-time raw response text
605+ let rawText = await this . fetchOrigText (
606+ page ,
607+ "text-from-response" ,
608+ url ,
609+ date . toISOString ( ) . replace ( / [ ^ \d ] / g, "" ) ,
610+ ) ;
611+
612+ // If not stored (older archive), extract it from the archived raw response on-the-fly
613+ if ( rawText === undefined ) {
614+ logger . info (
615+ "Stored raw text not found, extracting from archived response" ,
616+ { url } ,
617+ "replay" ,
618+ ) ;
619+ rawText = await this . extractRawTextFromArchive ( page , url , date ) ;
620+ }
621+
622+ // Get the replay-time rendered text (extracted from browser after JS runs)
623+ const renderedText = state . text ;
624+
625+ if ( rawText === undefined || renderedText === undefined ) {
626+ logger . warn (
627+ "Text missing for raw vs rendered comparison" ,
628+ {
629+ url,
630+ rawTextLen : rawText ?. length ,
631+ renderedTextLen : renderedText ?. length ,
632+ } ,
633+ "replay" ,
634+ ) ;
635+ return ;
636+ }
637+
638+ const dist = levenshtein ( rawText , renderedText ) ;
639+ const maxLen = Math . max ( rawText . length , renderedText . length ) ;
640+
641+ let matchPercent = 1.0 ;
642+ if ( maxLen > 0 ) {
643+ matchPercent = ( maxLen - dist ) / maxLen ;
644+ }
645+ logger . info ( "Raw vs Rendered Text Levenshtein Dist" , {
646+ url,
647+ dist,
648+ matchPercent,
649+ maxLen,
650+ rawTextLen : rawText . length ,
651+ renderedTextLen : renderedText . length ,
652+ } ) ;
653+
654+ const pageInfo = this . pageInfos . get ( page ) ;
655+ if ( pageInfo ) {
656+ pageInfo . comparison . rawTextMatch = matchPercent ;
657+ }
658+ }
659+
660+ async extractRawTextFromArchive (
661+ page : Page ,
662+ url : string ,
663+ date : Date ,
664+ ) : Promise < string | undefined > {
665+ const timestamp = date . toISOString ( ) . slice ( 0 , 19 ) . replace ( / [ T : - ] / g, "" ) ;
666+ // `if_` suffix ensures wabac.js serves the unaltered source
667+ const replayUrl = REPLAY_PREFIX + `${ timestamp } if_/${ url } ` ;
668+
669+ const frame = page . frames ( ) [ 1 ] ;
670+ if ( ! frame ) {
671+ logger . warn (
672+ "Replay frame missing for raw text extraction" ,
673+ { url } ,
674+ "replay" ,
675+ ) ;
676+ return undefined ;
677+ }
678+
679+ const htmlContent = await frame . evaluate ( async ( url ) => {
680+ const response = await fetch ( url , {
681+ method : "GET" ,
682+ credentials : "include" ,
683+ } ) ;
684+ if ( response . status !== 200 ) {
685+ return undefined ;
686+ }
687+ return await response . text ( ) ;
688+ } , replayUrl ) ;
689+
690+ if ( ! htmlContent ) {
691+ logger . warn (
692+ "Could not fetch HTML content for raw text extraction" ,
693+ { url } ,
694+ "replay" ,
695+ ) ;
696+ return undefined ;
697+ }
698+
699+ // Extract text from the raw HTML using the shared extraction logic
700+ const { JSDOM } = await import ( "jsdom" ) ;
701+ const dom = new JSDOM ( htmlContent ) ;
702+ try {
703+ return extractTextFromDOM ( dom . window . document ) ;
704+ } finally {
705+ dom . window . close ( ) ;
706+ }
707+ }
708+
599709 async compareResources (
600710 page : Page ,
601711 state : PageState ,
0 commit comments