diff --git a/x-pack/plugins/reporting/common/errors/index.ts b/x-pack/plugins/reporting/common/errors/index.ts index 0e84fa854f386..d5032c206a186 100644 --- a/x-pack/plugins/reporting/common/errors/index.ts +++ b/x-pack/plugins/reporting/common/errors/index.ts @@ -60,14 +60,39 @@ export class PdfWorkerOutOfMemoryError extends ReportingError { 'Cannot generate PDF due to low memory. Consider making a smaller PDF before retrying this report.', }); + /** + * No need to provide extra details, we know exactly what happened and can provide + * a nicely formatted message + */ public override get message(): string { return this.details; } } +export class BrowserCouldNotLaunchError extends ReportingError { + code = 'browser_could_not_launch_error'; + + details = i18n.translate('xpack.reporting.common.browserCouldNotLaunchErrorMessage', { + defaultMessage: 'Cannot generate screenshots because the browser did not launch.', + }); + + /** + * For this error message we expect that users will use the diagnostics + * functionality in reporting to debug further. + */ + public override get message() { + return this.details; + } +} + +export class BrowserUnexpectedlyClosedError extends ReportingError { + code = 'browser_unexpectedly_closed_error'; +} + +export class BrowserScreenshotError extends ReportingError { + code = 'browser_screenshot_error'; +} + export class KibanaShuttingDownError extends ReportingError { code = 'kibana_shutting_down_error'; } - -// TODO: Add ReportingError for missing Chromium dependencies -// TODO: Add ReportingError for Chromium not starting for an unknown reason diff --git a/x-pack/plugins/reporting/common/errors/map_to_reporting_error.test.ts b/x-pack/plugins/reporting/common/errors/map_to_reporting_error.test.ts new file mode 100644 index 0000000000000..5f2082d413778 --- /dev/null +++ b/x-pack/plugins/reporting/common/errors/map_to_reporting_error.test.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { mapToReportingError } from './map_to_reporting_error'; +import { errors } from '../../../screenshotting/common'; +import { + UnknownError, + BrowserCouldNotLaunchError, + BrowserUnexpectedlyClosedError, + BrowserScreenshotError, +} from '.'; + +describe('mapToReportingError', () => { + test('Non-Error values', () => { + [null, undefined, '', 0, false, true, () => {}, {}, []].forEach((v) => { + expect(mapToReportingError(v)).toBeInstanceOf(UnknownError); + }); + }); + + test('Screenshotting error', () => { + expect(mapToReportingError(new errors.BrowserClosedUnexpectedly())).toBeInstanceOf( + BrowserUnexpectedlyClosedError + ); + expect(mapToReportingError(new errors.FailedToCaptureScreenshot())).toBeInstanceOf( + BrowserScreenshotError + ); + expect(mapToReportingError(new errors.FailedToSpawnBrowserError())).toBeInstanceOf( + BrowserCouldNotLaunchError + ); + }); +}); diff --git a/x-pack/plugins/reporting/common/errors/map_to_reporting_error.ts b/x-pack/plugins/reporting/common/errors/map_to_reporting_error.ts new file mode 100644 index 0000000000000..ff24e684901fe --- /dev/null +++ b/x-pack/plugins/reporting/common/errors/map_to_reporting_error.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { errors } from '../../../screenshotting/common'; +import { + UnknownError, + ReportingError, + BrowserCouldNotLaunchError, + BrowserUnexpectedlyClosedError, + BrowserScreenshotError, +} from '.'; + +export function mapToReportingError(error: unknown): ReportingError { + if (error instanceof ReportingError) { + return error; + } + switch (true) { + case error instanceof errors.BrowserClosedUnexpectedly: + return new BrowserUnexpectedlyClosedError((error as Error).message); + case error instanceof errors.FailedToCaptureScreenshot: + return new BrowserScreenshotError((error as Error).message); + case error instanceof errors.FailedToSpawnBrowserError: + return new BrowserCouldNotLaunchError(); + } + return new UnknownError(); +} diff --git a/x-pack/plugins/reporting/server/lib/tasks/execute_report.ts b/x-pack/plugins/reporting/server/lib/tasks/execute_report.ts index 9007f5701f6f9..bd4c57437b353 100644 --- a/x-pack/plugins/reporting/server/lib/tasks/execute_report.ts +++ b/x-pack/plugins/reporting/server/lib/tasks/execute_report.ts @@ -20,12 +20,8 @@ import type { TaskRunCreatorFunction, } from '../../../../task_manager/server'; import { CancellationToken } from '../../../common/cancellation_token'; -import { - ReportingError, - UnknownError, - QueueTimeoutError, - KibanaShuttingDownError, -} from '../../../common/errors'; +import { mapToReportingError } from '../../../common/errors/map_to_reporting_error'; +import { ReportingError, QueueTimeoutError, KibanaShuttingDownError } from '../../../common/errors'; import { durationToNumber, numberToDuration } from '../../../common/schema_utils'; import type { ReportOutput } from '../../../common/types'; import type { ReportingConfigType } from '../../config'; @@ -238,7 +234,7 @@ export class ExecuteReportTask implements ReportingTask { const defaultOutput = null; docOutput.content = output.toString() || defaultOutput; docOutput.content_type = unknownMime; - docOutput.warnings = [output.details ?? output.toString()]; + docOutput.warnings = [output.toString()]; docOutput.error_code = output.code; } @@ -432,10 +428,7 @@ export class ExecuteReportTask implements ReportingTask { if (report == null) { throw new Error(`Report ${jobId} is null!`); } - const error = - failedToExecuteErr instanceof ReportingError - ? failedToExecuteErr - : new UnknownError(); + const error = mapToReportingError(failedToExecuteErr); error.details = error.details || `Max attempts (${attempts}) reached for job ${jobId}. Failed with: ${failedToExecuteErr.message}`; diff --git a/x-pack/plugins/screenshotting/common/errors.ts b/x-pack/plugins/screenshotting/common/errors.ts new file mode 100644 index 0000000000000..35082c7cc4071 --- /dev/null +++ b/x-pack/plugins/screenshotting/common/errors.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/* eslint-disable max-classes-per-file */ + +export class FailedToSpawnBrowserError extends Error {} + +export class BrowserClosedUnexpectedly extends Error {} + +export class FailedToCaptureScreenshot extends Error {} diff --git a/x-pack/plugins/screenshotting/common/index.ts b/x-pack/plugins/screenshotting/common/index.ts index 1492f3f945abe..d514eb507bb70 100644 --- a/x-pack/plugins/screenshotting/common/index.ts +++ b/x-pack/plugins/screenshotting/common/index.ts @@ -7,3 +7,6 @@ export type { LayoutParams } from './layout'; export { LayoutTypes } from './layout'; + +import * as errors from './errors'; +export { errors }; diff --git a/x-pack/plugins/screenshotting/server/browsers/chromium/driver_factory/index.ts b/x-pack/plugins/screenshotting/server/browsers/chromium/driver_factory/index.ts index 4664f31f51be4..27bb4e79374f7 100644 --- a/x-pack/plugins/screenshotting/server/browsers/chromium/driver_factory/index.ts +++ b/x-pack/plugins/screenshotting/server/browsers/chromium/driver_factory/index.ts @@ -29,6 +29,7 @@ import { import type { Logger } from 'src/core/server'; import type { ScreenshotModePluginSetup } from 'src/plugins/screenshot_mode/server'; import { ConfigType } from '../../../config'; +import { errors } from '../../../../common'; import { getChromiumDisconnectedError } from '../'; import { safeChildProcess } from '../../safe_child_process'; import { HeadlessChromiumDriver } from '../driver'; @@ -145,7 +146,6 @@ export class HeadlessChromiumDriverFactory { logger.debug(`Chromium launch args set to: ${chromiumArgs}`); let browser: Browser | undefined; - try { browser = await puppeteer.launch({ pipe: !this.config.browser.chromium.inspect, @@ -166,7 +166,9 @@ export class HeadlessChromiumDriverFactory { }, }); } catch (err) { - observer.error(new Error(`Error spawning Chromium browser! ${err}`)); + observer.error( + new errors.FailedToSpawnBrowserError(`Error spawning Chromium browser! ${err}`) + ); return; } diff --git a/x-pack/plugins/screenshotting/server/browsers/chromium/index.ts b/x-pack/plugins/screenshotting/server/browsers/chromium/index.ts index bc32e719e0f79..70fb9caa45125 100644 --- a/x-pack/plugins/screenshotting/server/browsers/chromium/index.ts +++ b/x-pack/plugins/screenshotting/server/browsers/chromium/index.ts @@ -5,8 +5,12 @@ * 2.0. */ +import { errors } from '../../../common'; + export const getChromiumDisconnectedError = () => - new Error('Browser was closed unexpectedly! Check the server logs for more info.'); + new errors.BrowserClosedUnexpectedly( + 'Browser was closed unexpectedly! Check the server logs for more info.' + ); export { HeadlessChromiumDriver } from './driver'; export type { Context } from './driver'; diff --git a/x-pack/plugins/screenshotting/server/screenshots/observable.ts b/x-pack/plugins/screenshotting/server/screenshots/observable.ts index a743c206ef98d..1c78d397a7415 100644 --- a/x-pack/plugins/screenshotting/server/screenshots/observable.ts +++ b/x-pack/plugins/screenshotting/server/screenshots/observable.ts @@ -9,6 +9,7 @@ import type { Transaction } from 'elastic-apm-node'; import { defer, forkJoin, throwError, Observable } from 'rxjs'; import { catchError, mergeMap, switchMapTo, timeoutWith } from 'rxjs/operators'; import type { Headers, Logger } from 'src/core/server'; +import { errors } from '../../common'; import type { Context, HeadlessChromiumDriver } from '../browsers'; import { getChromiumDisconnectedError, DEFAULT_VIEWPORT } from '../browsers'; import type { Layout } from '../layouts'; @@ -244,7 +245,12 @@ export class ScreenshotObservableHandler { const elements = data.elementsPositionAndAttributes ?? getDefaultElementPosition(this.layout.getViewport(1)); - const screenshots = await getScreenshots(this.driver, this.logger, elements); + let screenshots: Screenshot[] = []; + try { + screenshots = await getScreenshots(this.driver, this.logger, elements); + } catch (e) { + throw new errors.FailedToCaptureScreenshot(e.message); + } const { timeRange, error: setupError } = data; return { diff --git a/x-pack/test/reporting_api_integration/reporting_and_security/network_policy.ts b/x-pack/test/reporting_api_integration/reporting_and_security/network_policy.ts index 68899e65528ed..212b3f6de97c4 100644 --- a/x-pack/test/reporting_api_integration/reporting_and_security/network_policy.ts +++ b/x-pack/test/reporting_api_integration/reporting_and_security/network_policy.ts @@ -36,7 +36,7 @@ export default function ({ getService }: FtrProviderContext) { await retry.tryForTime(120000, async () => { const { body } = await supertest.get(downloadPath).expect(500); expect(body.message).to.match( - /Reporting generation failed: ReportingError\(code: unknown_error\) "/ + /Reporting generation failed: ReportingError\(code: browser_unexpectedly_closed_error\) "/ ); }); });