From 97f856a0b8571e6ba21a176a207130e7319e8788 Mon Sep 17 00:00:00 2001 From: Tim Sullivan Date: Fri, 6 Mar 2020 10:27:43 -0700 Subject: [PATCH] [Reporting/Screenshots] Handle page setup errors and capture the page, don't fail the job (#58683) (#59519) * [Reporting] Handle error if intercepted request could not be continued * [Reporting/Screenshots] Handle page setup errors and capture the page with errors shown * show warnings in UI * i18n todos * Cleanup an old troubleshooting task * set the default for all new timeout settings to 30 seconds * fix some tests * update error strings * Cleanup 2 * fix tests 2 * polish the job info map status items * More error message updating * Log the error that was caught * Oops fix ts * add documentation * fix i18n * fix mocha test * use the openUrl timeout as the default for navigation * fix comment Co-authored-by: Elastic Machine Co-authored-by: Elastic Machine --- docs/settings/reporting-settings.asciidoc | 22 +++ .../__snapshots__/index.test.js.snap | 20 ++ x-pack/legacy/plugins/reporting/config.ts | 11 ++ .../export_types/common/constants.ts | 2 +- .../export_types/common/layouts/layout.ts | 8 +- .../common/layouts/preserve_layout.ts | 4 +- .../common/lib/screenshots/check_for_toast.ts | 58 ------ .../common/lib/screenshots/constants.ts | 2 +- .../screenshots/get_element_position_data.ts | 89 +++++---- .../lib/screenshots/get_number_of_items.ts | 72 +++++-- .../common/lib/screenshots/get_screenshots.ts | 40 ++-- .../common/lib/screenshots/inject_css.ts | 40 ++-- .../common/lib/screenshots/observable.test.ts | 60 ++++-- .../common/lib/screenshots/observable.ts | 77 +++++--- .../common/lib/screenshots/open_url.ts | 37 +++- .../common/lib/screenshots/scan_page.ts | 30 --- .../common/lib/screenshots/types.ts | 7 + .../lib/screenshots/wait_for_dom_elements.ts | 34 ---- .../common/lib/screenshots/wait_for_render.ts | 15 +- .../screenshots/wait_for_visualizations.ts | 67 +++++++ .../png/server/execute_job/index.test.js | 2 +- .../png/server/execute_job/index.ts | 19 +- .../png/server/lib/generate_png.ts | 25 ++- .../server/execute_job/index.test.js | 2 +- .../printable_pdf/server/execute_job/index.ts | 26 ++- .../printable_pdf/server/lib/generate_pdf.ts | 33 ++-- .../public/components/report_info_button.tsx | 181 +++++++++--------- .../public/components/report_listing.tsx | 24 ++- .../reporting/public/lib/job_queue_client.ts | 1 + .../chromium/driver/chromium_driver.ts | 120 +++++++----- .../browsers/chromium/driver_factory/index.ts | 29 ++- .../server/browsers/chromium/index.ts | 13 +- .../browsers/create_browser_driver_factory.ts | 10 +- .../reporting/server/lib/esqueue/worker.js | 10 +- .../create_mock_browserdriverfactory.ts | 18 +- .../create_mock_layoutinstance.ts | 1 - .../plugins/reporting/test_helpers/index.ts | 2 +- x-pack/legacy/plugins/reporting/types.d.ts | 8 +- .../translations/translations/ja-JP.json | 4 - .../translations/translations/zh-CN.json | 4 - 40 files changed, 724 insertions(+), 503 deletions(-) delete mode 100644 x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/check_for_toast.ts delete mode 100644 x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/scan_page.ts delete mode 100644 x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/wait_for_dom_elements.ts create mode 100644 x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/wait_for_visualizations.ts diff --git a/docs/settings/reporting-settings.asciidoc b/docs/settings/reporting-settings.asciidoc index a9fa2bd18d315..9a45fb9ab1d0c 100644 --- a/docs/settings/reporting-settings.asciidoc +++ b/docs/settings/reporting-settings.asciidoc @@ -95,6 +95,8 @@ index for any pending Reporting jobs. Defaults to `3000` (3 seconds). [[xpack-reporting-q-timeout]]`xpack.reporting.queue.timeout`:: How long each worker has to produce a report. If your machine is slow or under heavy load, you might need to increase this timeout. Specified in milliseconds. +If a Reporting job execution time goes over this time limit, the job will be +marked as a failure and there will not be a download available. Defaults to `120000` (two minutes). [float] @@ -104,6 +106,26 @@ Defaults to `120000` (two minutes). Reporting works by capturing screenshots from Kibana. The following settings control the capturing process. +`xpack.reporting.capture.timeouts.openUrl`:: +How long to allow the Reporting browser to wait for the initial data of the +Kibana page to load. Defaults to `30000` (30 seconds). + +`xpack.reporting.capture.timeouts.waitForElements`:: +How long to allow the Reporting browser to wait for the visualization panels to +load on the Kibana page. Defaults to `30000` (30 seconds). + +`xpack.reporting.capture.timeouts.renderComplete`:: +How long to allow the Reporting brwoser to wait for each visualization to +signal that it is done renderings. Defaults to `30000` (30 seconds). + +[NOTE] +============ +If any timeouts from `xpack.reporting.capture.timeouts.*` settings occur when +running a report job, Reporting will log the error and try to continue +capturing the page with a screenshot. As a result, a download will be +available, but there will likely be errors in the visualizations in the report. +============ + `xpack.reporting.capture.maxAttempts`:: If capturing a report fails for any reason, Kibana will re-attempt othe reporting job, as many times as this setting. Defaults to `3`. diff --git a/x-pack/legacy/plugins/reporting/__snapshots__/index.test.js.snap b/x-pack/legacy/plugins/reporting/__snapshots__/index.test.js.snap index 469f5e6e7b3c6..757677f1d4f82 100644 --- a/x-pack/legacy/plugins/reporting/__snapshots__/index.test.js.snap +++ b/x-pack/legacy/plugins/reporting/__snapshots__/index.test.js.snap @@ -47,6 +47,11 @@ Object { }, "settleTime": 1000, "timeout": 20000, + "timeouts": Object { + "openUrl": 30000, + "renderComplete": 30000, + "waitForElements": 30000, + }, "viewport": Object { "height": 1200, "width": 1950, @@ -138,6 +143,11 @@ Object { }, "settleTime": 1000, "timeout": 20000, + "timeouts": Object { + "openUrl": 30000, + "renderComplete": 30000, + "waitForElements": 30000, + }, "viewport": Object { "height": 1200, "width": 1950, @@ -228,6 +238,11 @@ Object { }, "settleTime": 1000, "timeout": 20000, + "timeouts": Object { + "openUrl": 30000, + "renderComplete": 30000, + "waitForElements": 30000, + }, "viewport": Object { "height": 1200, "width": 1950, @@ -319,6 +334,11 @@ Object { }, "settleTime": 1000, "timeout": 20000, + "timeouts": Object { + "openUrl": 30000, + "renderComplete": 30000, + "waitForElements": 30000, + }, "viewport": Object { "height": 1200, "width": 1950, diff --git a/x-pack/legacy/plugins/reporting/config.ts b/x-pack/legacy/plugins/reporting/config.ts index 34fc1f452fbc0..211fa70301bbf 100644 --- a/x-pack/legacy/plugins/reporting/config.ts +++ b/x-pack/legacy/plugins/reporting/config.ts @@ -31,6 +31,17 @@ export async function config(Joi: any) { .default(120000), }).default(), capture: Joi.object({ + timeouts: Joi.object({ + openUrl: Joi.number() + .integer() + .default(30000), + waitForElements: Joi.number() + .integer() + .default(30000), + renderComplete: Joi.number() + .integer() + .default(30000), + }).default(), networkPolicy: Joi.object({ enabled: Joi.boolean().default(true), rules: Joi.array() diff --git a/x-pack/legacy/plugins/reporting/export_types/common/constants.ts b/x-pack/legacy/plugins/reporting/export_types/common/constants.ts index 02a3e787da750..254cfbaa878bd 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/constants.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/constants.ts @@ -9,4 +9,4 @@ export const LayoutTypes = { PRINT: 'print', }; -export const WAITFOR_SELECTOR = '.application'; +export const PAGELOAD_SELECTOR = '.application'; diff --git a/x-pack/legacy/plugins/reporting/export_types/common/layouts/layout.ts b/x-pack/legacy/plugins/reporting/export_types/common/layouts/layout.ts index 54fae60a0773c..2c43517dbcaa9 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/layouts/layout.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/layouts/layout.ts @@ -27,7 +27,6 @@ export interface LayoutSelectorDictionary { renderComplete: string; itemsCountAttribute: string; timefilterDurationAttribute: string; - toastHeader: string; } export interface PdfImageSize { @@ -40,7 +39,6 @@ export const getDefaultLayoutSelectors = (): LayoutSelectorDictionary => ({ renderComplete: '[data-shared-item]', itemsCountAttribute: 'data-shared-items-count', timefilterDurationAttribute: 'data-shared-timefilter-duration', - toastHeader: '[data-test-subj="euiToastHeader"]', }); export abstract class Layout { @@ -75,9 +73,11 @@ export interface LayoutParams { dimensions: Size; } -export type LayoutInstance = Layout & { +interface LayoutSelectors { // Fields that are not part of Layout: the instances // independently implement these fields on their own selectors: LayoutSelectorDictionary; positionElements?: (browser: HeadlessChromiumDriver, logger: LevelLogger) => Promise; -}; +} + +export type LayoutInstance = Layout & LayoutSelectors & Size; diff --git a/x-pack/legacy/plugins/reporting/export_types/common/layouts/preserve_layout.ts b/x-pack/legacy/plugins/reporting/export_types/common/layouts/preserve_layout.ts index cfa421b6f66ab..07dbba7d25883 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/layouts/preserve_layout.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/layouts/preserve_layout.ts @@ -19,8 +19,8 @@ const ZOOM: number = 2; export class PreserveLayout extends Layout { public readonly selectors: LayoutSelectorDictionary = getDefaultLayoutSelectors(); public readonly groupCount = 1; - private readonly height: number; - private readonly width: number; + public readonly height: number; + public readonly width: number; private readonly scaledHeight: number; private readonly scaledWidth: number; diff --git a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/check_for_toast.ts b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/check_for_toast.ts deleted file mode 100644 index c888870bd2bc3..0000000000000 --- a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/check_for_toast.ts +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { i18n } from '@kbn/i18n'; -import { ElementHandle } from 'puppeteer'; -import { HeadlessChromiumDriver as HeadlessBrowser } from '../../../../server/browsers'; -import { LevelLogger } from '../../../../server/lib'; -import { LayoutInstance } from '../../layouts/layout'; -import { CONTEXT_CHECKFORTOASTMESSAGE } from './constants'; - -export const checkForToastMessage = async ( - browser: HeadlessBrowser, - layout: LayoutInstance, - logger: LevelLogger -): Promise> => { - return await browser - .waitForSelector(layout.selectors.toastHeader, { silent: true }, logger) - .then(async () => { - // Check for a toast message on the page. If there is one, capture the - // message and throw an error, to fail the screenshot. - const toastHeaderText: string = await browser.evaluate( - { - fn: selector => { - const nodeList = document.querySelectorAll(selector); - return nodeList.item(0).innerText; - }, - args: [layout.selectors.toastHeader], - }, - { context: CONTEXT_CHECKFORTOASTMESSAGE }, - logger - ); - - // Log an error to track the event in kibana server logs - logger.error( - i18n.translate( - 'xpack.reporting.exportTypes.printablePdf.screenshots.unexpectedErrorMessage', - { - defaultMessage: 'Encountered an unexpected message on the page: {toastHeaderText}', - values: { toastHeaderText }, - } - ) - ); - - // Throw an error to fail the screenshot job with a message - throw new Error( - i18n.translate( - 'xpack.reporting.exportTypes.printablePdf.screenshots.unexpectedErrorMessage', - { - defaultMessage: 'Encountered an unexpected message on the page: {toastHeaderText}', - values: { toastHeaderText }, - } - ) - ); - }); -}; diff --git a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/constants.ts b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/constants.ts index bbc97ca57940c..a3faf9337524e 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/constants.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/constants.ts @@ -9,6 +9,6 @@ export const CONTEXT_INJECTCSS = 'InjectCss'; export const CONTEXT_WAITFORRENDER = 'WaitForRender'; export const CONTEXT_GETTIMERANGE = 'GetTimeRange'; export const CONTEXT_ELEMENTATTRIBUTES = 'ElementPositionAndAttributes'; -export const CONTEXT_CHECKFORTOASTMESSAGE = 'CheckForToastMessage'; export const CONTEXT_WAITFORELEMENTSTOBEINDOM = 'WaitForElementsToBeInDOM'; export const CONTEXT_SKIPTELEMETRY = 'SkipTelemetry'; +export const CONTEXT_READMETADATA = 'ReadVisualizationsMetadata'; diff --git a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_element_position_data.ts b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_element_position_data.ts index 4302f4c631e3c..2f93765165e50 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_element_position_data.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_element_position_data.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { i18n } from '@kbn/i18n'; import { HeadlessChromiumDriver as HeadlessBrowser } from '../../../../server/browsers'; import { LayoutInstance } from '../../layouts/layout'; import { AttributesMap, ElementsPositionAndAttribute } from './types'; @@ -14,50 +15,58 @@ export const getElementPositionAndAttributes = async ( browser: HeadlessBrowser, layout: LayoutInstance, logger: Logger -): Promise => { - const elementsPositionAndAttributes: ElementsPositionAndAttribute[] = await browser.evaluate( - { - fn: (selector: string, attributes: any) => { - const elements: NodeListOf = document.querySelectorAll(selector); +): Promise => { + const { screenshot: screenshotSelector } = layout.selectors; // data-shared-items-container + let elementsPositionAndAttributes: ElementsPositionAndAttribute[] | null; + try { + elementsPositionAndAttributes = await browser.evaluate( + { + fn: (selector, attributes) => { + const elements: NodeListOf = document.querySelectorAll(selector); - // NodeList isn't an array, just an iterator, unable to use .map/.forEach - const results: ElementsPositionAndAttribute[] = []; - for (let i = 0; i < elements.length; i++) { - const element = elements[i]; - const boundingClientRect = element.getBoundingClientRect() as DOMRect; - results.push({ - position: { - boundingClientRect: { - // modern browsers support x/y, but older ones don't - top: boundingClientRect.y || boundingClientRect.top, - left: boundingClientRect.x || boundingClientRect.left, - width: boundingClientRect.width, - height: boundingClientRect.height, + // NodeList isn't an array, just an iterator, unable to use .map/.forEach + const results: ElementsPositionAndAttribute[] = []; + for (let i = 0; i < elements.length; i++) { + const element = elements[i]; + const boundingClientRect = element.getBoundingClientRect() as DOMRect; + results.push({ + position: { + boundingClientRect: { + // modern browsers support x/y, but older ones don't + top: boundingClientRect.y || boundingClientRect.top, + left: boundingClientRect.x || boundingClientRect.left, + width: boundingClientRect.width, + height: boundingClientRect.height, + }, + scroll: { + x: window.scrollX, + y: window.scrollY, + }, }, - scroll: { - x: window.scrollX, - y: window.scrollY, - }, - }, - attributes: Object.keys(attributes).reduce((result: AttributesMap, key) => { - const attribute = attributes[key]; - (result as any)[key] = element.getAttribute(attribute); - return result; - }, {} as AttributesMap), - }); - } - return results; + attributes: Object.keys(attributes).reduce((result: AttributesMap, key) => { + const attribute = attributes[key]; + (result as any)[key] = element.getAttribute(attribute); + return result; + }, {} as AttributesMap), + }); + } + return results; + }, + args: [screenshotSelector, { title: 'data-title', description: 'data-description' }], }, - args: [layout.selectors.screenshot, { title: 'data-title', description: 'data-description' }], - }, - { context: CONTEXT_ELEMENTATTRIBUTES }, - logger - ); - - if (elementsPositionAndAttributes.length === 0) { - throw new Error( - `No shared items containers were found on the page! Reporting requires a container element with the '${layout.selectors.screenshot}' attribute on the page.` + { context: CONTEXT_ELEMENTATTRIBUTES }, + logger ); + + if (!elementsPositionAndAttributes || elementsPositionAndAttributes.length === 0) { + throw new Error( + i18n.translate('xpack.reporting.screencapture.noElements', { + defaultMessage: `An error occurred while reading the page for visualization panels: no panels were found.`, + }) + ); + } + } catch (err) { + elementsPositionAndAttributes = null; } return elementsPositionAndAttributes; diff --git a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_number_of_items.ts b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_number_of_items.ts index 1beae719cd6b0..16eb433e8a75e 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_number_of_items.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_number_of_items.ts @@ -4,38 +4,72 @@ * you may not use this file except in compliance with the Elastic License. */ +import { i18n } from '@kbn/i18n'; import { HeadlessChromiumDriver as HeadlessBrowser } from '../../../../server/browsers'; import { LevelLogger } from '../../../../server/lib'; +import { ServerFacade } from '../../../../types'; import { LayoutInstance } from '../../layouts/layout'; -import { CONTEXT_GETNUMBEROFITEMS } from './constants'; +import { CONTEXT_GETNUMBEROFITEMS, CONTEXT_READMETADATA } from './constants'; export const getNumberOfItems = async ( + server: ServerFacade, browser: HeadlessBrowser, layout: LayoutInstance, logger: LevelLogger ): Promise => { - logger.debug('determining how many rendered items to wait for'); + const config = server.config(); + const { renderComplete: renderCompleteSelector, itemsCountAttribute } = layout.selectors; + let itemsCount: number; - // returns the value of the `itemsCountAttribute` if it's there, otherwise - // we just count the number of `itemSelector` - const itemsCount: number = await browser.evaluate( - { - fn: (selector, countAttribute) => { - const elementWithCount = document.querySelector(`[${countAttribute}]`); - if (elementWithCount && elementWithCount != null) { - const count = elementWithCount.getAttribute(countAttribute); - if (count && count != null) { - return parseInt(count, 10); + logger.debug( + i18n.translate('xpack.reporting.screencapture.logWaitingForElements', { + defaultMessage: 'waiting for elements or items count attribute; or not found to interrupt', + }) + ); + + try { + // the dashboard is using the `itemsCountAttribute` attribute to let us + // know how many items to expect since gridster incrementally adds panels + // we have to use this hint to wait for all of them + await browser.waitForSelector( + `${renderCompleteSelector},[${itemsCountAttribute}]`, + { timeout: config.get('xpack.reporting.capture.timeouts.waitForElements') }, + { context: CONTEXT_READMETADATA }, + logger + ); + + // returns the value of the `itemsCountAttribute` if it's there, otherwise + // we just count the number of `itemSelector`: the number of items already rendered + itemsCount = await browser.evaluate( + { + fn: (selector, countAttribute) => { + const elementWithCount = document.querySelector(`[${countAttribute}]`); + if (elementWithCount && elementWithCount != null) { + const count = elementWithCount.getAttribute(countAttribute); + if (count && count != null) { + return parseInt(count, 10); + } } - } - return document.querySelectorAll(selector).length; + return document.querySelectorAll(selector).length; + }, + args: [renderCompleteSelector, itemsCountAttribute], }, - args: [layout.selectors.renderComplete, layout.selectors.itemsCountAttribute], - }, - { context: CONTEXT_GETNUMBEROFITEMS }, - logger - ); + { context: CONTEXT_GETNUMBEROFITEMS }, + logger + ); + } catch (err) { + throw new Error( + i18n.translate('xpack.reporting.screencapture.readVisualizationsError', { + defaultMessage: `An error occurred when trying to read the page for visualization panel info. You may need to increase '{configKey}'. {error}`, + values: { + error: err, + configKey: 'xpack.reporting.capture.timeouts.waitForElements', + }, + }) + ); + itemsCount = 1; + } return itemsCount; }; diff --git a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_screenshots.ts b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_screenshots.ts index b21d1e752ba3f..d50ac64743f07 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_screenshots.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_screenshots.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { i18n } from '@kbn/i18n'; import { HeadlessChromiumDriver as HeadlessBrowser } from '../../../../server/browsers'; import { LevelLogger } from '../../../../server/lib'; import { Screenshot, ElementsPositionAndAttribute } from './types'; @@ -12,21 +13,29 @@ const getAsyncDurationLogger = (logger: LevelLogger) => { return async (description: string, promise: Promise) => { const start = Date.now(); const result = await promise; - logger.debug(`${description} took ${Date.now() - start}ms`); + logger.debug( + i18n.translate('xpack.reporting.screencapture.asyncTook', { + defaultMessage: '{description} took {took}ms', + values: { + description, + took: Date.now() - start, + }, + }) + ); return result; }; }; -export const getScreenshots = async ({ - browser, - elementsPositionAndAttributes, - logger, -}: { - logger: LevelLogger; - browser: HeadlessBrowser; - elementsPositionAndAttributes: ElementsPositionAndAttribute[]; -}): Promise => { - logger.info(`taking screenshots`); +export const getScreenshots = async ( + browser: HeadlessBrowser, + elementsPositionAndAttributes: ElementsPositionAndAttribute[], + logger: LevelLogger +): Promise => { + logger.info( + i18n.translate('xpack.reporting.screencapture.takingScreenshots', { + defaultMessage: `taking screenshots`, + }) + ); const asyncDurationLogger = getAsyncDurationLogger(logger); const screenshots: Screenshot[] = []; @@ -45,7 +54,14 @@ export const getScreenshots = async ({ }); } - logger.info(`screenshots taken: ${screenshots.length}`); + logger.info( + i18n.translate('xpack.reporting.screencapture.screenshotsTaken', { + defaultMessage: `screenshots taken: {numScreenhots}`, + values: { + numScreenhots: screenshots.length, + }, + }) + ); return screenshots; }; diff --git a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/inject_css.ts b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/inject_css.ts index 40204804a276f..cb2673e85186b 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/inject_css.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/inject_css.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { i18n } from '@kbn/i18n'; import fs from 'fs'; import { promisify } from 'util'; import { LevelLogger } from '../../../../server/lib'; @@ -18,21 +19,34 @@ export const injectCustomCss = async ( layout: Layout, logger: LevelLogger ): Promise => { - logger.debug('injecting custom css'); + logger.debug( + i18n.translate('xpack.reporting.screencapture.injectingCss', { + defaultMessage: 'injecting custom css', + }) + ); const filePath = layout.getCssOverridesPath(); const buffer = await fsp.readFile(filePath); - await browser.evaluate( - { - fn: css => { - const node = document.createElement('style'); - node.type = 'text/css'; - node.innerHTML = css; // eslint-disable-line no-unsanitized/property - document.getElementsByTagName('head')[0].appendChild(node); + try { + await browser.evaluate( + { + fn: css => { + const node = document.createElement('style'); + node.type = 'text/css'; + node.innerHTML = css; // eslint-disable-line no-unsanitized/property + document.getElementsByTagName('head')[0].appendChild(node); + }, + args: [buffer.toString()], }, - args: [buffer.toString()], - }, - { context: CONTEXT_INJECTCSS }, - logger - ); + { context: CONTEXT_INJECTCSS }, + logger + ); + } catch (err) { + throw new Error( + i18n.translate('xpack.reporting.screencapture.injectCss', { + defaultMessage: `An error occurred when trying to update Kibana CSS for reporting. {error}`, + values: { error: err }, + }) + ); + } }; diff --git a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/observable.test.ts b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/observable.test.ts index 9f8e218f4f614..13d07bcdd6baf 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/observable.test.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/observable.test.ts @@ -23,7 +23,6 @@ import { createMockBrowserDriverFactory, createMockLayoutInstance, createMockServer, - mockSelectors, } from '../../../../test_helpers'; import { ConditionalHeaders, HeadlessChromiumDriver } from '../../../../types'; import { screenshotsObservableFactory } from './observable'; @@ -61,6 +60,7 @@ describe('Screenshot Observable Pipeline', () => { expect(result).toMatchInlineSnapshot(` Array [ Object { + "error": undefined, "screenshots": Array [ Object { "base64EncodedData": "allyourBase64 of boundingClientRect,scroll", @@ -98,6 +98,7 @@ describe('Screenshot Observable Pipeline', () => { expect(result).toMatchInlineSnapshot(` Array [ Object { + "error": undefined, "screenshots": Array [ Object { "base64EncodedData": "allyourBase64 screenshots", @@ -108,6 +109,7 @@ describe('Screenshot Observable Pipeline', () => { "timeRange": "Default GetTimeRange Result", }, Object { + "error": undefined, "screenshots": Array [ Object { "base64EncodedData": "allyourBase64 screenshots", @@ -122,15 +124,10 @@ describe('Screenshot Observable Pipeline', () => { }); describe('error handling', () => { - it('fails if error toast message is found', async () => { + it('recovers if waitForSelector fails', async () => { // mock implementations const mockWaitForSelector = jest.fn().mockImplementation((selectorArg: string) => { - const { toastHeader } = mockSelectors; - if (selectorArg === toastHeader) { - return Promise.resolve(true); - } - // make the error toast message get found before anything else - return Rx.interval(100).toPromise(); + throw new Error('Mock error!'); }); // mocks @@ -153,12 +150,35 @@ describe('Screenshot Observable Pipeline', () => { }).toPromise(); }; - await expect(getScreenshot()).rejects.toMatchInlineSnapshot( - `[Error: Encountered an unexpected message on the page: Toast Message]` - ); + await expect(getScreenshot()).resolves.toMatchInlineSnapshot(` + Array [ + Object { + "error": [Error: An error occurred when trying to read the page for visualization panel info. You may need to increase 'xpack.reporting.capture.timeouts.waitForElements'. Error: Mock error!], + "screenshots": Array [ + Object { + "base64EncodedData": "allyourBase64 of boundingClientRect,scroll", + "description": undefined, + "title": undefined, + }, + ], + "timeRange": null, + }, + Object { + "error": [Error: An error occurred when trying to read the page for visualization panel info. You may need to increase 'xpack.reporting.capture.timeouts.waitForElements'. Error: Mock error!], + "screenshots": Array [ + Object { + "base64EncodedData": "allyourBase64 of boundingClientRect,scroll", + "description": undefined, + "title": undefined, + }, + ], + "timeRange": null, + }, + ] + `); }); - it('fails if exit$ fires a timeout or error signal', async () => { + it('recovers if exit$ fires a timeout signal', async () => { // mocks const mockGetCreatePage = (driver: HeadlessChromiumDriver) => jest @@ -188,7 +208,21 @@ describe('Screenshot Observable Pipeline', () => { }).toPromise(); }; - await expect(getScreenshot()).rejects.toMatchInlineSnapshot(`"Instant timeout has fired!"`); + await expect(getScreenshot()).resolves.toMatchInlineSnapshot(` + Array [ + Object { + "error": "Instant timeout has fired!", + "screenshots": Array [ + Object { + "base64EncodedData": "allyourBase64 of boundingClientRect,scroll", + "description": undefined, + "title": undefined, + }, + ], + "timeRange": null, + }, + ] + `); }); }); }); diff --git a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/observable.ts b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/observable.ts index d429931602951..878a9d3b87393 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/observable.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/observable.ts @@ -5,19 +5,18 @@ */ import * as Rx from 'rxjs'; -import { concatMap, first, mergeMap, take, toArray } from 'rxjs/operators'; +import { catchError, concatMap, first, mergeMap, take, takeUntil, toArray } from 'rxjs/operators'; import { CaptureConfig, HeadlessChromiumDriverFactory, ServerFacade } from '../../../../types'; import { getElementPositionAndAttributes } from './get_element_position_data'; import { getNumberOfItems } from './get_number_of_items'; import { getScreenshots } from './get_screenshots'; import { getTimeRange } from './get_time_range'; -import { injectCustomCss } from './inject_css'; import { openUrl } from './open_url'; -import { scanPage } from './scan_page'; -import { ScreenshotObservableOpts, ScreenshotResults } from './types'; -import { waitForElementsToBeInDOM } from './wait_for_dom_elements'; -import { waitForRenderComplete } from './wait_for_render'; import { skipTelemetry } from './skip_telemetry'; +import { ScreenSetupData, ScreenshotObservableOpts, ScreenshotResults } from './types'; +import { waitForRenderComplete } from './wait_for_render'; +import { waitForVisualizations } from './wait_for_visualizations'; +import { injectCustomCss } from './inject_css'; export function screenshotsObservableFactory( server: ServerFacade, @@ -41,16 +40,16 @@ export function screenshotsObservableFactory( concatMap(url => { return create$.pipe( mergeMap(({ driver, exit$ }) => { - const screenshot$ = Rx.of(1).pipe( - mergeMap(() => openUrl(driver, url, conditionalHeaders, logger)), + const setup$: Rx.Observable = Rx.of(1).pipe( + takeUntil(exit$), + mergeMap(() => openUrl(server, driver, url, conditionalHeaders, logger)), mergeMap(() => skipTelemetry(driver, logger)), - mergeMap(() => scanPage(driver, layout, logger)), - mergeMap(() => getNumberOfItems(driver, layout, logger)), + mergeMap(() => getNumberOfItems(server, driver, layout, logger)), mergeMap(async itemsCount => { const viewport = layout.getViewport(itemsCount); await Promise.all([ driver.setViewport(viewport, logger), - waitForElementsToBeInDOM(driver, itemsCount, layout, logger), + waitForVisualizations(server, driver, itemsCount, layout, logger), ]); }), mergeMap(async () => { @@ -63,28 +62,35 @@ export function screenshotsObservableFactory( await layout.positionElements(driver, logger); } - await waitForRenderComplete(captureConfig, driver, layout, logger); + await waitForRenderComplete(driver, layout, captureConfig, logger); }), - mergeMap(() => getTimeRange(driver, layout, logger)), - mergeMap( - async (timeRange): Promise => { - const elementsPositionAndAttributes = await getElementPositionAndAttributes( - driver, - layout, - logger - ); - const screenshots = await getScreenshots({ - browser: driver, - elementsPositionAndAttributes, - logger, - }); + mergeMap(async () => { + return await Promise.all([ + getTimeRange(driver, layout, logger), + getElementPositionAndAttributes(driver, layout, logger), + ]).then(([timeRange, elementsPositionAndAttributes]) => ({ + elementsPositionAndAttributes, + timeRange, + })); + }), + catchError(err => { + logger.error(err); + return Rx.of({ elementsPositionAndAttributes: null, timeRange: null, error: err }); + }) + ); - return { timeRange, screenshots }; + return setup$.pipe( + mergeMap( + async (data: ScreenSetupData): Promise => { + const elements = data.elementsPositionAndAttributes + ? data.elementsPositionAndAttributes + : getDefaultElementPosition(layout.getViewport(1)); + const screenshots = await getScreenshots(driver, elements, logger); + const { timeRange, error: setupError } = data; + return { timeRange, screenshots, error: setupError }; } ) ); - - return Rx.race(screenshot$, exit$); }), first() ); @@ -94,3 +100,18 @@ export function screenshotsObservableFactory( ); }; } + +/* + * If an error happens setting up the page, we don't know if there actually + * are any visualizations showing. These defaults should help capture the page + * enough for the user to see the error themselves + */ +const getDefaultElementPosition = ({ height, width }: { height: number; width: number }) => [ + { + position: { + boundingClientRect: { top: 0, left: 0, height, width }, + scroll: { x: 0, y: 0 }, + }, + attributes: {}, + }, +]; diff --git a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/open_url.ts b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/open_url.ts index e465499f839f9..fbae1f91a7a6a 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/open_url.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/open_url.ts @@ -4,23 +4,40 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ConditionalHeaders } from '../../../../types'; +import { i18n } from '@kbn/i18n'; +import { ConditionalHeaders, ServerFacade } from '../../../../types'; import { LevelLogger } from '../../../../server/lib'; import { HeadlessChromiumDriver as HeadlessBrowser } from '../../../../server/browsers'; -import { WAITFOR_SELECTOR } from '../../constants'; +import { PAGELOAD_SELECTOR } from '../../constants'; export const openUrl = async ( + server: ServerFacade, browser: HeadlessBrowser, url: string, conditionalHeaders: ConditionalHeaders, logger: LevelLogger ): Promise => { - await browser.open( - url, - { - conditionalHeaders, - waitForSelector: WAITFOR_SELECTOR, - }, - logger - ); + const config = server.config(); + + try { + await browser.open( + url, + { + conditionalHeaders, + waitForSelector: PAGELOAD_SELECTOR, + timeout: config.get('xpack.reporting.capture.timeouts.openUrl'), + }, + logger + ); + } catch (err) { + throw new Error( + i18n.translate('xpack.reporting.screencapture.couldntLoadKibana', { + defaultMessage: `An error occurred when trying to open the Kibana URL. You may need to increase '{configKey}'. {error}`, + values: { + configKey: 'xpack.reporting.capture.timeouts.openUrl', + error: err, + }, + }) + ); + } }; diff --git a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/scan_page.ts b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/scan_page.ts deleted file mode 100644 index 010ffe8f23afc..0000000000000 --- a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/scan_page.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import * as Rx from 'rxjs'; -import { HeadlessChromiumDriver } from '../../../../server/browsers'; -import { LevelLogger } from '../../../../server/lib'; -import { LayoutInstance } from '../../layouts/layout'; -import { checkForToastMessage } from './check_for_toast'; - -export function scanPage( - browser: HeadlessChromiumDriver, - layout: LayoutInstance, - logger: LevelLogger -) { - logger.debug('waiting for elements or items count attribute; or not found to interrupt'); - - // the dashboard is using the `itemsCountAttribute` attribute to let us - // know how many items to expect since gridster incrementally adds panels - // we have to use this hint to wait for all of them - const renderSuccess = browser.waitForSelector( - `${layout.selectors.renderComplete},[${layout.selectors.itemsCountAttribute}]`, - {}, - logger - ); - const renderError = checkForToastMessage(browser, layout, logger); - return Rx.race(Rx.from(renderSuccess), Rx.from(renderError)); -} diff --git a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/types.ts b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/types.ts index 78cd42f0cae2f..ab81a952f345c 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/types.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/types.ts @@ -35,7 +35,14 @@ export interface Screenshot { description: string; } +export interface ScreenSetupData { + elementsPositionAndAttributes: ElementsPositionAndAttribute[] | null; + timeRange: TimeRange | null; + error?: Error; +} + export interface ScreenshotResults { timeRange: TimeRange | null; screenshots: Screenshot[]; + error?: Error; } diff --git a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/wait_for_dom_elements.ts b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/wait_for_dom_elements.ts deleted file mode 100644 index c958585f78e0d..0000000000000 --- a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/wait_for_dom_elements.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { HeadlessChromiumDriver as HeadlessBrowser } from '../../../../server/browsers'; -import { LevelLogger } from '../../../../server/lib'; -import { LayoutInstance } from '../../layouts/layout'; -import { CONTEXT_WAITFORELEMENTSTOBEINDOM } from './constants'; - -export const waitForElementsToBeInDOM = async ( - browser: HeadlessBrowser, - itemsCount: number, - layout: LayoutInstance, - logger: LevelLogger -): Promise => { - logger.debug(`waiting for ${itemsCount} rendered elements to be in the DOM`); - - await browser.waitFor( - { - fn: selector => { - return document.querySelectorAll(selector).length; - }, - args: [layout.selectors.renderComplete], - toEqual: itemsCount, - }, - { context: CONTEXT_WAITFORELEMENTSTOBEINDOM }, - logger - ); - - logger.info(`found ${itemsCount} rendered elements in the DOM`); - return itemsCount; -}; diff --git a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/wait_for_render.ts b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/wait_for_render.ts index 632f008ca63bc..2f6dc2829dfd8 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/wait_for_render.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/wait_for_render.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { i18n } from '@kbn/i18n'; import { CaptureConfig } from '../../../../types'; import { HeadlessChromiumDriver as HeadlessBrowser } from '../../../../server/browsers'; import { LevelLogger } from '../../../../server/lib'; @@ -11,12 +12,16 @@ import { LayoutInstance } from '../../layouts/layout'; import { CONTEXT_WAITFORRENDER } from './constants'; export const waitForRenderComplete = async ( - captureConfig: CaptureConfig, browser: HeadlessBrowser, layout: LayoutInstance, + captureConfig: CaptureConfig, logger: LevelLogger ) => { - logger.debug('waiting for rendering to complete'); + logger.debug( + i18n.translate('xpack.reporting.screencapture.waitingForRenderComplete', { + defaultMessage: 'waiting for rendering to complete', + }) + ); return await browser .evaluate( @@ -66,6 +71,10 @@ export const waitForRenderComplete = async ( logger ) .then(() => { - logger.debug('rendering is complete'); + logger.debug( + i18n.translate('xpack.reporting.screencapture.renderIsComplete', { + defaultMessage: 'rendering is complete', + }) + ); }); }; diff --git a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/wait_for_visualizations.ts b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/wait_for_visualizations.ts new file mode 100644 index 0000000000000..93ad40026dff8 --- /dev/null +++ b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/wait_for_visualizations.ts @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import { ServerFacade } from '../../../../types'; +import { HeadlessChromiumDriver as HeadlessBrowser } from '../../../../server/browsers'; +import { LevelLogger } from '../../../../server/lib'; +import { LayoutInstance } from '../../layouts/layout'; +import { CONTEXT_WAITFORELEMENTSTOBEINDOM } from './constants'; + +type SelectorArgs = Record; + +const getCompletedItemsCount = ({ renderCompleteSelector }: SelectorArgs) => { + return document.querySelectorAll(renderCompleteSelector).length; +}; + +/* + * 1. Wait for the visualization metadata to be found in the DOM + * 2. Read the metadata for the number of visualization items + * 3. Wait for the render complete event to be fired once for each item + */ +export const waitForVisualizations = async ( + server: ServerFacade, + browser: HeadlessBrowser, + itemsCount: number, + layout: LayoutInstance, + logger: LevelLogger +): Promise => { + const config = server.config(); + const { renderComplete: renderCompleteSelector } = layout.selectors; + + logger.debug( + i18n.translate('xpack.reporting.screencapture.waitingForRenderedElements', { + defaultMessage: `waiting for {itemsCount} rendered elements to be in the DOM`, + values: { itemsCount }, + }) + ); + + try { + await browser.waitFor( + { + fn: getCompletedItemsCount, + args: [{ renderCompleteSelector }], + toEqual: itemsCount, + timeout: config.get('xpack.reporting.capture.timeouts.renderComplete'), + }, + { context: CONTEXT_WAITFORELEMENTSTOBEINDOM }, + logger + ); + + logger.debug(`found ${itemsCount} rendered elements in the DOM`); + } catch (err) { + throw new Error( + i18n.translate('xpack.reporting.screencapture.couldntFinishRendering', { + defaultMessage: `An error occurred when trying to wait for {count} visualizations to finish rendering. You may need to increase '{configKey}'. {error}`, + values: { + count: itemsCount, + configKey: 'xpack.reporting.capture.timeouts.renderComplete', + error: err, + }, + }) + ); + } +}; diff --git a/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.test.js b/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.test.js index c0c21119e1d53..e2e6ba1b89096 100644 --- a/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.test.js +++ b/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.test.js @@ -114,7 +114,7 @@ test(`returns content of generatePng getBuffer base64 encoded`, async () => { const testContent = 'test content'; const generatePngObservable = generatePngObservableFactory(); - generatePngObservable.mockReturnValue(Rx.of(Buffer.from(testContent))); + generatePngObservable.mockReturnValue(Rx.of({ buffer: Buffer.from(testContent) })); const executeJob = await executeJobFactory( mockReporting, diff --git a/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.ts b/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.ts index 5cde245080914..8670f0027af89 100644 --- a/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.ts +++ b/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.ts @@ -4,17 +4,23 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as Rx from 'rxjs'; import { ElasticsearchServiceSetup } from 'kibana/server'; +import * as Rx from 'rxjs'; import { catchError, map, mergeMap, takeUntil } from 'rxjs/operators'; -import { ReportingCore } from '../../../../server'; import { PNG_JOB_TYPE } from '../../../../common/constants'; -import { ServerFacade, ExecuteJobFactory, ESQueueWorkerExecuteFn, Logger } from '../../../../types'; +import { ReportingCore } from '../../../../server'; +import { + ESQueueWorkerExecuteFn, + ExecuteJobFactory, + JobDocOutput, + Logger, + ServerFacade, +} from '../../../../types'; import { decryptJobHeaders, - omitBlacklistedHeaders, getConditionalHeaders, getFullUrls, + omitBlacklistedHeaders, } from '../../../common/execute_job/'; import { JobDocPayloadPNG } from '../../types'; import { generatePngObservableFactory } from '../lib/generate_png'; @@ -33,7 +39,7 @@ export const executeJobFactory: QueuedPngExecutorFactory = async function execut return function executeJob(jobId: string, job: JobDocPayloadPNG, cancellationToken: any) { const jobLogger = logger.clone([jobId]); - const process$ = Rx.of(1).pipe( + const process$: Rx.Observable = Rx.of(1).pipe( mergeMap(() => decryptJobHeaders({ server, job, logger })), map(decryptedHeaders => omitBlacklistedHeaders({ job, decryptedHeaders })), map(filteredHeaders => getConditionalHeaders({ server, job, filteredHeaders })), @@ -48,11 +54,12 @@ export const executeJobFactory: QueuedPngExecutorFactory = async function execut job.layout ); }), - map((buffer: Buffer) => { + map(({ buffer, warnings }) => { return { content_type: 'image/png', content: buffer.toString('base64'), size: buffer.byteLength, + warnings, }; }), catchError(err => { diff --git a/x-pack/legacy/plugins/reporting/export_types/png/server/lib/generate_png.ts b/x-pack/legacy/plugins/reporting/export_types/png/server/lib/generate_png.ts index 600762c451a79..88e91982adc63 100644 --- a/x-pack/legacy/plugins/reporting/export_types/png/server/lib/generate_png.ts +++ b/x-pack/legacy/plugins/reporting/export_types/png/server/lib/generate_png.ts @@ -7,10 +7,11 @@ import * as Rx from 'rxjs'; import { map } from 'rxjs/operators'; import { LevelLogger } from '../../../../server/lib'; -import { ServerFacade, HeadlessChromiumDriverFactory, ConditionalHeaders } from '../../../../types'; -import { screenshotsObservableFactory } from '../../../common/lib/screenshots'; -import { PreserveLayout } from '../../../common/layouts/preserve_layout'; +import { ConditionalHeaders, HeadlessChromiumDriverFactory, ServerFacade } from '../../../../types'; import { LayoutParams } from '../../../common/layouts/layout'; +import { PreserveLayout } from '../../../common/layouts/preserve_layout'; +import { screenshotsObservableFactory } from '../../../common/lib/screenshots'; +import { ScreenshotResults } from '../../../common/lib/screenshots/types'; export function generatePngObservableFactory( server: ServerFacade, @@ -24,7 +25,7 @@ export function generatePngObservableFactory( browserTimezone: string, conditionalHeaders: ConditionalHeaders, layoutParams: LayoutParams - ): Rx.Observable { + ): Rx.Observable<{ buffer: Buffer; warnings: string[] }> { if (!layoutParams || !layoutParams.dimensions) { throw new Error(`LayoutParams.Dimensions is undefined.`); } @@ -37,12 +38,16 @@ export function generatePngObservableFactory( layout, browserTimezone, }).pipe( - map(([{ screenshots }]) => { - if (screenshots.length !== 1) { - throw new Error(`Expected there to be 1 screenshot, but there are ${screenshots.length}`); - } - - return screenshots[0].base64EncodedData; + map((results: ScreenshotResults[]) => { + return { + buffer: results[0].screenshots[0].base64EncodedData, + warnings: results.reduce((found, current) => { + if (current.error) { + found.push(current.error.message); + } + return found; + }, [] as string[]), + }; }) ); diff --git a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.test.js b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.test.js index ebd3e8a403b53..d2a6bfafce613 100644 --- a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.test.js +++ b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.test.js @@ -82,7 +82,7 @@ test(`returns content of generatePdf getBuffer base64 encoded`, async () => { const testContent = 'test content'; const generatePdfObservable = generatePdfObservableFactory(); - generatePdfObservable.mockReturnValue(Rx.of(Buffer.from(testContent))); + generatePdfObservable.mockReturnValue(Rx.of({ buffer: Buffer.from(testContent) })); const executeJob = await executeJobFactory( mockReporting, diff --git a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.ts b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.ts index e8461862bee82..535c2dcd439a7 100644 --- a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.ts +++ b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.ts @@ -4,21 +4,27 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as Rx from 'rxjs'; import { ElasticsearchServiceSetup } from 'kibana/server'; +import * as Rx from 'rxjs'; import { catchError, map, mergeMap, takeUntil } from 'rxjs/operators'; -import { ReportingCore } from '../../../../server'; -import { ServerFacade, ExecuteJobFactory, ESQueueWorkerExecuteFn, Logger } from '../../../../types'; -import { JobDocPayloadPDF } from '../../types'; import { PDF_JOB_TYPE } from '../../../../common/constants'; -import { generatePdfObservableFactory } from '../lib/generate_pdf'; +import { ReportingCore } from '../../../../server'; +import { + ESQueueWorkerExecuteFn, + ExecuteJobFactory, + JobDocOutput, + Logger, + ServerFacade, +} from '../../../../types'; import { decryptJobHeaders, - omitBlacklistedHeaders, getConditionalHeaders, - getFullUrls, getCustomLogo, + getFullUrls, + omitBlacklistedHeaders, } from '../../../common/execute_job/'; +import { JobDocPayloadPDF } from '../../types'; +import { generatePdfObservableFactory } from '../lib/generate_pdf'; type QueuedPdfExecutorFactory = ExecuteJobFactory>; @@ -34,8 +40,7 @@ export const executeJobFactory: QueuedPdfExecutorFactory = async function execut return function executeJob(jobId: string, job: JobDocPayloadPDF, cancellationToken: any) { const jobLogger = logger.clone([jobId]); - - const process$ = Rx.of(1).pipe( + const process$: Rx.Observable = Rx.of(1).pipe( mergeMap(() => decryptJobHeaders({ server, job, logger })), map(decryptedHeaders => omitBlacklistedHeaders({ job, decryptedHeaders })), map(filteredHeaders => getConditionalHeaders({ server, job, filteredHeaders })), @@ -54,10 +59,11 @@ export const executeJobFactory: QueuedPdfExecutorFactory = async function execut logo ); }), - map((buffer: Buffer) => ({ + map(({ buffer, warnings }) => ({ content_type: 'application/pdf', content: buffer.toString('base64'), size: buffer.byteLength, + warnings, })), catchError(err => { jobLogger.error(err); diff --git a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/generate_pdf.ts b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/generate_pdf.ts index 9a8db308bea79..d78effaa1fc2f 100644 --- a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/generate_pdf.ts +++ b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/generate_pdf.ts @@ -4,17 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ +import { groupBy } from 'lodash'; import * as Rx from 'rxjs'; import { mergeMap } from 'rxjs/operators'; -import { groupBy } from 'lodash'; import { LevelLogger } from '../../../../server/lib'; -import { ServerFacade, HeadlessChromiumDriverFactory, ConditionalHeaders } from '../../../../types'; -// @ts-ignore untyped module -import { pdf } from './pdf'; -import { screenshotsObservableFactory } from '../../../common/lib/screenshots'; +import { ConditionalHeaders, HeadlessChromiumDriverFactory, ServerFacade } from '../../../../types'; import { createLayout } from '../../../common/layouts'; -import { ScreenshotResults } from '../../../common/lib/screenshots/types'; import { LayoutInstance, LayoutParams } from '../../../common/layouts/layout'; +import { screenshotsObservableFactory } from '../../../common/lib/screenshots'; +import { ScreenshotResults } from '../../../common/lib/screenshots/types'; +// @ts-ignore untyped module +import { pdf } from './pdf'; const getTimeRange = (urlScreenshots: ScreenshotResults[]) => { const grouped = groupBy(urlScreenshots.map(u => u.timeRange)); @@ -40,7 +40,7 @@ export function generatePdfObservableFactory( conditionalHeaders: ConditionalHeaders, layoutParams: LayoutParams, logo?: string - ): Rx.Observable { + ): Rx.Observable<{ buffer: Buffer; warnings: string[] }> { const layout = createLayout(server, layoutParams) as LayoutInstance; const screenshots$ = screenshotsObservable({ logger, @@ -49,17 +49,17 @@ export function generatePdfObservableFactory( layout, browserTimezone, }).pipe( - mergeMap(async urlScreenshots => { + mergeMap(async (results: ScreenshotResults[]) => { const pdfOutput = pdf.create(layout, logo); if (title) { - const timeRange = getTimeRange(urlScreenshots); + const timeRange = getTimeRange(results); title += timeRange ? ` - ${timeRange.duration}` : ''; pdfOutput.setTitle(title); } - urlScreenshots.forEach(({ screenshots }) => { - screenshots.forEach(screenshot => { + results.forEach(r => { + r.screenshots.forEach(screenshot => { pdfOutput.addImage(screenshot.base64EncodedData, { title: screenshot.title, description: screenshot.description, @@ -68,7 +68,16 @@ export function generatePdfObservableFactory( }); pdfOutput.generate(); - return await pdfOutput.getBuffer(); + + return { + buffer: await pdfOutput.getBuffer(), + warnings: results.reduce((found, current) => { + if (current.error) { + found.push(current.error.message); + } + return found; + }, [] as string[]), + }; }) ); diff --git a/x-pack/legacy/plugins/reporting/public/components/report_info_button.tsx b/x-pack/legacy/plugins/reporting/public/components/report_info_button.tsx index 77869c40d3577..7f5d070948e50 100644 --- a/x-pack/legacy/plugins/reporting/public/components/report_info_button.tsx +++ b/x-pack/legacy/plugins/reporting/public/components/report_info_button.tsx @@ -86,95 +86,102 @@ export class ReportInfoButton extends Component { const maxAttempts = info.max_attempts ? info.max_attempts.toString() : NA; const priority = info.priority ? info.priority.toString() : NA; const timeout = info.timeout ? info.timeout.toString() : NA; + const warnings = info.output && info.output.warnings ? info.output.warnings.join(',') : null; + + const jobInfoDateTimes: JobInfo[] = [ + { + title: 'Created By', + description: info.created_by || NA, + }, + { + title: 'Created At', + description: info.created_at || NA, + }, + { + title: 'Started At', + description: info.started_at || NA, + }, + { + title: 'Completed At', + description: info.completed_at || NA, + }, + { + title: 'Processed By', + description: + info.kibana_name && info.kibana_id ? `${info.kibana_name} (${info.kibana_id})` : UNKNOWN, + }, + { + title: 'Browser Timezone', + description: get(info, 'payload.browserTimezone') || NA, + }, + ]; + const jobInfoPayload: JobInfo[] = [ + { + title: 'Title', + description: get(info, 'payload.title') || NA, + }, + { + title: 'Type', + description: get(info, 'payload.type') || NA, + }, + { + title: 'Layout', + description: get(info, 'meta.layout') || NA, + }, + { + title: 'Dimensions', + description: getDimensions(info), + }, + { + title: 'Job Type', + description: jobType, + }, + { + title: 'Content Type', + description: get(info, 'output.content_type') || NA, + }, + { + title: 'Size in Bytes', + description: get(info, 'output.size') || NA, + }, + ]; + const jobInfoStatus: JobInfo[] = [ + { + title: 'Attempts', + description: attempts, + }, + { + title: 'Max Attempts', + description: maxAttempts, + }, + { + title: 'Priority', + description: priority, + }, + { + title: 'Timeout', + description: timeout, + }, + { + title: 'Status', + description: info.status || NA, + }, + { + title: 'Browser Type', + description: USES_HEADLESS_JOB_TYPES.includes(jobType) ? info.browser_type || UNKNOWN : NA, + }, + ]; + if (warnings) { + jobInfoStatus.push({ + title: 'Errors', + description: warnings, + }); + } const jobInfoParts: JobInfoMap = { - datetimes: [ - { - title: 'Created By', - description: info.created_by || NA, - }, - { - title: 'Created At', - description: info.created_at || NA, - }, - { - title: 'Started At', - description: info.started_at || NA, - }, - { - title: 'Completed At', - description: info.completed_at || NA, - }, - { - title: 'Processed By', - description: - info.kibana_name && info.kibana_id - ? `${info.kibana_name} (${info.kibana_id})` - : UNKNOWN, - }, - { - title: 'Browser Timezone', - description: get(info, 'payload.browserTimezone') || NA, - }, - ], - payload: [ - { - title: 'Title', - description: get(info, 'payload.title') || NA, - }, - { - title: 'Type', - description: get(info, 'payload.type') || NA, - }, - { - title: 'Layout', - description: get(info, 'meta.layout') || NA, - }, - { - title: 'Dimensions', - description: getDimensions(info), - }, - { - title: 'Job Type', - description: jobType, - }, - { - title: 'Content Type', - description: get(info, 'output.content_type') || NA, - }, - { - title: 'Size in Bytes', - description: get(info, 'output.size') || NA, - }, - ], - status: [ - { - title: 'Attempts', - description: attempts, - }, - { - title: 'Max Attempts', - description: maxAttempts, - }, - { - title: 'Priority', - description: priority, - }, - { - title: 'Timeout', - description: timeout, - }, - { - title: 'Status', - description: info.status || NA, - }, - { - title: 'Browser Type', - description: USES_HEADLESS_JOB_TYPES.includes(jobType) - ? info.browser_type || UNKNOWN - : NA, - }, - ], + datetimes: jobInfoDateTimes, + payload: jobInfoPayload, + status: jobInfoStatus, }; return ( diff --git a/x-pack/legacy/plugins/reporting/public/components/report_listing.tsx b/x-pack/legacy/plugins/reporting/public/components/report_listing.tsx index 9783372aa29c4..c0fe33a3a542d 100644 --- a/x-pack/legacy/plugins/reporting/public/components/report_listing.tsx +++ b/x-pack/legacy/plugins/reporting/public/components/report_listing.tsx @@ -43,6 +43,7 @@ interface Job { attempts: number; max_attempts: number; csv_contains_formulas: boolean; + warnings: string[]; } interface Props { @@ -203,7 +204,7 @@ class ReportListingUi extends Component { return (
@@ -215,13 +216,27 @@ class ReportListingUi extends Component { maxSizeReached = ( ); } + let warnings; + if (record.warnings) { + warnings = ( + + + + + + ); + } + let statusTimestamp; if (status === JobStatuses.PROCESSING && record.started_at) { statusTimestamp = this.formatDate(record.started_at); @@ -242,7 +257,7 @@ class ReportListingUi extends Component { return (
{ }} /> {maxSizeReached} + {warnings}
); } @@ -259,6 +275,7 @@ class ReportListingUi extends Component {
{statusLabel} {maxSizeReached} + {warnings}
); }, @@ -437,6 +454,7 @@ class ReportListingUi extends Component { attempts: source.attempts, max_attempts: source.max_attempts, csv_contains_formulas: get(source, 'output.csv_contains_formulas'), + warnings: source.output ? source.output.warnings : undefined, }; } ), diff --git a/x-pack/legacy/plugins/reporting/public/lib/job_queue_client.ts b/x-pack/legacy/plugins/reporting/public/lib/job_queue_client.ts index 281a2e1cdf9a5..87d4174168b7f 100644 --- a/x-pack/legacy/plugins/reporting/public/lib/job_queue_client.ts +++ b/x-pack/legacy/plugins/reporting/public/lib/job_queue_client.ts @@ -31,6 +31,7 @@ export interface JobInfo { output: { content_type: string; size: number; + warnings: string[]; }; process_expiration: string; completed_at: string; diff --git a/x-pack/legacy/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts b/x-pack/legacy/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts index 0592124b9897b..60799e3e918b8 100644 --- a/x-pack/legacy/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts +++ b/x-pack/legacy/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts @@ -4,13 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { trunc, map } from 'lodash'; +import { i18n } from '@kbn/i18n'; +import { map, trunc } from 'lodash'; import open from 'opn'; +import { ElementHandle, EvaluateFn, Page, SerializableOrJSHandle } from 'puppeteer'; import { parse as parseUrl } from 'url'; -import { Page, SerializableOrJSHandle, EvaluateFn } from 'puppeteer'; import { ViewZoomWidthHeight } from '../../../../export_types/common/layouts/layout'; import { LevelLogger } from '../../../../server/lib'; -import { allowRequest } from '../../network_policy'; import { ConditionalHeaders, ConditionalHeadersConditions, @@ -18,6 +18,7 @@ import { InterceptedRequest, NetworkPolicy, } from '../../../../types'; +import { allowRequest } from '../../network_policy'; export interface ChromiumDriverOptions { inspect: boolean; @@ -25,7 +26,7 @@ export interface ChromiumDriverOptions { } interface WaitForSelectorOpts { - silent?: boolean; + timeout: number; } interface EvaluateOpts { @@ -65,10 +66,15 @@ export class HeadlessChromiumDriver { url: string, { conditionalHeaders, - waitForSelector, - }: { conditionalHeaders: ConditionalHeaders; waitForSelector: string }, + waitForSelector: pageLoadSelector, + timeout, + }: { + conditionalHeaders: ConditionalHeaders; + waitForSelector: string; + timeout: number; + }, logger: LevelLogger - ) { + ): Promise { logger.info(`opening url ${url}`); // @ts-ignore const client = this.page._client; @@ -81,7 +87,7 @@ export class HeadlessChromiumDriver { // https://github.com/puppeteer/puppeteer/issues/5003 // Docs on this client/protocol can be found here: // https://chromedevtools.github.io/devtools-protocol/tot/Fetch - client.on('Fetch.requestPaused', (interceptedRequest: InterceptedRequest) => { + client.on('Fetch.requestPaused', async (interceptedRequest: InterceptedRequest) => { const { requestId, request: { url: interceptedUrl }, @@ -92,12 +98,17 @@ export class HeadlessChromiumDriver { // We should never ever let file protocol requests go through if (!allowed || !this.allowRequest(interceptedUrl)) { logger.error(`Got bad URL: "${interceptedUrl}", closing browser.`); - client.send('Fetch.failRequest', { + await client.send('Fetch.failRequest', { errorReason: 'Aborted', requestId, }); this.page.browser().close(); - throw new Error(`Received disallowed outgoing URL: "${interceptedUrl}", exiting`); + throw new Error( + i18n.translate('xpack.reporting.chromiumDriver.disallowedOutgoingUrl', { + defaultMessage: `Received disallowed outgoing URL: "{interceptedUrl}", exiting`, + values: { interceptedUrl }, + }) + ); } if (this._shouldUseCustomHeaders(conditionalHeaders.conditions, interceptedUrl)) { @@ -112,14 +123,33 @@ export class HeadlessChromiumDriver { value, }) ); - client.send('Fetch.continueRequest', { - requestId, - headers, - }); + + try { + await client.send('Fetch.continueRequest', { + requestId, + headers, + }); + } catch (err) { + logger.error( + i18n.translate('xpack.reporting.chromiumDriver.failedToCompleteRequestUsingHeaders', { + defaultMessage: 'Failed to complete a request using headers: {error}', + values: { error: err }, + }) + ); + } } else { const loggedUrl = isData ? this.truncateUrl(interceptedUrl) : interceptedUrl; logger.debug(`No custom headers for ${loggedUrl}`); - client.send('Fetch.continueRequest', { requestId }); + try { + await client.send('Fetch.continueRequest', { requestId }); + } catch (err) { + logger.error( + i18n.translate('xpack.reporting.chromiumDriver.failedToCompleteRequest', { + defaultMessage: 'Failed to complete a request: {error}', + values: { error: err }, + }) + ); + } } interceptedCount = interceptedCount + (isData ? 0 : 1); }); @@ -144,11 +174,16 @@ export class HeadlessChromiumDriver { await this.launchDebugger(); } - await this.waitForSelector(waitForSelector, {}, logger); + await this.waitForSelector( + pageLoadSelector, + { timeout }, + { context: 'waiting for page load selector' }, + logger + ); logger.info(`handled ${interceptedCount} page requests`); } - public async screenshot(elementPosition: ElementPosition) { + public async screenshot(elementPosition: ElementPosition): Promise { let clip; if (elementPosition) { const { boundingClientRect, scroll = { x: 0, y: 0 } } = elementPosition; @@ -176,63 +211,56 @@ export class HeadlessChromiumDriver { const result = await this.page.evaluate(fn, ...args); return result; } + public async waitForSelector( selector: string, - opts: WaitForSelectorOpts = {}, + opts: WaitForSelectorOpts, + context: EvaluateMetaOpts, logger: LevelLogger - ) { - const { silent = false } = opts; + ): Promise> { + const { timeout } = opts; logger.debug(`waitForSelector ${selector}`); - - let resp; - try { - resp = await this.page.waitFor(selector); - } catch (err) { - if (!silent) { - // Provide some troubleshooting info to see if we're on the login page, - // "Kibana could not load correctly", etc - logger.error(`waitForSelector ${selector} failed on ${this.page.url()}`); - const pageText = await this.evaluate( - { - fn: () => document.querySelector('body')!.innerText, - args: [], - }, - { context: `waitForSelector${selector}` }, - logger - ); - logger.debug(`Page plain text: ${pageText.replace(/\n/g, '\\n')}`); // replace newline with escaped for single log line - } - throw err; - } - + const resp = await this.page.waitFor(selector, { timeout }); // override default 30000ms logger.debug(`waitForSelector ${selector} resolved`); return resp; } - public async waitFor( + public async waitFor( { fn, args, toEqual, + timeout, }: { fn: EvaluateFn; args: SerializableOrJSHandle[]; - toEqual: T; + toEqual: number; + timeout: number; }, context: EvaluateMetaOpts, logger: LevelLogger - ) { + ): Promise { + const startTime = Date.now(); + while (true) { const result = await this.evaluate({ fn, args }, context, logger); if (result === toEqual) { return; } + if (Date.now() - startTime > timeout) { + throw new Error( + `Timed out waiting for the items selected to equal ${toEqual}. Found: ${result}. Context: ${context.context}` + ); + } await new Promise(r => setTimeout(r, WAIT_FOR_DELAY_MS)); } } - public async setViewport({ width, height, zoom }: ViewZoomWidthHeight, logger: LevelLogger) { + public async setViewport( + { width, height, zoom }: ViewZoomWidthHeight, + logger: LevelLogger + ): Promise { logger.debug(`Setting viewport to width: ${width}, height: ${height}, zoom: ${zoom}`); await this.page.setViewport({ diff --git a/x-pack/legacy/plugins/reporting/server/browsers/chromium/driver_factory/index.ts b/x-pack/legacy/plugins/reporting/server/browsers/chromium/driver_factory/index.ts index 6fa46b893de8c..1a57408f41dd6 100644 --- a/x-pack/legacy/plugins/reporting/server/browsers/chromium/driver_factory/index.ts +++ b/x-pack/legacy/plugins/reporting/server/browsers/chromium/driver_factory/index.ts @@ -18,7 +18,7 @@ import * as Rx from 'rxjs'; import { ignoreElements, map, mergeMap, tap } from 'rxjs/operators'; import { InnerSubscriber } from 'rxjs/internal/InnerSubscriber'; -import { BrowserConfig, NetworkPolicy } from '../../../../types'; +import { BrowserConfig, CaptureConfig } from '../../../../types'; import { LevelLogger as Logger } from '../../../lib/level_logger'; import { HeadlessChromiumDriver } from '../driver'; import { safeChildProcess } from '../../safe_child_process'; @@ -27,30 +27,27 @@ import { getChromeLogLocation } from '../paths'; import { args } from './args'; type binaryPath = string; -type queueTimeout = number; +type ViewportConfig = BrowserConfig['viewport']; export class HeadlessChromiumDriverFactory { private binaryPath: binaryPath; + private captureConfig: CaptureConfig; private browserConfig: BrowserConfig; - private queueTimeout: queueTimeout; - private networkPolicy: NetworkPolicy; private userDataDir: string; - private getChromiumArgs: (viewport: BrowserConfig['viewport']) => string[]; + private getChromiumArgs: (viewport: ViewportConfig) => string[]; constructor( binaryPath: binaryPath, logger: Logger, browserConfig: BrowserConfig, - queueTimeout: queueTimeout, - networkPolicy: NetworkPolicy + captureConfig: CaptureConfig ) { this.binaryPath = binaryPath; this.browserConfig = browserConfig; - this.queueTimeout = queueTimeout; - this.networkPolicy = networkPolicy; + this.captureConfig = captureConfig; this.userDataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'chromium-')); - this.getChromiumArgs = (viewport: BrowserConfig['viewport']) => + this.getChromiumArgs = (viewport: ViewportConfig) => args({ userDataDir: this.userDataDir, viewport, @@ -88,7 +85,7 @@ export class HeadlessChromiumDriverFactory { * Return an observable to objects which will drive screenshot capture for a page */ createPage( - { viewport, browserTimezone }: { viewport: BrowserConfig['viewport']; browserTimezone: string }, + { viewport, browserTimezone }: { viewport: ViewportConfig; browserTimezone: string }, pLogger: Logger ): Rx.Observable<{ driver: HeadlessChromiumDriver; exit$: Rx.Observable }> { return Rx.Observable.create(async (observer: InnerSubscriber) => { @@ -113,11 +110,9 @@ export class HeadlessChromiumDriverFactory { page = await browser.newPage(); - // All navigation/waitFor methods default to 30 seconds, - // which can cause the job to fail even if we bump timeouts in - // the config. Help alleviate errors like - // "TimeoutError: waiting for selector ".application" failed: timeout 30000ms exceeded" - page.setDefaultTimeout(this.queueTimeout); + // Set the default timeout for all navigation methods to the openUrl timeout (30 seconds) + // All waitFor methods have their own timeout config passed in to them + page.setDefaultTimeout(this.captureConfig.timeouts.openUrl); logger.debug(`Browser page driver created`); } catch (err) { @@ -158,7 +153,7 @@ export class HeadlessChromiumDriverFactory { // HeadlessChromiumDriver: object to "drive" a browser page const driver = new HeadlessChromiumDriver(page, { inspect: this.browserConfig.inspect, - networkPolicy: this.networkPolicy, + networkPolicy: this.captureConfig.networkPolicy, }); // Rx.Observable: stream to interrupt page capture diff --git a/x-pack/legacy/plugins/reporting/server/browsers/chromium/index.ts b/x-pack/legacy/plugins/reporting/server/browsers/chromium/index.ts index d5f7027e025d4..d32338ae3e311 100644 --- a/x-pack/legacy/plugins/reporting/server/browsers/chromium/index.ts +++ b/x-pack/legacy/plugins/reporting/server/browsers/chromium/index.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { BrowserConfig, NetworkPolicy } from '../../../types'; +import { BrowserConfig, CaptureConfig } from '../../../types'; import { LevelLogger } from '../../lib'; import { HeadlessChromiumDriverFactory } from './driver_factory'; @@ -14,14 +14,7 @@ export async function createDriverFactory( binaryPath: string, logger: LevelLogger, browserConfig: BrowserConfig, - queueTimeout: number, - networkPolicy: NetworkPolicy + captureConfig: CaptureConfig ): Promise { - return new HeadlessChromiumDriverFactory( - binaryPath, - logger, - browserConfig, - queueTimeout, - networkPolicy - ); + return new HeadlessChromiumDriverFactory(binaryPath, logger, browserConfig, captureConfig); } diff --git a/x-pack/legacy/plugins/reporting/server/browsers/create_browser_driver_factory.ts b/x-pack/legacy/plugins/reporting/server/browsers/create_browser_driver_factory.ts index 128df4d318c76..49c6222c9f276 100644 --- a/x-pack/legacy/plugins/reporting/server/browsers/create_browser_driver_factory.ts +++ b/x-pack/legacy/plugins/reporting/server/browsers/create_browser_driver_factory.ts @@ -22,8 +22,6 @@ export async function createBrowserDriverFactory( const browserType = captureConfig.browser.type; const browserAutoDownload = captureConfig.browser.autoDownload; const browserConfig = captureConfig.browser[BROWSER_TYPE]; - const networkPolicy = captureConfig.networkPolicy; - const reportingTimeout: number = config.get('xpack.reporting.queue.timeout'); if (browserConfig.disableSandbox) { logger.warning(`Enabling the Chromium sandbox provides an additional layer of protection.`); @@ -34,13 +32,7 @@ export async function createBrowserDriverFactory( try { const { binaryPath } = await installBrowser(logger, chromium, dataDir); - return chromium.createDriverFactory( - binaryPath, - logger, - browserConfig, - reportingTimeout, - networkPolicy - ); + return chromium.createDriverFactory(binaryPath, logger, browserConfig, captureConfig); } catch (error) { if (error.cause && ['EACCES', 'EEXIST'].includes(error.cause.code)) { logger.error( diff --git a/x-pack/legacy/plugins/reporting/server/lib/esqueue/worker.js b/x-pack/legacy/plugins/reporting/server/lib/esqueue/worker.js index cea60b46818f0..009775b7253b4 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/esqueue/worker.js +++ b/x-pack/legacy/plugins/reporting/server/lib/esqueue/worker.js @@ -226,8 +226,10 @@ export class Worker extends events.EventEmitter { docOutput.content = output.content; docOutput.content_type = output.content_type || unknownMime; docOutput.max_size_reached = output.max_size_reached; - docOutput.size = output.size; docOutput.csv_contains_formulas = output.csv_contains_formulas; + docOutput.size = output.size; + docOutput.warnings = + output.warnings && output.warnings.length > 0 ? output.warnings : undefined; } else { docOutput.content = output || defaultOutput; docOutput.content_type = unknownMime; @@ -248,7 +250,11 @@ export class Worker extends events.EventEmitter { Promise.resolve(this.workerFn.call(null, job, jobSource.payload, cancellationToken)) .then(res => { // job execution was successful - this.info(`Job execution completed successfully`); + if (res && res.warnings && res.warnings.length > 0) { + this.warn(`Job execution completed with warnings`); + } else { + this.info(`Job execution completed successfully`); + } isResolved = true; resolve(res); diff --git a/x-pack/legacy/plugins/reporting/test_helpers/create_mock_browserdriverfactory.ts b/x-pack/legacy/plugins/reporting/test_helpers/create_mock_browserdriverfactory.ts index 6d9ae2153255f..883276d43e27e 100644 --- a/x-pack/legacy/plugins/reporting/test_helpers/create_mock_browserdriverfactory.ts +++ b/x-pack/legacy/plugins/reporting/test_helpers/create_mock_browserdriverfactory.ts @@ -10,7 +10,7 @@ import * as contexts from '../export_types/common/lib/screenshots/constants'; import { ElementsPositionAndAttribute } from '../export_types/common/lib/screenshots/types'; import { HeadlessChromiumDriver, HeadlessChromiumDriverFactory } from '../server/browsers'; import { createDriverFactory } from '../server/browsers/chromium'; -import { BrowserConfig, Logger, NetworkPolicy } from '../types'; +import { BrowserConfig, CaptureConfig, Logger } from '../types'; interface CreateMockBrowserDriverFactoryOpts { evaluate: jest.Mock, any[]>; @@ -19,7 +19,7 @@ interface CreateMockBrowserDriverFactoryOpts { getCreatePage: (driver: HeadlessChromiumDriver) => jest.Mock; } -export const mockSelectors = { +const mockSelectors = { renderComplete: 'renderedSelector', itemsCountAttribute: 'itemsSelector', screenshot: 'screenshotSelector', @@ -73,9 +73,6 @@ mockBrowserEvaluate.mockImplementation(() => { if (mockCall === contexts.CONTEXT_ELEMENTATTRIBUTES) { return Promise.resolve(getMockElementsPositionAndAttributes('Default Mock Title', 'Default ')); } - if (mockCall === contexts.CONTEXT_CHECKFORTOASTMESSAGE) { - return Promise.resolve('Toast Message'); - } throw new Error(mockCall); }); const mockScreenshot = jest.fn(); @@ -105,19 +102,20 @@ export const createMockBrowserDriverFactory = async ( } as BrowserConfig; const binaryPath = '/usr/local/share/common/secure/'; - const queueTimeout = 55; - const networkPolicy = {} as NetworkPolicy; + const captureConfig = { networkPolicy: {}, timeouts: {} } as CaptureConfig; const mockBrowserDriverFactory = await createDriverFactory( binaryPath, logger, browserConfig, - queueTimeout, - networkPolicy + captureConfig ); const mockPage = {} as Page; - const mockBrowserDriver = new HeadlessChromiumDriver(mockPage, { inspect: true, networkPolicy }); + const mockBrowserDriver = new HeadlessChromiumDriver(mockPage, { + inspect: true, + networkPolicy: captureConfig.networkPolicy, + }); // mock the driver methods as either default mocks or passed-in mockBrowserDriver.waitForSelector = opts.waitForSelector ? opts.waitForSelector : defaultOpts.waitForSelector; // prettier-ignore diff --git a/x-pack/legacy/plugins/reporting/test_helpers/create_mock_layoutinstance.ts b/x-pack/legacy/plugins/reporting/test_helpers/create_mock_layoutinstance.ts index a2eb03c3fe300..0250e6c0a9afd 100644 --- a/x-pack/legacy/plugins/reporting/test_helpers/create_mock_layoutinstance.ts +++ b/x-pack/legacy/plugins/reporting/test_helpers/create_mock_layoutinstance.ts @@ -19,7 +19,6 @@ export const createMockLayoutInstance = (__LEGACY: ServerFacade) => { itemsCountAttribute: 'itemsSelector', screenshot: 'screenshotSelector', timefilterDurationAttribute: 'timefilterDurationSelector', - toastHeader: 'toastHeaderSelector', }; return mockLayout; }; diff --git a/x-pack/legacy/plugins/reporting/test_helpers/index.ts b/x-pack/legacy/plugins/reporting/test_helpers/index.ts index 91c348ba1db3d..491d390c370b9 100644 --- a/x-pack/legacy/plugins/reporting/test_helpers/index.ts +++ b/x-pack/legacy/plugins/reporting/test_helpers/index.ts @@ -6,5 +6,5 @@ export { createMockServer } from './create_mock_server'; export { createMockReportingCore } from './create_mock_reportingplugin'; -export { createMockBrowserDriverFactory, mockSelectors } from './create_mock_browserdriverfactory'; +export { createMockBrowserDriverFactory } from './create_mock_browserdriverfactory'; export { createMockLayoutInstance } from './create_mock_layoutinstance'; diff --git a/x-pack/legacy/plugins/reporting/types.d.ts b/x-pack/legacy/plugins/reporting/types.d.ts index d6bbb0a7cfca1..c088adf682095 100644 --- a/x-pack/legacy/plugins/reporting/types.d.ts +++ b/x-pack/legacy/plugins/reporting/types.d.ts @@ -122,6 +122,11 @@ export interface CaptureConfig { maxAttempts: number; networkPolicy: NetworkPolicy; loadDelay: number; + timeouts: { + openUrl: number; + waitForElements: number; + renderComplet: number; + }; } export interface BrowserConfig { @@ -219,8 +224,9 @@ export interface JobSource { export interface JobDocOutput { content_type: string; content: string | null; - max_size_reached: boolean; size: number; + max_size_reached?: boolean; + warnings?: string[]; } export interface ESQueueWorker { diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 2f4e0af6e2684..76d1a3e94e223 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -10154,7 +10154,6 @@ "xpack.reporting.exportTypes.printablePdf.documentStreamIsNotgeneratedErrorMessage": "ドキュメントストリームが生成されていません。", "xpack.reporting.exportTypes.printablePdf.logoDescription": "Elastic 提供", "xpack.reporting.exportTypes.printablePdf.pagingDescription": "{pageCount} ページ中 {currentPage} ページ目", - "xpack.reporting.exportTypes.printablePdf.screenshots.unexpectedErrorMessage": "ページで予期せぬメッセージが発生しました: {toastHeaderText}", "xpack.reporting.jobStatuses.cancelledText": "キャンセル済み", "xpack.reporting.jobStatuses.completedText": "完了", "xpack.reporting.jobStatuses.failedText": "失敗", @@ -10172,9 +10171,6 @@ "xpack.reporting.listing.tableColumns.createdAtTitle": "作成日時:", "xpack.reporting.listing.tableColumns.reportTitle": "レポート", "xpack.reporting.listing.tableColumns.statusTitle": "ステータス", - "xpack.reporting.listing.tableValue.createdAtDetail.maxSizeReachedText": " - 最大サイズに達成", - "xpack.reporting.listing.tableValue.createdAtDetail.pendingStatusReachedText": "保留中 - ジョブの処理持ち", - "xpack.reporting.listing.tableValue.createdAtDetail.statusTimestampText": "{statusTimestamp} 時点で {statusLabel}", "xpack.reporting.management.reportingTitle": "レポート", "xpack.reporting.panelContent.copyUrlButtonLabel": "POST URL をコピー", "xpack.reporting.panelContent.generateButtonLabel": "{reportingType} を生成", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index d00314e478259..af6fd082acbf6 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -10154,7 +10154,6 @@ "xpack.reporting.exportTypes.printablePdf.documentStreamIsNotgeneratedErrorMessage": "尚未生成文档流", "xpack.reporting.exportTypes.printablePdf.logoDescription": "由 Elastic 提供支持", "xpack.reporting.exportTypes.printablePdf.pagingDescription": "第 {currentPage} 页,共 {pageCount} 页", - "xpack.reporting.exportTypes.printablePdf.screenshots.unexpectedErrorMessage": "在页面上出现意外消息:{toastHeaderText}", "xpack.reporting.jobStatuses.cancelledText": "已取消", "xpack.reporting.jobStatuses.completedText": "已完成", "xpack.reporting.jobStatuses.failedText": "失败", @@ -10172,9 +10171,6 @@ "xpack.reporting.listing.tableColumns.createdAtTitle": "创建于", "xpack.reporting.listing.tableColumns.reportTitle": "报告", "xpack.reporting.listing.tableColumns.statusTitle": "状态", - "xpack.reporting.listing.tableValue.createdAtDetail.maxSizeReachedText": " - 最大大小已达到", - "xpack.reporting.listing.tableValue.createdAtDetail.pendingStatusReachedText": "待处理 - 正在等候处理作业", - "xpack.reporting.listing.tableValue.createdAtDetail.statusTimestampText": "{statusTimestamp} 时为 {statusLabel}", "xpack.reporting.management.reportingTitle": "报告", "xpack.reporting.panelContent.copyUrlButtonLabel": "复制 POST URL", "xpack.reporting.panelContent.generateButtonLabel": "生成 {reportingType}",