[Reporting] Capture browser errors (#127135)

* added some custom errors to the screenshotting plugin at keypoints

* add screenshotting errors file in common

* added new reporting error codes

* take the screenshot errors and map them to reporting errors, also added a test for the mapping logic

* remove unused import

* update error tests snapshots
This commit is contained in:
Jean-Louis Leysens 2022-03-10 14:59:03 +01:00 committed by GitHub
parent 7ef97181db
commit 714f3b2b91
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 131 additions and 19 deletions

View file

@ -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

View file

@ -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
);
});
});

View file

@ -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();
}

View file

@ -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}`;

View file

@ -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 {}

View file

@ -7,3 +7,6 @@
export type { LayoutParams } from './layout';
export { LayoutTypes } from './layout';
import * as errors from './errors';
export { errors };

View file

@ -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;
}

View file

@ -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';

View file

@ -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 {

View file

@ -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\) "/
);
});
});