import type { Transaction } from 'elastic-apm-node' ; import { defer , forkJoin , throwError , Observable } from 'rxjs' ; import { catchError , mergeMap , switchMapTo , timeoutWith } from 'rxjs/operators' ; import type { Logger } from 'src/core/server' ; import type { Layout as ScreenshotModeLayout } from 'src/plugins/screenshot_mode/common' ; import type { ConditionalHeaders , HeadlessChromiumDriver } from '../browsers' ; import { getChromiumDisconnectedError } from '../browsers' ; import type { Layout } from '../layouts' ; import type { ElementsPositionAndAttribute } from './get_element_position_data' ; import { getElementPositionAndAttributes } from './get_element_position_data' ; import { getNumberOfItems } from './get_number_of_items' ; import { getRenderErrors } from './get_render_errors' ; import { getScreenshots } from './get_screenshots' ; import type { Screenshot } from './get_screenshots' ; import { getTimeRange } from './get_time_range' ; import { injectCustomCss } from './inject_css' ; import { openUrl } from './open_url' ; import type { UrlOrUrlWithContext } from './open_url' ; import { waitForRenderComplete } from './wait_for_render' ; import { waitForVisualizations } from './wait_for_visualizations' ; export interface PhaseTimeouts { /**
     * Open URL phase timeout.
     */ openUrl : number ; /**
     * Timeout of the page readiness phase.
     */ waitForElements : number ; /**
     * Timeout of the page render phase.
     */ renderComplete : number ; /**
     * An additional delay to wait until the visualizations are ready.
     */ loadDelay : number ; } export interface ScreenshotObservableOptions { /**
     * The browser timezone that will be emulated in the browser instance.
     * This option should be used to keep timezone on server and client in sync.
     */ browserTimezone ? : string ; /**
     * Custom headers to be sent with each request.
     */ conditionalHeaders : ConditionalHeaders ; /**
     * Timeouts for each phase of the screenshot.
     */ timeouts : PhaseTimeouts ; /**
     * The list or URL to take screenshots of.
     * Every item can either be a string or a tuple containing a URL and a context.
     */ urls : UrlOrUrlWithContext [ ] ; } export interface ScreenshotObservableResult { /**
     * Used time range filter.
     */ timeRange : string | null ; /**
     * Taken screenshots.
     */ screenshots : Screenshot [ ] ; /**
     * Error that occurred during the screenshotting.
     */ error ? : Error ; /**
     * Individual visualizations might encounter errors at runtime. If there are any they are added to this
     * field. Any text captured here is intended to be shown to the user for debugging purposes, reporting
     * does no further sanitization on these strings.
     */ renderErrors ? : string [ ] ; internal elementsPositionAndAttributes ? : ElementsPositionAndAttribute [ ] ; } interface PageSetupResults { elementsPositionAndAttributes : ElementsPositionAndAttribute [ ] | null ; timeRange : string | null ; error ? : Error ; } const DEFAULT_SCREENSHOT_CLIP_HEIGHT = 1200 ; const DEFAULT_SCREENSHOT_CLIP_WIDTH = 1800 ; const getDefaultElementPosition = ( dimensions ) => { const height = dimensions ?. height || DEFAULT_SCREENSHOT_CLIP_HEIGHT ; const width = dimensions ?. width || DEFAULT_SCREENSHOT_CLIP_WIDTH ; return [ { position : { boundingClientRect : { top : 0 , left : 0 , height , width } , scroll : { x : 0 , y : 0 } , } , attributes : { } , } , ] ; } ; const getDefaultViewPort = ( ) => ( { height : DEFAULT_SCREENSHOT_CLIP_HEIGHT , width : DEFAULT_SCREENSHOT_CLIP_WIDTH , zoom : 1 , } ) ; export class ScreenshotObservableHandler { constructor ( private readonly driver , private readonly logger , private readonly layout , private options ) { } public waitUntil < O > ( timeoutValue , label ) { return ( source ) => source . pipe ( catchError ( ( error ) => { throw new Error ( `The "${ label }" phase encountered an error: ${ error }` ) ; } ) , timeoutWith ( timeoutValue , throwError ( new Error ( `The "${ label }" phase took longer than ${ timeoutValue / 1000 } seconds.` ) ) ) ) ; } private openUrl ( index , url ) { return defer ( ( ) => openUrl ( this . driver , this . logger , this . options . timeouts . openUrl , index , url , this . options . conditionalHeaders , this . layout . id as ScreenshotModeLayout ) ) . pipe ( this . waitUntil ( this . options . timeouts . openUrl , 'open URL' ) ) ; } private waitForElements ( ) { const driver = this . driver ; const waitTimeout = this . options . timeouts . waitForElements ; return defer ( ( ) => getNumberOfItems ( driver , this . logger , waitTimeout , this . layout ) ) . pipe ( mergeMap ( ( itemsCount ) => { const viewport = this . layout . getViewport ( itemsCount ) || getDefaultViewPort ( ) ; return forkJoin ( [ driver . setViewport ( viewport , this . logger ) , waitForVisualizations ( driver , this . logger , waitTimeout , itemsCount , this . layout ) , ] ) ; } ) , this . waitUntil ( waitTimeout , 'wait for elements' ) ) ; } private completeRender ( apmTrans ) { const driver = this . driver ; const layout = this . layout ; const logger = this . logger ; return defer ( async ( ) => { await injectCustomCss ( driver , logger , layout ) ; const apmPositionElements = apmTrans ?. startSpan ( 'position-elements' , 'correction' ) ; await layout . positionElements ?. ( driver , logger ) ; apmPositionElements ?. end ( ) ; await waitForRenderComplete ( driver , logger , this . options . timeouts . loadDelay , layout ) ; } ) . pipe ( mergeMap ( ( ) => forkJoin ( { timeRange : getTimeRange ( driver , logger , layout ) , elementsPositionAndAttributes : getElementPositionAndAttributes ( driver , logger , layout ) , renderErrors : getRenderErrors ( driver , logger , layout ) , } ) ) , this . waitUntil ( this . options . timeouts . renderComplete , 'render complete' ) ) ; } public setupPage ( index , url , apmTrans ) { return this . openUrl ( index , url ) . pipe ( switchMapTo ( this . waitForElements ( ) ) , switchMapTo ( this . completeRender ( apmTrans ) ) ) ; } public getScreenshots ( ) { return ( withRenderComplete ) => withRenderComplete . pipe ( mergeMap ( async ( data ) : Promise < ScreenshotObservableResult > => { this . checkPageIsOpen ( ) ; const elements = data . elementsPositionAndAttributes ?? getDefaultElementPosition ( this . layout . getViewport ( 1 ) ) ; const screenshots = await getScreenshots ( this . driver , this . logger , elements ) ; const { timeRange , error : setupError } = data ; return { timeRange , screenshots , error : setupError , elementsPositionAndAttributes : elements , } ; } ) ) ; } public checkPageIsOpen ( ) { if ( ! this . driver . isPageOpen ( ) ) { throw getChromiumDisconnectedError ( ) ; } } }