import { Device } from "last-hit-types" ; import { CDPSession , Page , ResourceType } from "puppeteer" ; import uuidv4 from "uuid/v4" ; import BrowserHelper from "./browser-helper" ; const staticResourceTypes = [ "document" , "stylesheet" , "image" , "media" , "font" , "script" , "texttrack" , "eventsource" , "manifest" , "other" , ] ; const dynamicResourceTypes = [ "xhr" , "fetch" , "websocket" ] ; const ignoredIdRegexps = [ /^md-.+-.{6,16}$/ , /^select2-.+$/ , /^.+\d{10,}$/ , /^\s*$/ , /^.+-\d{2,10}--value$/ , /^.+-\d{1,}$/ , /^.+_\d{1,}$/ , /^react-select.+-.+$/ , ] ; const idShouldIgnore = ( id ) : boolean => ignoredIdRegexps . some ( ( regexp ) => regexp . test ( id ) ) ; export default class PageHelper { private static dataAttrName : string | null | undefined = null ; public static setDataAttrName ( dataAttrName ? ) { PageHelper . dataAttrName = dataAttrName ; } private static async createCDPClient ( page ) { return await page . target ( ) . createCDPSession ( ) ; } static async captureScreenshot ( page ) { return await page . screenshot ( { encoding : "base64" } ) ; } static async isAllRelatedPagesClosed ( page ) { const pages = await page . browser ( ) . pages ( ) ; return pages . filter ( ( p ) => page !== p ) . length === 0 ; } static shouldIgnore ( id ) { return idShouldIgnore ( id ) ; } private static async emulate ( page , device , client ) { if ( device . wechat && ( device . userAgent || "" ) . indexOf ( "MicroMessenger" ) === - 1 ) { device . userAgent = `${ device . userAgent } MicroMessenger/6.5.7` ; } await page . emulate ( device ) ; await page . emulateMedia ( "screen" ) ; const setBackground = ( ) => ( document . documentElement . style . backgroundColor = "rgba(25,25,25,0.8)" ) ; await page . evaluate ( setBackground ) ; page . on ( "load" , async ( ) => await page . evaluate ( setBackground ) ) ; if ( device . viewport . isMobile ) { await client . send ( "Emulation.setFocusEmulationEnabled" , { enabled : true } ) ; } } private static async exposeFunctionToPage ( browserHelper , page ) { const uuid = browserHelper . getAllPages ( ) . findUuidByPage ( page ) ; const flowKey = browserHelper . getFlowKey ( ) ; const eventRecorder = browserHelper . getEventRecorder ( ) ; let exists = await page . evaluate ( ( ) => window . $lhGetUuid != null ) ; if ( ! exists ) { await page . exposeFunction ( "$lhGetUuid" , ( ) => uuid ) ; } await page . exposeFunction ( "$lhGetFlowKey" , ( ) => flowKey ) ; await page . exposeFunction ( "$lhRecordEvent" , eventRecorder . record ) ; } private static async installListenersOnPage ( browserHelper , page , onSwitchFromReplayToRecord = false ) { console . log ( "install listener on page" ) ; const god = ( ) => { if ( window . $lhGod ) { return ; } window . $lhGod = true ; console . log ( "%c last-hit: %c evaluate on new document start..." , "color:red" , "color:brown" ) ; const ignoredIdRegexps = [ /^md-.+-.{6,16}$/ , /^select2-.+$/ , /^.+\d{10,}$/ , /^\s*$/ , /^.+-\d{2,10}--value$/ , /^.+-\d{1,}$/ , /^.+_\d{1,}$/ , /^react-select.+-.+$/ , ] ; const idShouldIgnore = ( id ) : boolean => ignoredIdRegexps . some ( ( regexp ) => regexp . test ( id ) ) ; const idSelector = ( id ) => `#${ CSS . escape ( id ) }` ; const nodeNameInCorrectCase = ( elm ) : string => { if ( ! elm . localName ) { return elm . nodeName ; } if ( elm . localName . length !== elm . nodeName . length ) { return elm . nodeName ; } return elm . localName ; } ; const areNodesSimilar = ( left , right ) : boolean => { if ( left === right ) { return true ; } if ( left . nodeType === Node . ELEMENT_NODE && right . nodeType === Node . ELEMENT_NODE ) { return left . localName === right . localName ; } if ( left . nodeType === right . nodeType ) { return true ; } const leftType = left . nodeType === Node . CDATA_SECTION_NODE ? Node . TEXT_NODE : left . nodeType ; const rightType = right . nodeType === Node . CDATA_SECTION_NODE ? Node . TEXT_NODE : right . nodeType ; return leftType === rightType ; } ; return { number } const getNodeIndexForXPath = ( elm ) : number => { const siblings = elm . parentNode ? elm . parentNode . children : null ; if ( ! siblings ) { return 0 ; } let hasSameNamedElements ; for ( let i = 0 ; i < siblings . length ; ++ i ) { if ( areNodesSimilar ( elm , siblings [ i ] ) && siblings [ i ] !== elm ) { hasSameNamedElements = true ; break ; } } if ( ! hasSameNamedElements ) { return 0 ; } let ownIndex = 1 ; for ( let i = 0 ; i < siblings . length ; ++ i ) { if ( areNodesSimilar ( elm , siblings [ i ] ) ) { if ( siblings [ i ] === elm ) { return ownIndex ; } ++ ownIndex ; } } return - 1 ; } ; const createXPathStep = ( elm , optimized ) : StepPath => { let ownValue ; const ownIndex = getNodeIndexForXPath ( elm ) ; if ( ownIndex === - 1 ) { return null ; } switch ( elm . nodeType ) { case Node . ELEMENT_NODE : const id = elm . getAttribute ( "id" ) ; if ( optimized && id && ! idShouldIgnore ( id ) ) { return new StepPath ( '//*[@id="' + id + '"]' , true ) ; } ownValue = elm . localName ; break ; case Node . ATTRIBUTE_NODE : ownValue = "@" + elm . nodeName ; break ; case Node . TEXT_NODE : case Node . CDATA_SECTION_NODE : ownValue = "text()" ; break ; case Node . PROCESSING_INSTRUCTION_NODE : ownValue = "processing-instruction()" ; break ; case Node . COMMENT_NODE : ownValue = "comment()" ; break ; case Node . DOCUMENT_NODE : ownValue = "" ; break ; default : ownValue = "" ; break ; } if ( ownIndex > 0 ) { ownValue += "[" + ownIndex + "]" ; } return new StepPath ( ownValue , elm . nodeType === Node . DOCUMENT_NODE ) ; } ; const createXPathFromElement = ( elm , optimized ) : string => { if ( elm . nodeType === Node . DOCUMENT_NODE ) { return "/" ; } const steps = [ ] ; let contextNode = elm ; while ( contextNode ) { const step = createXPathStep ( contextNode , optimized ) ; if ( ! step ) { break ; } steps . push ( step ) ; if ( step . optimized ) { break ; } contextNode = contextNode . parentNode as Element ; } steps . reverse ( ) ; return ( steps . length && steps [ 0 ] . optimized ? "" : "/" ) + steps . join ( "/" ) ; } ; const prefixedElementClassNames = ( elm ) : string [ ] => { const classNames = elm . getAttribute ( "class" ) ; if ( ! classNames ) { return [ ] ; } return classNames . split ( /\s+/g ) . filter ( Boolean ) . map ( ( name ) => { return "$" + name ; } ) ; } ; const createCssPathStep = ( elm , optimized , isTargetNode ) : StepPath => { if ( elm . nodeType !== Node . ELEMENT_NODE ) { return null ; } const id = elm . getAttribute ( "id" ) ; if ( optimized ) { if ( id && ! idShouldIgnore ( id ) ) { return new StepPath ( idSelector ( id ) , true ) ; } const nodeNameLower = elm . nodeName . toLowerCase ( ) ; if ( nodeNameLower === "body" || nodeNameLower === "head" || nodeNameLower === "html" ) { return new StepPath ( nodeNameInCorrectCase ( elm ) , true ) ; } } const nodeName = nodeNameInCorrectCase ( elm ) ; if ( id && ! idShouldIgnore ( id ) ) { return new StepPath ( nodeName + idSelector ( id ) , true ) ; } const parent = elm . parentNode ; if ( ! parent || parent . nodeType === Node . DOCUMENT_NODE ) { return new StepPath ( nodeName , true ) ; } const prefixedOwnClassNamesArray = prefixedElementClassNames ( elm ) ; let needsClassNames = false ; let needsNthChild = false ; let ownIndex = - 1 ; let elementIndex = - 1 ; const siblings = parent . children ; for ( let i = 0 ; ( ownIndex === - 1 || ! needsNthChild ) && i < siblings . length ; ++ i ) { const sibling = siblings [ i ] ; if ( sibling . nodeType !== Node . ELEMENT_NODE ) { continue ; } elementIndex += 1 ; if ( sibling === elm ) { ownIndex = elementIndex ; continue ; } if ( needsNthChild ) { continue ; } if ( nodeNameInCorrectCase ( sibling ) !== nodeName ) { continue ; } needsClassNames = true ; const ownClassNames = new Set ( prefixedOwnClassNamesArray ) ; if ( ! ownClassNames . size ) { needsNthChild = true ; continue ; } const siblingClassNamesArray = prefixedElementClassNames ( sibling ) ; for ( let j = 0 ; j < siblingClassNamesArray . length ; ++ j ) { const siblingClass = siblingClassNamesArray [ j ] ; if ( ! ownClassNames . has ( siblingClass ) ) { continue ; } ownClassNames . delete ( siblingClass ) ; if ( ! ownClassNames . size ) { needsNthChild = true ; break ; } } } let result = nodeName ; if ( isTargetNode && nodeName . toLowerCase ( ) === "input" && elm . getAttribute ( "type" ) && ( ! id || idShouldIgnore ( id ) ) && ! elm . getAttribute ( "class" ) ) { result += "[type=" + CSS . escape ( elm . getAttribute ( "type" ) ) + "]" ; } if ( needsNthChild ) { result += ":nth-child(" + ( ownIndex + 1 ) + ")" ; } else if ( needsClassNames ) { for ( const prefixedName of prefixedOwnClassNamesArray ) { result += "." + CSS . escape ( prefixedName . slice ( 1 ) ) ; } } return new StepPath ( result , false ) ; } ; const createCssPathFromElement = ( elm , optimized ) : string => { if ( elm . nodeType !== Node . ELEMENT_NODE ) { return "" ; } const steps = [ ] ; let contextNode = elm ; while ( contextNode ) { const step = createCssPathStep ( contextNode , ! ! optimized , contextNode === elm ) ; if ( ! step ) { break ; } steps . push ( step ) ; if ( step . optimized ) { break ; } contextNode = contextNode . parentNode as Element ; } steps . reverse ( ) ; return steps . join ( " > " ) ; } ; const createDataPathFromElement = ( elm ) : string | null => { if ( elm . nodeType === Node . DOCUMENT_NODE ) { return "" ; } const attrName = `data-${ window . $lhDataAttrName || "lh-key" }` ; const paths = [ ] ; while ( true ) { const value = elm . getAttribute ( attrName ) ; if ( value ) { paths . push ( `[${ attrName }="${ value }"]` ) ; return paths . reverse ( ) . join ( " > " ) ; } else { const parent = elm . parentElement ; if ( ! parent || parent == document . body ) { return null ; } else { const children = parent . children ; for ( let index = 0 , count = children . length ; index < count ; index ++ ) { const child = children . item ( index ) ; if ( child == elm ) { paths . push ( `${ elm . tagName }:nth-child(${ index + 1 })` ) ; elm = parent ; break ; } } } } } } ; class StepPath { private value : string ; optimized : boolean ; constructor ( value , optimized = false ) { this . value = value ; this . optimized = optimized ; } toString ( ) { return this . value ; } } const transformEvent = ( e , element ) : { [ key in string ] : any ; } => { let xpath = createXPathFromElement ( element , true ) ; let csspath = createCssPathFromElement ( element , true ) ; let datapath = createDataPathFromElement ( element ) ; if ( ( e . type === "click" || e . type === "mousedown" ) && xpath . indexOf ( "/svg" ) !== - 1 ) { console . log ( "xpath contains svg dom node." ) ; const newXpath = xpath . replace ( /^(.*button.*)\/svg.*$/ , "$1" ) ; const newCssPath = csspath . replace ( /^(.*button.*)\s>\ssvg.*$/ , "$1" ) ; console . log ( `new xpath after svg cut-off is ${ newXpath }.` ) ; if ( newXpath !== xpath ) { let parent = element ; while ( parent . tagName !== "BUTTON" ) { parent = parent . parentElement ; } element = parent ; xpath = newXpath ; csspath = newCssPath ; } } const isDocument = element . nodeType === Node . DOCUMENT_NODE ; return { altKey : e . altKey , ctrlKey : e . ctrlKey , metaKey : e . metaKey , shiftKey : e . shiftKey , button : e . button , buttons : e . buttons , detail : e . detail , /** positions */ clientX : e . clientX , clientY : e . clientY , pageX : e . pageX , pageY : e . pageY , screenX : e . screenX , screenY : e . screenY , scrollTop : element === document ? document . documentElement . scrollTop : element . scrollTop , scrollLeft : element === document ? document . documentElement . scrollLeft : element . scrollLeft , timeStamp : e . timeStamp , type : e . type , isTrusted : e . isTrusted , value : e . type !== "keydown" ? element . value : e . key , path : xpath , csspath : csspath , datapath : datapath , target : isDocument ? "document" : `<${ element . tagName . toLowerCase ( ) } ${ element . getAttributeNames ( ) . map ( ( name ) => `${ name }="${ element . getAttribute ( name ) }"` ) . join ( " " ) }>` , } ; } ; let scrollTimeoutHandle ; const eventHandler = ( e ) => { if ( ! e ) { return ; } if ( [ "STYLE" ] . includes ( e . target && e . target . tagName ) ) { return ; } if ( e . type === "keydown" && e . key !== "Enter" ) { return ; } let element = e . target ; const data = transformEvent ( e , element ) ; data . uuid = window . $lhUuid ; if ( e . type === "scroll" ) { if ( scrollTimeoutHandle ) { clearTimeout ( scrollTimeoutHandle ) ; } scrollTimeoutHandle = setTimeout ( ( ) => { window . $lhRecordEvent ( JSON . stringify ( data ) ) ; scrollTimeoutHandle = null ; } , 100 ) ; } else if ( e . type === "change" && element . tagName === "INPUT" && ( element . getAttribute ( "type" ) || "" ) . toLowerCase ( ) === "file" ) { const file = element . files [ 0 ] ; if ( file ) { const reader = new FileReader ( ) ; reader . onload = ( ) => { data . file = reader . result ; window . $lhRecordEvent ( JSON . stringify ( data ) ) ; } ; reader . readAsDataURL ( file ) ; } } else if ( element . tagName === "INPUT" && [ "checkbox" , "radio" ] . indexOf ( ( element . getAttribute ( "type" ) || "" ) . toLowerCase ( ) ) != - 1 ) { data . checked = element . checked ; window . $lhRecordEvent ( JSON . stringify ( data ) ) ; } else { window . $lhRecordEvent ( JSON . stringify ( data ) ) ; } } ; window . $lhGetUuid ( ) . then ( ( uuid ) => { window . $lhUuid = uuid ; } ) ; Object . values ( { CLICK : "click" , CHANGE : "change" , KEYDOWN : "keydown" , FOCUS : "focus" , SCROLL : "scroll" , MOUSE_DOWN : "mousedown" , SUBMIT : "submit" , } ) . forEach ( ( eventType ) => { document . addEventListener ( eventType , eventHandler , { capture : true } ) ; if ( window . $lhOnSwitchFromReplayToRecord ) { document . querySelectorAll ( "iframe" ) . forEach ( ( frame ) => { frame . contentDocument ! . addEventListener ( eventType , eventHandler , { capture : true } ) ; } ) ; } } ) ; const recordDialogEvent = ( options ) => { const { message , defaultMessage , returnValue , eventType , dialogType } = options ; window . $lhRecordEvent ( JSON . stringify ( { uuid : window . $lhUuid , type : eventType , dialog : dialogType , message , defaultMessage , returnValue , target : "document" , url : window . location . href , } ) ) ; } ; const nativeAlert = window . alert ; window . alert = ( message ) => { recordDialogEvent ( { message , eventType : "dialog-open" , dialogType : "alert" } ) ; nativeAlert ( message ) ; recordDialogEvent ( { message , eventType : "dialog-close" , dialogType : "alert" } ) ; } ; const nativeConfirm = window . confirm ; window . confirm = ( message ) => { recordDialogEvent ( { message , eventType : "dialog-open" , dialogType : "confirm" } ) ; const ret = nativeConfirm ( message ) ; recordDialogEvent ( { message , eventType : "dialog-close" , dialogType : "confirm" , returnValue : ret , } ) ; return ret ; } ; const nativePrompt = window . prompt ; window . prompt = ( message , defaultMessage ) => { recordDialogEvent ( { message , defaultMessage , eventType : "dialog-open" , dialogType : "prompt" , } ) ; const ret = nativePrompt ( message , defaultMessage ) ; recordDialogEvent ( { message , defaultMessage , eventType : "dialog-close" , dialogType : "prompt" , returnValue : ret , } ) ; return ret ; } ; ( ( window ) => { if ( ! /MicroMessenger/i . test ( navigator . userAgent ) ) { return ; } const WeixinJSBridgeData = { } ; let imageData = null ; const transformPNG2JPEG = ( base64Image ) => { return new Promise ( ( resolve ) => { const canvas = document . createElement ( "canvas" ) ; const ctx = canvas . getContext ( "2d" ) ; const image = new Image ( ) ; image . crossOrigin = "anonymous" ; image . onload = function ( ) { const { width , height } = image ; canvas . width = width ; canvas . height = height ; ctx . fillStyle = "#fff" ; ctx . fillRect ( 0 , 0 , width , height ) ; ctx . drawImage ( image , 0 , 0 , width , height ) ; resolve ( canvas . toDataURL ( "image/jpeg" , 1.0 ) ) ; } ; image . src = base64Image ; } ) ; } ; window . WeixinJSBridge = { invoke : ( event , data , func ) : void => { switch ( event ) { case "sendAppMessage" : case "shareTimeline" : console . log ( "%c Ready for share" , "color:red" , data ) ; WeixinJSBridgeData [ event ] = data ; WeixinJSBridgeData [ event ] . _callback = func ; break ; case "preVerifyJSAPI" : func ( { } ) ; break ; case "chooseImage" : const input = document . createElement ( "input" ) ; input . setAttribute ( "type" , "file" ) ; input . style . visibility = "hidden" ; input . onchange = ( evt ) => { const file = input . files [ 0 ] ; const reader = new FileReader ( ) ; reader . onload = ( evt ) => { const base64Image = evt . target . result ; if ( typeof base64Image === "string" && base64Image . startsWith ( "data:image/png;" ) ) { transformPNG2JPEG ( base64Image ) . then ( ( base64Image ) => { imageData = base64Image ; func ( { localIds : [ 0 ] , errMsg : "chooseImage:ok" } ) ; } ) ; } else { imageData = base64Image ; func ( { localIds : [ 0 ] , errMsg : "chooseImage:ok" } ) ; } } ; reader . readAsDataURL ( file ) ; } ; document . body . append ( input ) ; input . click ( ) ; break ; case "getLocalImgData" : func ( { localData : imageData , errMsg : "getLocalImgData:ok" } ) ; break ; } } , on : ( event , func ) => { func ( { } ) ; } , } ; window . addEventListener ( "DOMContentLoaded" , ( ) => { if ( document . getElementById ( "last-hit-bars" ) != null ) { return ; } const div = document . createElement ( "DIV" ) ; div . id = "last-hit-bars" ; div . style . position = "fixed" ; div . style . display = "flex" ; div . style . top = "0" ; div . style . right = "0" ; div . style . backgroundColor = "transparent" ; div . style . zIndex = "100000" ; const span = document . createElement ( "SPAN" ) ; span . id = "last-hit-wechat-share" ; span . style . height = "24px" ; span . style . width = "24px" ; span . style . backgroundColor = "burlywood" ; span . style . opacity = "0.7" ; span . style . borderRadius = "100%" ; span . style . margin = "3px" ; span . style . boxSizing = "border-box" ; span . style . cursor = "pointer" ; span . style . fontWeight = "bold" ; span . style . lineHeight = "24px" ; span . style . fontSize = "12px" ; span . style . color = "#fff" ; const textSpan = document . createElement ( "span" ) ; textSpan . textContent = "Share" ; textSpan . style . lineHeight = "24px" ; textSpan . style . transform = "scale(0.7)" ; textSpan . style . display = "block" ; textSpan . style . transformOrigin = "left" ; textSpan . style . whiteSpace = "nowrap" ; span . append ( textSpan ) ; span . onclick = ( ) => { const data = WeixinJSBridgeData [ "sendAppMessage" ] ; if ( data && data . link ) { console . log ( `%c Share to: %c ${ data . link }` , "color:red" , "color:brown" ) ; window . open ( data . link ) ; data . _callback && data . _callback ( { errMsg : "sendAppMessage:ok" } ) ; } else { console . log ( `%c Share to: %c ${ location . href }` , "color:red" , "color:brown" ) ; window . open ( location . href ) ; } } ; div . append ( span ) ; document . body . append ( div ) ; } ) ; } ) ( window ) ; console . log ( "%c last-hit: %c evaluate on new document end..." , "color:red" , "color:brown" ) ; } ; if ( onSwitchFromReplayToRecord ) { await page . evaluate ( ( ) => ( window . $lhOnSwitchFromReplayToRecord = true ) ) ; } if ( PageHelper . dataAttrName ) { await page . evaluate ( ( dataAttrName ) => ( window . $lhDataAttrName = dataAttrName ) , PageHelper . dataAttrName ) ; } else { await page . evaluate ( ( ) => ( window . $lhDataAttrName = null ) ) ; } await page . evaluateOnNewDocument ( god ) ; await page . evaluate ( god ) ; } static async control ( browserHelper , page , onSwitchFromReplayToRecord = false ) { await PageHelper . exposeFunctionToPage ( browserHelper , page ) ; await PageHelper . installListenersOnPage ( browserHelper , page , onSwitchFromReplayToRecord ) ; const client = await PageHelper . createCDPClient ( page ) ; PageHelper . emulate ( page , browserHelper . getDevice ( ) , client ) ; await PageHelper . monitorAnimation ( client , browserHelper , page ) ; PageHelper . monitorPageClosed ( page , browserHelper , client ) ; PageHelper . monitorPagePopuped ( page , browserHelper ) ; PageHelper . monitorPageErrorOccurred ( page , browserHelper ) ; PageHelper . monitorDialogOpened ( page , browserHelper ) ; } private static monitorDialogOpened ( page , browserHelper ) { page . on ( "dialog" , async ( dialog ) => { console . log ( `page event dialog caught` ) ; if ( dialog . type ( ) === "beforeunload" ) { const base64 = await PageHelper . captureScreenshot ( page ) ; const uuid = browserHelper . getAllPages ( ) . findUuidByPage ( page ) ; browserHelper . recordPageWindowEvent ( { type : "dialog-open" , dialog : "beforeunload" , url : page . url ( ) , image : base64 , uuid , } ) ; } } ) ; } private static monitorPageErrorOccurred ( page , browserHelper ) { page . on ( "pageerror" , async ( ) => { console . log ( `page event pageerror caught` ) ; const base64 = await PageHelper . captureScreenshot ( page ) ; const uuid = browserHelper . getAllPages ( ) . findUuidByPage ( page ) ; browserHelper . recordPageWindowEvent ( { type : "page-error" , url : page . url ( ) , image : base64 , uuid , } ) ; } ) ; } /**
     * page created by window.open or anchor
     */ private static monitorPagePopuped ( page , browserHelper ) { page . on ( "popup" , async ( newPage ) => { console . log ( "page event popup caught" ) ; const allPages = browserHelper . getAllPages ( ) ; if ( ! allPages . exists ( newPage ) ) { const uuid = uuidv4 ( ) ; allPages . add ( uuid , newPage ) ; await PageHelper . control ( browserHelper , newPage , false ) ; browserHelper . recordPageWindowEvent ( { type : "page-created" , url : newPage . url ( ) , uuid , } ) ; } } ) ; } private static monitorPageClosed ( page , browserHelper , client ) { page . on ( "close" , async ( ) => { const allClosed = await PageHelper . isAllRelatedPagesClosed ( page ) ; const uuid = browserHelper . getAllPages ( ) . removeByPage ( page ) ; if ( uuid ) { browserHelper . recordPageWindowEvent ( { type : "page-closed" , url : page . url ( ) , allClosed , uuid , } ) ; } try { client . detach ( ) ; } catch { } } ) ; } private static async monitorAnimation ( client , browserHelper , page ) { } private static isDynamicResource ( resourceType ) { return dynamicResourceTypes . includes ( resourceType ) ; } static async monitorRequests ( page , browserHelper ) { PageHelper . monitorRequestFinished ( page , browserHelper ) ; PageHelper . monitorRequestFailed ( page , browserHelper ) ; } private static monitorRequestFailed ( page , browserHelper ) { page . on ( "requestfailed" , async ( request ) => { const url = request . url ( ) ; const resourceType = request . resourceType ( ) ; if ( PageHelper . isDynamicResource ( resourceType ) ) { try { browserHelper . recordPageWindowEvent ( { type : "ajax" , uuid : browserHelper . getAllPages ( ) . findUuidByPage ( page ) , failed : true , request : { url , method : request . method ( ) , headers : request . headers ( ) , body : request . postData ( ) , resourceType , } , } ) ; } catch ( err ) { console . error ( `Failed getting data from: ${ url }` ) ; console . error ( err ) ; } } else { } } ) ; } private static monitorRequestFinished ( page , browserHelper ) { page . on ( "requestfinished" , async ( request ) => { const url = request . url ( ) ; const response = request . response ( ) ; const resourceType = request . resourceType ( ) ; if ( PageHelper . isDynamicResource ( resourceType ) ) { const sendEvent = ( body ? ) => { try { browserHelper . recordPageWindowEvent ( { type : "ajax" , uuid : browserHelper . getAllPages ( ) . findUuidByPage ( page ) , request : { url , method : request . method ( ) , headers : request . headers ( ) , body : request . postData ( ) , resourceType , } , response : { statusCode : response . status ( ) , statusMessage : response . statusText ( ) , headers : response . headers ( ) , body , } , } ) ; } catch ( err ) { console . error ( `Failed getting data from: ${ url }` ) ; console . error ( err ) ; } } ; try { sendEvent ( await response . text ( ) ) ; } catch { setTimeout ( async ( ) => { try { sendEvent ( await response . text ( ) ) ; } catch { sendEvent ( ) ; } } , 1000 ) ; } } else { } } ) ; } }